Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

706 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pito

CI License: AGPL v3 Sponsor

▶ Pito — Inception Wednesday: a guided tour

▶ A guided tour of Pito — watch the tour, on @gmrdad82.

Why does this thing exist?

Twenty-odd years building Ruby on Rails apps for other people's companies — a lot of companies, a lot of apps — and then I started my own YouTube adventure with a fistful of channels. That's when I discovered the tooling situation is grim. YouTube Studio, in its infinite wisdom, lets you manage exactly one channel at a time — log out, log in, log out, log in, repeat until your will to live quietly files for unemployment.

So I went shopping. Social Blade, vidIQ, TubeBuddy — I paid for them, gave each a fair shot, and not one did the specific things I actually needed. They'd love €50+ a month, please, forever, amen, for the privilege of almost fitting.

So I did the math. The math said "build it yourself, you cheapskate." Two decades of Rails muscle memory agreed. So I did.

The name

One evening I heard my son singing a little song from school — the old Spanish counting rhyme kids use to pick who's "it":

Pito, pito, gorgorito, ¿a dónde vas tú tan bonito?

I fell for it on the spot: the sound, the daftness, the way it lodges in your head and won't leave. So I baptized the thing Pito and never looked back.

What I wanted it to be

Awesome. Glamorous. Charming. Stupidly easy. And mine — all mine, built exactly the way I work, answering to no product manager and no quarterly roadmap.

Then it occurred to me that maybe someone else runs their channels the way I do and wants the same thing. So here it is: you can have it too. Charmingly awesome, and now open. One dashboard, every channel, zero monthly ransom, and nobody handing a random SaaS company the keys to my business. My laptop, my data, my rules. It scratched my own itch first — if it scratches yours, wonderful. That's what the AGPL is for.

And hey — if this saves you a headache (or fifty euros), the nicest possible "thank you" is a click on the channel that dragged this tool into existence in the first place:

@gmrdad82


Self-hosted YouTube tool for creators who run multiple channels — track channels, videos, and games in one place, schedule across channels without conflicts, and get game/channel recommendations. Your laptop, your data.

one person's tool, open-sourced as-is. no SLA, no roadmap, no support obligation. issues triaged when there's time; PRs welcome but not guaranteed to merge. no hosted service from this repo.

What Pito does that no one else does — not even Studio

Everything in this section is native to Pito and simply absent from YouTube Studio, TubeBuddy, and vidIQ. Not "better" — absent. Studio knows your videos inside out; it has just never once asked which game you were playing. Pito is built around that question — and it's best seen: it moves.

An assistant that lives in your library

@ai comparing channels — a real table, a braille bar chart, and the model's cost receipt

@ai — ask in plain words ("compare my channels, chart the split") and the assistant reads your library through Pito's own read-only tools, then answers in Pito's native voice: real tables, braille charts, score bars — streaming in block by block. Bring any model with your own key (that answer came from a free one); every reply is signed with the model's name and its reported cost, and the web stays out of it unless you pass --web.

Your library speaks MCP

The Pito OAuth consent screen, approving a new MCP client with a TOTP code

The same library speaks MCP — Claude on your phone, ChatGPT, or any MCP client can read your channels, vids, games, and analytics from wherever you are. Read-only, gated by a consent screen and your TOTP (setup).

Which of your channels owns a game

Pito — cross-channel game coverage: distribution + recommendation

show game answers the one question a multi-channel gamer actually has: side by side, how this game's coverage is distributed across your channels (weighted by vids + views + lifetime watch-time, streaming in as braille bars) and which channel it best fits next (top-5 recommendation, avatar and score, row-aligned with the distribution). Nobody else has a cross-channel game view — Studio doesn't even let a second channel into the same tab.

Game price, tracked

Game price

Coins on the card. Know what covering a slate costs before you promise it to an audience.

Smart linkage: games ↔ videos ↔ channels

Pito — game/video/channel linkage demo

Explicitly link a video to a game and Pito builds the graph: which games you cover, which channels they fit, and — via a local embedding sidecar — the similar games and best-fit channels you hadn't thought of. It never guesses from titles; the links are yours.

Game–video linkage

Your library finally knows which game every video covers — and everything below runs on that graph.

Your library understands how games feel

Typing 'search games with a good story' into the chatbox and getting a ranked list of games back, no keywords or filters involved

No keywords, no syntax to memorize — just say what you're after. Type search games with a good story or something brutal but worth every second and Pito reads the meaning, not the words, then ranks your library by how close each game lands. Every game carries a small, curated trait vocabulary — difficulty from easy to brutal, story from bad to emotional, pace from relaxing to chaotic, plus a set of qualitative tags — and that vocabulary rides straight into the search, so "brutal" and "worth every second" actually mean something. Typo and all: search games requireing patience finds the same games the spelled-right version would, because meaning doesn't care about spelling.

search games about brutal but worth every second — the punishing-but-loved shelf, ranked by meaning with the closest match at 100

Ask for a feeling and you get the shelf that feels that way — search games about brutal but worth every second surfaces the punishing-but-loved corner of your library, closest match first, with a pager for the rest. It's honest when it comes up empty, too: if nothing in your library is genuinely close to what you typed, Pito says so instead of padding the page with weak matches just to fill it. And the whole thing runs on an embedding model living inside your own Pito stack: no API key, no cloud call, nothing leaves your machine.

And the rest of the loot

Pito — one chatbox demo

One chatbox — that's the entire app. No menus, no forty-tab settings labyrinth. You type, Pito answers — a terminal-style chat with keyboard-first UI/UX that gets out of your way and stays out of it.

Pito — plain-language query demo

Ask in plain language. Type the way you think. Pito reads a little natural language, so list vids, show game, or a quick question gets you your numbers without hunting through dashboards.

Pito — scheduling helper demo

A calendar that finds the gapschedule <id> slate lays out what's already booked across every channel, so releases spread out instead of quietly stacking on a Tuesday and you never double-book a day.

Schedule slate

Per-game footage on the time-to-beat bar

Footage hours per game — your recorded backlog, sitting right on the time-to-beat bar. "Do I have enough material for this one?" — answered at a glance.

Per-platform release dates

Release dates per platform — PlayStation, Switch, Xbox, and Steam — grouped by date with their logos, and a countdown that names which platform lands ("…on PlayStation + Steam in 3 days"). Unreleased games re-sync nightly until the date is real.

Game-level analytics

Analytics at the game level — avg % viewed, retention, avg view duration, aggregated across a game's linked vids (channels get the same treatment). Studio does these strictly one video at a time.

Day-of-week heatmap

Day-of-week heatmap — Mon→Sun bars computed from your daily views, busiest day green. The YouTube API doesn't even offer this dimension; Pito does the math itself.

Similar games

Similar games — a local embedding sidecar surfaces what sits near your library, right on the game card, with the channels each one fits.

Shareable message

Any message is a linkshare mints a public URL to that exact reply, chart included. Go ahead, try sending someone a Studio screen.

Conversation snapshot

Conversations are snapshots — an old conversation still shows your numbers as they were. A time machine no dashboard gives you.

Pito on mobile

Happily mobile — the chatbox works on your phone. Studio on a phone is… an app that isn't this.

Shinies

Shinies — lifetime achievement badges across channels, vids, and games. Gaming-flavored bragging rights that unlock as you grow.

Pito — live theme switching demo

Themes for every desire — 19 built-in palettes — familiar editor themes, dark and light — switched live with /themes and remembered per your taste. Don't love one? The tokens are right there to tune.

The one chatbox

And the whole thing is a conversation — one chatbox, one monospace font, full keyboard navigation. The features above aren't buried in menus; you type them.

Free, yours, and built to grow — $0. It runs on a machine you already own, on free, open tooling — the walkthroughs below get you from nothing to running for exactly €0. Built by a 20-year Rails engineer for his own channels, not a growth team's OKRs: no dark patterns, no telemetry, no paywall creeping in later. What's coming next — richer language, deeper analytics, more MCP — lands free too.

Everything you can type

Everything happens in one chatbox: type a verb, or reply to any message with #<handle> <action> (the message shows you its handle). <verb> --help prints a man page for any verb. Keywords are case-insensitive and a little natural-language. The full reference lives in CHANGELOG.md; the short version:

  • Channels — connect via Google OAuth (/connect), see them all at once with basic stats (subs · vids · views, with likes to add the summed like count), visit @handle, or /disconnect. Read-only except the four YouTube writes below.
  • Videoslist vids with addable columns (channel, visibility, game, duration, views, likes, category — with/without, sortable); show vid <id> for detail. The only YouTube writes: publish, unlist, schedule, delete a video. sync vids pulls the latest (private uploads included).
  • Gamesimport game [title] from IGDB; show game <id> for genres, themes, developer/publisher, release, footage, and price; list games grows columns the same way (platform, genre, developer, publisher, channels, footage, price, views, likes). Set fields with platform, price set/unset, and a manual footage total.
  • Recommendations & search — every game card surfaces similar games and the channels it best fits; search games about <a feeling> (or bare — no keyword needed) finds games by feel instead of by name, typo and all. Same local embedding sidecar — no API key, no cloud call.
  • Linking — explicit link / unlink between a video and a game, both directions; Pito never guesses from titles.
  • Planningschedule <id> slate shows what's already on the calendar so you can spread releases; per-game price and footage help you budget and plan.
  • Notifications — Slack + Discord integration (rich, colored, emoji'd) for reauth reminders, sync summaries, and upcoming-release countdowns, plus a live in-app unread badge.
  • Shinies — lifetime achievements across subs, subs gained, views, watch time, likes, and comms. They unlock as your channels, videos, and games grow, each arriving as a 🏆 notification and collected right on the video/game.
  • The shell — terminal-style chat UI, one font, no hover; full keyboard navigation (ditch the mouse); themes; per-conversation scope/period that persists; self-hosted in Docker; free.

Themes

Pito ships 19 built-in themes — familiar editor palettes — switched live with /themes (opens a picker; your choice persists):

  • Darkayu-dark · ayu-mirage · catppuccin-mocha · dracula · github-dark · gruvbox-dark · nord · one-dark · solarized-dark · synthwave · tokyo-night · tomorrow-night
  • Lightayu-light · catppuccin-latte · github-light · gruvbox-light · one-light · solarized-light · tomorrow

Every theme is a set of CSS custom properties in app/assets/tailwind/themes.css, so all 19 are fully supported. That said — I personally run synthwave, so most of the visual tuning (shimmer colors, the #5170ff pito-blue accent, contrast choices) is dialed in around that theme. Everything still works on the others, but if a palette doesn't feel quite right to you, the tokens are right there: tune them to your preference.

🎨 Theme gallery — click to expand all 19

Dark

ayu-dark
ayu-mirage
catppuccin-mocha
dracula
github-dark
gruvbox-dark
nord
one-dark
solarized-dark
synthwave
tokyo-night
tomorrow-night

Light

ayu-light
catppuccin-latte
github-light
gruvbox-light
one-light
solarized-light
tomorrow

Stack

Rails 8 · Hotwire · Postgres · llama.cpp · IGDB · YouTube API · Tailwind CSS.

Requirements

The easy path needs only Docker (+ Docker Compose). The image is prebuilt and pulled from GitHub's registry, so there's nothing to compile and no Ruby to install.

Hacking on it natively instead? You'll want:

  • Ruby 3.4.9 (pinned in .ruby-version; mise / rbenv / asdf will read it)
  • PostgreSQL 17 with the pgvector extension (for the recommendation embeddings)
  • imagemagick · libvips (game cover / thumbnail image processing)

No Redis — background jobs, cache, and websockets all ride on Postgres (Solid Queue / Cache / Cable). And no ffmpeg: Pito itself never shells out to it. The only place it comes up is the optional footage snippet helper — a copyable one-liner you run in your own video folder (it uses ffprobe) to total your raw hours. Install ffmpeg only if you want that convenience, wherever your footage lives. Heads-up: setup is hands-on. This is a one-person tool wearing its "as-is" sticker proudly.

Install & run

Two ways in. Docker runs production mode; native runs development mode.

Docker — the easy path (only Docker needed)

No clone, no Ruby. One command fetches a small ./pito-cli install (a compose file + the pito-cli CLI), generates your own secrets, pulls the prebuilt image, and walks you through enrolling a login:

curl -fsSL https://raw.githubusercontent.com/gmrdad82/pito/main/script/install.sh | sh

curl | sh installing Pito — version picker, then it fetches + sets up

It first asks which version to install — pick a stable release (the newest is the default + recommended) or edge (latest image + bleeding-edge CLI from main). Then it asks which port to answer on (default 3028), checks that nothing is already listening there, mints a fresh master key + credentials (no editor required), enrolls TOTP (scan the printed otpauth:// into any authenticator) and offers to install a systemd unit so the stack comes back after a reboot. When it finishes, open http://localhost:PORT and /login.

Pito chat runs on localhost. It installs on your machine, listens on 127.0.0.1, and that is the whole story the installer tells. There is no public-host question and no HTTPS step: putting a local port on the internet is a separate job, with its own tools and its own security decisions, and the installer deliberately does not make them for you. If you want remote access, terminate TLS and reverse-proxy to 127.0.0.1:PORT with whatever you already run.

Pick the port, or install in place. Flags pass through with sh -s --:

# answer on 4000, and install into the CURRENT folder (no ./pito-cli subdir):
curl -fsSL https://raw.githubusercontent.com/gmrdad82/pito/main/script/install.sh \
  | sh -s -- --port 4000 --dir .

The script's --help lists them all: --port PORT (default: prompt, then 3028), --dir DIR (default ./pito-cli), --version vX.Y.Z (pin a release) / --edge (skip the version prompt), --skip-pull, plus --service-only to (re)run just the systemd step. You provide nothing else — the master key + credentials are generated for you; API keys go in later via /config.

Changing the port later is pito-cli update --port PORT.

Versions: stable vs edge. stable pins a release — image tag and CLI/scripts come from the same vX.Y.Z git tag, so the whole install is reproducible. edge runs the :latest image with the CLI tracking main. pito-cli --version shows which you're on (pito 0.7.3 (stable)); pito-cli update lets you move between them.

Re-running is safe. Running the installer again keeps your existing master key + credentials, never touches the Postgres volume (channels, videos, games, /config keys + webhooks), and does not re-enroll TOTP — your authenticator keeps working. To just pull a newer image, use pito-cli update (image swap + restart only, nothing else touched).

The installer symlinks the CLI onto your PATH, so pito-cli runs from anywhere (pito is kept as a deprecation shim for one release, if you're used to the old name; if the symlink step is skipped — no sudo — use ./pito-cli from the install dir instead):

pito-cli logs -f     # tail the app
pito-cli console     # a Rails console in the container
pito-cli update      # pull the latest image + restart
pito-cli --help      # the rest

The shim is linked only when nothing else owns the pito name — an existing pito is never replaced or shadowed, the installer says so, and the operator CLI stays pito-cli either way.

Same on Linux, macOS, and Windows (WSL2) — and on both amd64 and arm64 (the image is multi-arch, so a Raspberry Pi 5 or Apple Silicon box is fine too).

The MCP service. Alongside the two web deploy slots, the compose file ships a second Puma, pito-mcp — the same image on an isolated port, so a slow AI tool-loop can never starve the app. pito-cli update re-fetches docker-compose.yml, then brings it up with docker compose up -d pito-mcp. If you put a reverse proxy in front of Pito, route the paths ^/(mcp|oauth|\.well-known) to it (port 3029) and everything else to 127.0.0.1:PORT — the internal load balancer (lb) that fronts the two deploy slots (see Zero-downtime deploys below).

Connect an AI chat (MCP)

Pito speaks the Model Context Protocol at /mcp, so an AI assistant can read your Pito — your games, videos, channels, analytics, breakdowns, at-a-glance snapshots, similar games, channel coverage, shinies, and your past conversations — as first-class tools. It is strictly read-only: nothing it can call changes anything, and MCP calls never appear in your scrollback or resume sidebar.

To attach a client (e.g. claude.ai, on your phone or desktop):

  1. In the client's connector settings, add a custom MCP connector with the URL https://<your-pito-host>/mcp — whatever public name you reach your instance by (see Reaching Pito from somewhere else). A remote MCP client cannot talk to a bare localhost install.
  2. The client discovers Pito's OAuth automatically and opens a consent page in your browser. It shows the app name and the read-only tool list, and asks for your current 6-digit TOTP code — the same code you use for /authenticate.
  3. Enter the code once to approve. That's it — the client refreshes its access silently from then on; you never enter a code again for that client (revoke by deleting its OauthClient / OauthToken rows in pito-cli console).

Then ask the assistant things like "what games does @gmrdad82 play?" or "what did Pito say about retention yesterday?" and it will call the matching tool.

Native — for hacking on it (development mode)

git clone https://github.com/gmrdad82/pito && cd pito

Make your own secrets (the bundled config/credentials.yml.enc is the author's — you can't decrypt it):

rm -f config/credentials.yml.enc config/master.key
EDITOR=nano bin/rails credentials:edit   # creates a fresh config/master.key
bin/rails db:encryption:init             # paste the printed keys into the credentials file

Then install your OS deps (below), run bin/setup (brings up Postgres + prepares the DB) and bin/devhttp://localhost:3027. Enroll your login with bin/rails pito:totp — or, in development, just type /login 123456 (a dev-only dummy code; see Operating Pito).

OS System packages
Arch sudo pacman -S postgresql imagemagick libvips + pgvector (extra/AUR)
Ubuntu/Debian/WSL sudo apt install postgresql-17 postgresql-17-pgvector imagemagick libvips42
Fedora sudo dnf install postgresql-server pgvector ImageMagick vips
macOS brew install postgresql@17 pgvector imagemagick vips

(Package names drift between distro versions — adjust as needed. Add ffmpeg only if you want the optional footage snippet helper.)

Not a cloud thing

Pito is built to run on your own machine — your laptop, a home server, a NUC under the TV. There's no hosted service from this repo and no cloud-deploy story baked in (no Kamal, no Helm, no "click to deploy"). You're welcome to put it behind a domain however you please (see Reaching Pito from somewhere else) — it's AGPL, go wild — but the supported, tested path is localhost self-host, and that is the only one the installer knows how to set up.

Accounts & API keys

Pito needs two sets of credentials. Grab them, then paste them into the chatbox with /config (stored encrypted). Only Google is strictly required to do anything useful; IGDB unlocks the game features. Similar games, channel recommendations, and the natural-language chat mapper run on local AI sidecars baked into the compose stack — no signup, no key, nothing to /config (see Local AI under Operating Pito).

1. Google / YouTube (required — it's the whole point)

  1. Google Cloud Console → create a project.
  2. APIs & Services → Library → enable YouTube Data API v3.
  3. OAuth consent screenExternal → add your own Google account as a test user.
  4. Credentials → Create credentials → OAuth client ID → Web application. Add the authorized redirect URI http://localhost:3028/auth/youtube/callback — swap 3028 for the port you installed on, or use your public URL if you put a reverse proxy in front. Copy the Client ID + Client secret.
  5. Credentials → Create credentials → API key. Copy it.
  6. In Pito: /config google client_id=… client_secret=… api_key=…, then /connect to authorize each channel.

2. IGDB (game data — runs on Twitch)

  1. Twitch Developer ConsoleRegister Your Application (any name; OAuth redirect http://localhost is fine).
  2. Copy the Client ID and generate a Client Secret.
  3. In Pito: /config igdb client_id=… client_secret=….

Optional — Slack / Discord notifications: create an incoming webhook in each, then /config webhook slack=… discord=….

First run

  1. /login <6-digit code> (from the authenticator you enrolled above).
  2. /config your keys, then /connect your first channel.
  3. list channelssync vidslist games. You're off.

Operating Pito

The Docker stack is driven by the pito-cli CLI (on your PATH after install — or ./pito-cli from the install dir):

pito-cli in action — help, version, logs, rake, backup

Command What it does
pito-cli up / down start / stop the stack
pito-cli logs [-f] tail container logs (Docker's own — capped + rotated)
pito-cli console a Rails console inside the running container
pito-cli rake [task] list pito:* tasks, or run one in the container
pito-cli clean clear tmp/ scratch (keeps storage/pids) + dev logs
pito-cli totp (re)enroll your login
pito-cli version show the running version + channel (stable/edge)
pito-cli update update — pick a release (stable) or edge, zero-downtime
pito-cli deploy-flip run the zero-downtime blue/green flip by hand
pito-cli backup dump DB + Active Storage to ./backups/<ts>/ (host)
pito-cli build build the image locally from a source checkout
pito-cli self-update refresh just the CLI (no image pull / restart)
pito-cli autoupdate pull new releases automatically (15-min systemd timer)

pito-cli update is the one you'll reach for most — it's interactive: it lists the available releases (or edge) and switches the whole stack (image and CLI) to your pick in one step:

pito-cli update — pick a stable release, the whole stack switches

Running a locally-built image

To run your own build instead of a published release — on the same Docker host:

# 1) in a source checkout — build + tag it locally (default tag: `local`)
PITO_TAG=local pito-cli build

# 2) in your install dir — point the stack at that tag and (re)start
echo 'PITO_TAG=local' >> .env      # or edit the existing PITO_TAG line
pito-cli up -d                         # runs the local image; no pull

pito-cli build tags the image ghcr.io/gmrdad82/pito:local; because the install-dir compose resolves image: ghcr.io/gmrdad82/pito:${PITO_TAG}, setting PITO_TAG=local makes pito-cli up run your build (Compose uses a present image and never pulls). The local tag keeps it from colliding with a real release. Use pito-cli up, not pito-cli updateupdate pulls from GHCR and would replace the local image. To return to a release, set PITO_TAG back (e.g. 0.8.5) and run pito-cli update.

In-app, /jobs is your window into the background queue: /jobs status (workers, state counts, recent failures), /jobs requeue <id|all>, /jobs run <key> (run a recurring task now), and /jobs pause / /jobs resume.

Dev conveniences (development only — inert in production):

  • Recurring jobs are off under bin/dev, so nothing hits YouTube/Discord while you hack. Want the scheduler running? PITO_DEV_JOBS=1 bin/dev.
  • /login 123456 just works — no authenticator needed. Change the code with PITO_DEV_TOTP_CODE=…, or disable it (PITO_DEV_TOTP_CODE=off) to exercise the real TOTP flow. The dummy code is impossible outside development.

Backups

pito-cli backup writes a timestamped folder on the host (./backups/<ts>/, git-ignored) with two artifacts:

  • database.sql.gzpg_dump run inside the Postgres container (version-matched, includes the pgvector embeddings).
  • active_storage.tar.gz — your avatars/thumbnails/covers and their variants from the assets volume.

It runs against the live stack, so bring it up first (pito-cli up -d). The full surface:

Command What it does
pito-cli backup back up DB + assets; prunes to the newest 7 afterward
pito-cli backup --list list existing backups with their sizes
pito-cli restore <dir> restore a backup over the live stack (prompts — it's destructive)
pito-cli backup-schedule install a daily systemd timer that runs pito-cli backup

Tune retention + location with PITO_BACKUP_KEEP (default 7) and PITO_BACKUP_DIR (default ./backups). pito-cli backup-schedule is also offered during install; once set, backups run daily at 03:00 and self-prune — hands-off rolling backups on the host.

Restore is deliberate (pito-cli restore confirms first, then reloads the DB + assets and restarts the service). The equivalent manual one-liners, if you prefer:

gunzip -c backups/<ts>/database.sql.gz | docker compose exec -T postgres psql -U pito -d pito_production
# <slot> is whichever of web-blue / web-green is currently active — `pito-cli version` says which:
gunzip -c backups/<ts>/active_storage.tar.gz | docker compose exec -T <slot> tar xf - -C /var/lib/pito-assets

Zero-downtime deploys

pito-cli update doesn't stop-and-start the app — it flips between two deploy slots, web-blue and web-green, one active at a time:

  1. Pull the new image into the idle slot and start it. Its entrypoint runs db:prepare for any pending migration before it can pass its health check — the active slot keeps serving the old code the whole time (this is exactly why every Pito migration is additive-only: old code has to keep working against the new schema for however long that takes).
  2. Wait for the idle slot to answer /up (bounded retries). If it never does, the idle slot is stopped and the update aborts loudly — the active slot is never touched, so a bad deploy is a no-op for anyone using the app.
  3. Once healthy, stop the old slot (SIGTERM; Puma drains in-flight requests). ActionCable clients reconnect through the load balancer to the new slot on their own — that's exactly what they're built to do.
  4. Flip the bookkeeping (PITO_ACTIVE_SLOT in .env) so the next deploy knows which slot is idle.

An internal Caddy instance (lb, always running, on 127.0.0.1:PORT — the port you picked at install time) sits in front of both slots with an active health check against /up; whichever slot answers gets 100% of traffic. Nothing about lb changes during a deploy — a flip never rewrites its config, it just starts/stops containers and lets the health check catch up.

Both slots briefly run at once during the overlap window — safe by construction: SolidQueue is DB-backed with SKIP LOCKED job claims, and its recurring schedule (config/recurring.yml) is deduped by a database unique index (task_key, run_at), so two independent schedulers never enqueue the same scheduled run twice.

Recovery. If a flip ever leaves things in a bad spot, sudo systemctl restart pito remains the full-stack bounce — it's downtime-ful (the old "just restart everything" behavior) but always works, regardless of which slot is stuck. pito-cli deploy-flip <tag> re-runs the flip by hand if you want to retry without a full restart.

Upgrading an existing install. Installs from before this feature have no PITO_ACTIVE_SLOT — their next pito-cli update migrates the compose file and .env to the two-slot shape automatically, once (and seeds PITO_PORT from the port that install was already publishing, so nothing moves under you). That migration itself needs one last ordinary restart (there's no shape to flip between yet); the version you actually asked for then lands via the new zero-downtime flip, in the same run. Every update after that is zero-downtime.

Auto-update (your server pulls new releases)

Your server keeps itself current — CI never logs in anywhere. pito-cli autoupdate checks GitHub for a newer release, waits until the release's multi-arch image is actually live on GHCR, and applies it with the same pito-cli update you'd type by hand. One command turns it on:

pito-cli autoupdate --install     # systemd timer, every 15 min + logrotate rule

Why pull instead of push:

  • Zero deploy credentials in GitHub. No SSH keys, no host secrets — nothing for a public repo to leak, nothing for a compromised action to steal.
  • No inbound access. CI runners never connect to your server.
  • Fork-friendly. Any self-hosted instance updates itself without touching the upstream repo's CI at all.
  • Race-proof. pito-cli update holds a single-updater lock (flock on .pito-update.lock), so the timer and a manual pito-cli update — for when you don't feel like waiting 15 minutes — can never run on top of each other.

Everything it does lands in log/autoupdate.log (rotated weekly). Optional: set SLACK_WEBHOOK=<url> in the install dir's .env and every applied (or failed) update pings you via a plain curl. pito-cli autoupdate --check dry-runs the decision; pito-cli autoupdate --uninstall removes the timer.

From then on: git tag v1.2.3 && git push origin v1.2.3 → green CI gate → multi-arch image on GHCR → within 15 minutes your server is running it, hands-off. (Edge-channel installs are deliberately skipped — latest stays a by-hand choice.)

Customizing your shinies (optional)

The achievement ladders are data, not code: config/pito/shinies.yml sets every scope × metric ceiling for the 1-2-5 stone ladders, plus the channel-subs award metals. The shipped defaults target the reference channel (a monetized, 100K-subs ambition):

ceilings:
  channel:
    subs: 50_000
    views: 50_000_000
awards:
  silver: 100_000
  gold: 1_000_000

To run your own ambitions, drop a full copy next to docker-compose.yml, edit the numbers, and mount it over the baked one (uncomment the ready-made line in the compose file):

- ./shinies.yml:/rails/config/pito/shinies.yml:ro

A broken file refuses to boot and names exactly what's wrong. Reshaping a ladder never revokes an unlocked shiny, but stone materials are positional — a shorter ladder may re-color badges you've already earned.

Monitoring (optional)

Pito can report performance, errors, and logs to AppSignal in production. Grab a Push API key from your AppSignal app (App settings → Push & deploy → Push API key), set APPSIGNAL_PUSH_API_KEY=<key> in the install dir's .env, then pito-cli up -d to pick it up. The key never touches the image, the repo, or Rails credentials — just that one .env line. No key, no AppSignal — the app boots exactly as before.

Local AI (embeddings and language mapping)

Two CPU-only llama.cpp sidecars ship in the compose stack — no signup, no API key, nothing leaves your machine:

  • embedder — embeddinggemma-300m (Q8), serving the OpenAI-compatible /v1/embeddings API (768-dim vectors, stored in Postgres via pgvector). Powers search-like matching, similar games, and channel-fit recommendations.
  • nlmapper — Qwen3-0.6B (Q8), grammar-constrained to Pito's own command set. Maps free-text chat ("list me the vids") onto a real command when nothing else in the grammar matches it — a cold path that never touches normal typing.

Both pull their GGUF weights from Hugging Face on first boot into their own model volumes (embedder_models / nlmapper_models), then never again — the embedder's is ~350MB, so give the first pito-cli up a few extra minutes while it downloads (the healthchecks' generous start_period covers it). Each sidecar is capped at 1g RAM, worth knowing if you're running Pito on a small box.

The app reaches them over PITO_EMBEDDER_URL / PITO_NLMAPPER_URL — the compose stack sets both for you; native dev's host-Puma uses the published http://127.0.0.1:8091 / http://127.0.0.1:8092. Either one blank or absent and the matching feature just quietly degrades (search-like/similar/link-suggestions, or the NL mapper) — nothing crashes, nothing bad persists.

Upgrading an existing instance? After pito-cli update, re-embed your library with the chat reindex command (reindex game <id> / reindex vid <id>) — or, for everything at once, run pito-cli rake pito:embeddings:reindex.

Troubleshooting (dev; both ports are published to 127.0.0.1 there):

curl http://127.0.0.1:8091/health   # embedder — 200 once the model's loaded
curl http://127.0.0.1:8092/health   # nlmapper — same
docker compose logs embedder        # watch the model download

Reaching Pito from somewhere else

Pito installs on this machine and answers on 127.0.0.1:PORT. That is the whole supported story, and the installer sticks to it: it asks for a port, checks the port is free, and stops there. It does not create tunnels, request certificates, open firewall holes, or write systemd units for anything but Pito itself.

If you want to reach your instance from another machine, that is your call and your tooling. Whatever you use — a Cloudflare tunnel, a Tailscale node, an nginx or Caddy in front, an SSH port-forward — the shape is the same:

  • point it at 127.0.0.1:PORT (the internal load balancer, lb, which fronts the two zero-downtime deploy slots — see Zero-downtime deploys), and let WebSockets through, because the scrollback is a live cable stream;
  • terminate TLS in front of it. Pito forces HTTPS in production, so a plain http:// public address will not work;
  • set PITO_APP_BASE_URL in the install dir's .env to that public URL. That one value wires Host Authorization, link generation and asset delivery — Pito has to be told the name it is reached by.

An SSH forward is the smallest thing that works and needs none of the above:

ssh -N -L 3028:127.0.0.1:3028 you@your-box   # then open http://localhost:3028

Beyond the browser

The server renders ONE app; everything else is a thin window onto it. No separate APIs to version, no second UI to maintain — your instance already serves them all.

  • pito-android — a thin Hotwire Native shell around the same server-rendered app, with native navigation and back-stack (the path configuration lives at /configurations/android_v1.json). Self-hoster friendly by design: on first launch the app asks for your instance URL, so it works with ANY domain you host Pito on — not just the author's. Signed APKs ship on the releases page (no Play Store required), and Pito itself offers the download in a dismissible banner when you visit from an Android browser.

And when you want the guided-strut version of all this, pitomd.com (source) is the over-the-top showcase.

Docs

  • CLAUDE.md — working agreement, plan discipline, condensed architecture + stack principles (read first)
  • docs/architecture.md — topology, models, namespaces
  • docs/design.md — visual system: typography, theming, color/message palette, component rules
  • docs/extending.md — how to add a theme, a language, a new message-content type, or a new fx

Sponsor

Pito is free, AGPL, and costs nothing to give away — but it costs time. If it saves you the €50-a-month the others wanted, you can point a fraction of that back at keeping it alive and growing, through GitHub Sponsors:

👉 github.com/sponsors/gmrdad82

How it works: pick a tier — a few euros a month, or a one-time tip — and that's it. GitHub takes 0% and covers payment processing, so what you pledge is what lands. There's no paywall and never will be; sponsoring buys you exactly nothing extra except my genuine gratitude and the quiet satisfaction of keeping an indie tool indie. Monthly pledges are what keep the lights on and the feature list moving; one-time tips are the "this saved my afternoon" handshake. Either is appreciated more than the badge can convey.

Sounds

event file original source license
send /sounds/send.mp3 vs-pop_5.mp3 Pop_5.mp3 by Vilkas_Sound CC BY 4.0
receive /sounds/receive.mp3 pop-1.wav Pop 1 by theplax CC BY 4.0
notify /sounds/notify.mp3 mail.wav 516867 by PokeyWokey CC0

Support

Pito is one person's tool, but if you're stuck, lost, or just want to report that the cover art finally loaded, there's a Discord — pop in, ask away, judgment kept to a minimum 👉 discord.gg/q947UyDTqJ

Prefer elsewhere? Find me on X 👉 @GamerDady82, or on YouTube at @gmrdad82 — my engineering/personal channel, where Pito gets its tour. (The gaming side is the Manfy network linked up top.)

No SLA, no ticket queue, no "your call is important to us." Just a channel and a human who checks it between renders.

License

AGPL-3.0 — see LICENSE. Use it for whatever you like — self-host it, fork it, learn from it, build on it. Just don't pass it off as your own thing. No warranty, as-is. Questions? Ping me: gmrdad82 [at] gmail [dot] com.

About

Self-hosted YouTube Tool for creators who run multiple channels. Rails 8 + Hotwire + Postgres + Voyage AI + IGDB + YouTube API + Tailwind CSS.

Topics

Resources

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages