ytpg is a Postgres wire-protocol proxy that stores your data on YouTube. Each table is an unlisted video. Each row is a caption cue. Every commit updates a pinned comment. There is no local database. If the process crashes it rebuilds itself from YouTube on the next start.
This is a project for fun. Please don't ban me Google.
- Python 3.11 or newer
- A Google account
- A YouTube channel you own (the ID starts with
UC) uvorpip
Over at https://console.cloud.google.com/, you need to:
- Make a project.
- Enable the YouTube Data API v3.
- Set up the OAuth consent screen as External, leave it in Testing mode, and add your own Google account as a test user.
- Create an OAuth client ID of type Desktop app and download the JSON.
- Save it as
client_secrets.jsonin the folder where you runytpg.
The YouTube API gives you 10,000 units per day by default. The operations that matter:
| Operation | Cost |
|---|---|
videos.list |
1 |
videos.update |
50 |
captions.download |
200 |
captions.update |
400 |
videos.insert |
1600 |
Roughly 400 units per write. So expect about 25 writes a day before the API politely declines further service. This is not a database you run a business on.
git clone https://github.com/neonwalker/ytpg.git
cd ytpg
python -m venv .venv && source .venv/bin/activate
pip install -e .Or with uv:
uv venv
source .venv/bin/activate
uv pip install -e .Drop the client_secrets.json from step 2.4 into this same folder, or pass --client-secrets /path/to/file to ytpg init later.
ytpg wants the UC... channel ID, not the @handle.
- Sign in to YouTube with the account that owns the channel.
- Go to https://www.youtube.com/account_advanced
- Copy the Channel ID. It starts with
UCand is 24 characters long.
ytpg init --channel-id UCxxxxxxxxxxxxxxxxxxxxxThis pops open a browser for the OAuth dance. Sign in as one of the test users you added earlier. Then ytpg will:
- Save refresh credentials to
~/.config/ytpg/<channel_id>.json. - Create (or find) the manifest video
ytpg::__manifest__on your channel. That is an unlisted video whose description holds the table registry as JSON. Yes, really. - Write a tiny bootstrap cache so next time it can find the manifest without searching.
If OAuth goes sideways, run it again with --reauth to force a fresh login.
ytpg serve --channel-id UCxxxxxxxxxxxxxxxxxxxxx
# ytpg proxy running on localhost:5432 (channel=UC...)Then connect with anything that speaks Postgres:
psql -h localhost -p 5432 -U ytpg -d UCxxxxxxxxxxxxxxxxxxxxxCREATE TABLE users (id INT, name TEXT NOT NULL, active BOOL);
CREATE TABLE orders (id INT, user_id INT, total FLOAT);
INSERT INTO users (id, name, active) VALUES (1, 'Alice', true);
INSERT INTO orders (id, user_id, total) VALUES (10, 1, 99.5);
SELECT u.name, o.total
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
ORDER BY o.total DESC NULLS LAST;
UPDATE users SET active = false WHERE id = 1;
\dt
\d usersCongrats, you just wrote a row to a video.
ytpg status --channel-id UCxxxxxxxxxxxxxxxxxxxxxShows the tables you have and how many recycled videos are sitting around ready for the next CREATE TABLE to claim.
- DDL:
CREATE TABLE,DROP TABLE(soft delete, the video gets recycled for later). - DML:
INSERT,UPDATE,DELETE.UPDATEis really a DELETE plus an INSERT under the hood. - SELECT: full scan,
COUNT(*),LIMIT, multi-columnORDER BYwith per-columnASC/DESCandNULLS FIRST/NULLS LAST. - JOINs:
INNER,LEFT,RIGHT,FULL OUTER,CROSS. Multi-way chains too. Each joined table is one morecaptions.download. - WHERE:
=,!=,<,>,<=,>=,IN,IS [NOT] NULL,LIKE/ILIKE,AND,OR,NOT, parens. - Types:
INT,FLOAT,BOOL,TEXTfamilies, with Postgres-ish implicit coercion. Bad casts raise22P02. NOT NULL: enforced at plan time. Raises23502.- psql meta commands:
\dt,\l,\dn,\d <table>. - Crash recovery: the LSN rehydrates from the pinned checkpoint on startup.
- Subqueries, transactions, foreign keys,
GROUP BY/HAVING, window functions, CTEs, indexes. JOINinsideUPDATE/DELETE.USINGandNATURAL JOIN.- The extended query protocol (Parse/Bind/Execute). Some drivers will be unhappy.