Skip to main content

Spooly

Your own TikTok for the videos and photos you already have.
Self-hosted. No cloud account. Your library stays with you.

PyPI License

Spooly Library grid on desktop with the Flow swipe feed on a phone

Spooly turns a folder of media into a private feed. Run one command on your computer or NAS, open it on your phone, and swipe through your own videos and photos full screen — or browse them as a Pinterest-style grid and a year/month timeline. Browsing and playback stay on your server; optional AI features send selected images to the provider you configure.

Local browsing and playback work without internet access. URL downloads and remote AI features require a connection.

Watch the 24-second demo · Screenshots and media kit

Quick start

Requires Python 3.10+.

pip install spooly
spooly ~/Videos

Open http://<your-computer>:8000 on any phone, tablet, or browser on the same network. The first start prints an access password in the terminal.

Docker

mkdir spooly-data
docker run -d --name spooly --restart unless-stopped --init \
  --user "$(id -u):$(id -g)" -p 8000:8000 \
  -v ~/Videos:/media -v "$PWD/spooly-data:/data" \
  ghcr.io/oddloafhq/spooly:latest
docker logs spooly    # access password

The image includes ffmpeg, yt-dlp and gallery-dl for amd64 and arm64. See Docker for Compose, NAS permissions and updates.

Android

An Android phone can run Spooly in Termux. See Android with Termux.

What it does

  • Flow — a full-screen vertical feed that mixes videos and images. Swipe to the next item, double-tap to favorite, hold a video to play it at 2×, and drag sideways to seek.
  • Library — a year/month timeline for multi-year archives and an Explore grid with random images, latest videos, and large files.
  • Home — pick up where you left off, see what happened On This Day, and open your collections.
  • Favorites and collections — save anything from any view; long videos resume where you stopped, across devices.
  • Downloads — paste a link and Spooly fetches it into your library with yt-dlp or gallery-dl.
  • Radio — an ambient audio player for the music in the same folders.
  • Many folders, one library — merge several media directories; deletions go to a trash you can undo.

Flow: a full-screen waterfall video with like, save and info actions Home: a collage of recent photos with entry points to Flow and Radio Library: a two-column grid of landscape and city photos

Home on desktop with the On This Day row

Screenshots use CC0 and public-domain photos and clips from Wikimedia Commons.

Flow controls

Gesture Video Image
Swipe up / down, wheel, ↑ ↓ Next / previous Next / previous
Tap Play / pause Show / hide the interface
Double-tap Favorite Favorite
Press and hold 2× while held Show / hide the interface
Drag sideways Seek Next image in a gallery
← → Back / forward 5 s Gallery image, otherwise next / previous

Files the browser cannot play (for example some HEVC or AV1 videos) are skipped with a short note instead of stopping the feed.

Configuration

Settings come from command-line options, then environment variables (MEDIA_ROOT, SPOOLY_HOST, SPOOLY_PORT), then config.yaml in the SPOOLY_INSTANCE data directory when set, then ~/.config/spooly/config.yaml:

media_sources:
  - id: default
    name: Main Library
    path: ~/Videos/Spooly
  - id: photos
    name: Photos
    path: ~/Pictures
download_source: default   # where URL downloads are saved
name: Studio Mac           # shown in the browser tab
port: 8000

A single media_root: ~/Videos still works. Sources are merged into one library; media URIs look like @source_id/path, and old bare-path links and favorites keep working through the default source.

spooly /path/to/media                        # start with one folder
spooly --port 9000                           # custom port
spooly --media-source photos=~/Pictures/AI   # add a source (repeatable)
spooly --help

Access password

Sign-in is on by default. Every page, API and media file requires it.

spooly auth status          # status and storage path
spooly auth set-password    # new password; signs out existing sessions
SPOOLY_AUTH_PASSWORD='a-long-private-password' spooly auth set-password

The password is stored as a scrypt hash in ~/.spooly/auth.json. Keep Spooly on a trusted network.

HTTPS

Spooly serves plain HTTP. For HTTPS, put it behind a reverse proxy that keeps the original Host header and media Range requests, and mark the session cookie secure:

FLASK_AUTH_COOKIE_SECURE=true spooly ~/Videos --host 127.0.0.1 --port 8000

URL downloads

The /download page queues background downloads. Install the tools it uses:

brew install yt-dlp gallery-dl ffmpeg        # macOS
sudo apt install yt-dlp gallery-dl ffmpeg    # Ubuntu / Debian
  • yt-dlp (required) handles video sites; Spooly asks it for H.264/AAC so downloads play in every browser. gallery-dl handles image posts and albums. ffmpeg merges formats and makes video thumbnails.
  • For sign-in-only content, put cookie files saved from your browser in ~/.spooly/cookies with the domain in the name (x.com.txt, youtube.com.cookies), or upload them on the download page.

Stopping the server (Ctrl + C or SIGTERM) cancels unfinished downloads and keeps finished files; interrupted jobs are not resumed.

More commands

Thumbnails

spooly thumbs /path/to/media              # generate video thumbnails
spooly thumbs /path --overwrite           # regenerate

Duplicates

spooly dedupe /path/to/media              # dry run
spooly dedupe /path --type video          # videos only
spooly dedupe /path --keep newest --execute

Options: --type video|image|all, --algorithm sha256|md5, --keep oldest|newest|shortest_path, --dry-run (default), --execute, --auto-confirm.

AI titles, media search and related media (optional)

Image titles and tags use a vision model; related media and description search use an embedding model. Both are off unless configured:

vision:
  enabled: true
  base_url: https://openrouter.ai/api/v1
  model_name: google/gemini-2.5-flash
  tags_limit: 5

experiments:
  similarity:
    enabled: true

embedding:
  base_url: https://openrouter.ai/api/v1
  model_name: google/gemini-embedding-2
  # api_key: ...             # optional; otherwise read from the environment

Vision keys come from SPOOLY_VISION_API_KEY, then the shared key variables. The embedding key comes from embedding.api_key, then SPOOLY_EMBEDDING_API_KEY, SPOOLY_AI_API_KEY, OPENAI_API_KEY or OPENROUTER_API_KEY. The connection is read only from config.yaml; restart Spooly after changing it. Settings shows it read-only and never displays the key. Spooly always requests 768-dimension vectors and sends photos as 512px JPEGs without location or other metadata.

Open Settings → Similar Images and choose Prepare. The job runs in the background, can be paused, and handles photos first, then videos when ffmpeg and ffprobe are installed. Videos use three frames at 20%, 50% and 80% of their duration; videos shorter than one second use one frame, and frames that cannot be read are skipped. Each frame is sent separately, so preparing a video usually makes three model requests. Sampled images are temporary and do not change the library cover. Three consecutive service failures stop the job; an unreadable file is skipped and retried next time. Progress lives in the serving process, so after a restart start preparation again; prepared files are skipped. Vectors are stored in ~/.spooly/spooly.sqlite3 (or /data in Docker), and viewing saved results never calls a model. Pausing finishes the current request; a partially prepared video is retried from the beginning. Previously prepared single-thumbnail videos are updated on the next preparation, while photo vectors are reused.

After preparation, Spooly selects up to 24 varied photo groups locally from the 5,000 most recent prepared photos, compared in windows of 64. Library → Explore shows a few groups, Flow may reuse an existing gallery slot for one, and photo details list related photos from all prepared photos. Video details show related videos, ranked by average best frame matches in both directions so one shared shot does not determine the whole ranking. Older thumbnail vectors remain usable until preparation replaces them. Search by description in Library → Explore sends one text request per search (Chinese works too) and ranks up to 48 photos and videos by similarity, which is not a confidence score; a video matches by its closest frame. Queries are not saved.

The same preparation is available from the terminal:

spooly vectorize ~/Videos --dry-run            # counts only; nothing is sent
spooly vectorize ~/Videos --limit 200          # try a small batch first
spooly vectorize ~/Videos --yes

Changing model_name starts a new vector space: files are prepared again for the new model, and search and related photos include them as they are reached. Files changed on disk are prepared again on the next run.

At the 2026-10-13 OpenRouter price, about 5,000 Gemini image vectors cost $0.58, excluding retries and fees. To run a model on your own NAS instead, see the isolated Docker deployment guide; Spooly does not install or start the model service.

experiments.similarity.enabled: false turns the feature off without deleting data; restart after changing it. When omitted, a legacy embedding.enabled: true enables it. An invalid embedding URL stops startup with a configuration error.

Embedding in another WSGI server

The CLI owns the download manager. When hosting create_app() yourself, call app.extensions['download_manager'].start() inside the serving worker and .close() in its shutdown hook — not in Flask's per-request teardown.

Documentation

Contributing

Bug reports, ideas and pull requests are welcome on GitHub. To refresh the screenshots, serve a demo folder without sign-in and run the capture script (it uses your installed Chrome, or the executable set by CHROME_PATH):

npm install
SPOOLY_INSTANCE=/tmp/spooly-demo FLASK_AUTH_ENABLED=false spooly demo-media --host 127.0.0.1 --port 8793
node scripts/screenshots.mjs

Made by Oddloaf. Contact hello@oddloaf.dev, or open an issue.

License

MIT

Metadata

Release files for spooly 0.8.48

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spooly 0.8.48
File Size Uploaded
spooly-0.8.48.tar.gz 2.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for spooly 0.8.48
File Interpreter ABI Platform
spooly-0.8.48-py3-none-any.whl Python 3 none any Details

Total release size: 5.5 MB

Release files / spooly-0.8.48.tar.gz

Download URL spooly-0.8.48.tar.gz
Size 2.7 MB
Tags Source
SHA-256 checksum
How to use checksums
198ad902f1043ae736f0e228011b2c70f2a6b71d02e52b62deccd64b920b4e86
BLAKE2b-256 checksum
How to use checksums
c56ad810a335b03ea576e6d62b6e9a4bd6674f70d59a7ae0d9c4e32f37e32567
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / spooly-0.8.48-py3-none-any.whl

Download URL spooly-0.8.48-py3-none-any.whl
Size 2.8 MB
Tags Python 3
SHA-256 checksum
How to use checksums
f8e9e17b88ea1c195b13fcdbe5a878a6dd3770ecc44ad98029df4fd9d25af03c
BLAKE2b-256 checksum
How to use checksums
96fe9d5bba20e8817c72b5d8b190789491265bef366b30537c51efca7c8da9e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.8.48 This release

2 release files

0.8.47

2 release files

0.8.46

2 release files

0.8.45

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page