Personal project, shared as-is. I built this for myself and my family. I'm sharing it in case it's useful to others, but I can't guarantee it works in every setup, and I'm not planning to build features on request. If something doesn't work for you, feel free to contribute.
A self-hosted YouTube companion: subscriptions, playlists, and watch history that live on your own server instead of Google's. Consists of a small FastAPI backend and a Firefox extension (desktop + Android).
app/ Python package — the FastAPI backend
__init__.py
main.py API routes
database.py SQLAlchemy / SQLite setup
models.py ORM models
frontend/
index.html Web UI — served by the backend at /
requirements.txt
mytube-extension/ Firefox WebExtension (desktop + Android)
mytube_sync-1.0.0.xpi Pre-signed build — install this directly, no AMO account needed
mytube-sync.service.example Systemd unit template
Requirements: Python 3.10+, a Linux server, an HTTPS reverse proxy (nginx, Caddy, apache, etc.).
sudo useradd -r -m -d /home/mytube -s /sbin/nologin mytube/home/mytube/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── database.py
│ └── models.py
├── frontend/
│ └── index.html
└── requirements.txt
cd /home/mytube
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtcd /home/mytube
venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 6060You should see uvicorn's startup output. Visit http://127.0.0.1:6060/ — it should
serve the web UI. Stop it with Ctrl+C once confirmed.
Copy the example unit file and edit it:
sudo cp mytube-sync.service.example /etc/systemd/system/mytube-sync.service
sudo nano /etc/systemd/system/mytube-sync.serviceThe working directory must be the parent of app/ (i.e. /home/mytube, not
/home/mytube/app), and uvicorn is invoked as app.main:app:
[Service]
User=mytube
Group=mytube
WorkingDirectory=/home/mytube
ExecStart=/home/mytube/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 6060Then enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now mytube-sync
sudo systemctl status mytube-sync| Variable | Default | Description |
|---|---|---|
MYTUBE_DB_PATH |
<WorkingDirectory>/data/mytube.db |
Path to the SQLite database file |
MYTUBE_ROOT_PATH |
(empty) | Path prefix if serving behind a reverse proxy subdirectory, e.g. /mytube-sync |
Set these in the [Service] block of the unit file:
Environment=MYTUBE_DB_PATH=/home/mytube/data/mytube.db
Environment=MYTUBE_ROOT_PATH=/mytube-syncThe extension runs on HTTPS (youtube.com), so the backend also needs HTTPS — browsers block cross-origin requests from HTTPS pages to plain HTTP backends.
Point your reverse proxy at 127.0.0.1:6060. Example nginx config:
location /mytube-sync/ {
proxy_pass http://127.0.0.1:6060/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}Example apache config:
ProxyPass /mytube-sync http://127.0.0.1:6060
ProxyPassReverse /mytube-sync http://127.0.0.1:6060
<Location /mytube-sync>
Require all granted
Header always set Access-Control-Allow-Headers "X-User-Id, Content-Type, Authorization"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
</Location>If you use a path prefix like /mytube-sync, set MYTUBE_ROOT_PATH=/mytube-sync in the
service environment.
There's no signup UI — create accounts via the API directly:
curl -X POST https://your-domain.example.com/mytube-sync/users \
-H "Content-Type: application/json" \
-d '{"name": "yourname"}'The response includes a token. Save it — this is what you'll paste into the extension
popup and the web UI. Each user has their own token; data is fully separated per user.
- Open
about:debugging#/runtime/this-firefox - Click "Load Temporary Add-on"
- Select
mytube-extension/manifest.json
The extension disappears when Firefox restarts — fine for quick testing, but you'll want a signed install for daily use (see below).
A signed build is included in this repo as mytube_sync-x.0.0.xpi.
- In Firefox, go to
about:addons - Gear icon → "Install Add-on From File"
- Select
mytube_sync-x.0.0.xpi
If you've modified the code and need a new signed build, see Sign it yourself below.
Firefox for Android only installs extensions from Mozilla's servers — it does not allow
sideloading .xpi files from local storage. There are a few ways around this:
Option A — Firefox Nightly (easiest for personal use)
Firefox Nightly has a developer mode that allows installing any signed .xpi directly:
- Install Firefox Nightly from the Play Store
- Open Nightly → Settings → About Firefox Nightly → tap the Firefox logo 5 times until "Debug menu enabled" appears
- Go back to Settings → Install Extension from File
- Transfer
mytube_sync-x.0.0.xpito the phone (USB, cloud storage, email attachment) and select it
This only works in Nightly, not regular Firefox for Android.
Option B — Custom AMO collection (regular Firefox for Android)
Regular Firefox for Android can be pointed at a custom AMO collection. This requires the extension to be hosted on AMO (even as an unlisted/private add-on):
- Upload the extension to AMO as an unlisted add-on (see Sign it yourself)
- Note your numeric AMO user ID — visible in your profile URL:
addons.mozilla.org/en-US/firefox/user/12345678/ - Create a Collection on AMO (My Collections → Create a Collection), give it any name and slug, and add this extension to it
- In Firefox for Android: Settings → About Firefox → tap the logo 5 times → Custom Add-on Collection → enter your user ID and collection slug
- Firefox restarts showing your collection under Add-ons — install from there
If you've modified the code and need a new signed build:
- Create a free account at addons.mozilla.org
- Zip the contents of
mytube-extension/—manifest.jsonmust be at the zip root:cd mytube-extension && zip -r ../mytube-extension.zip .
- addons.mozilla.org/developers → Submit a New Add-on → "On your own" (unlisted — not published publicly)
- Upload the zip — automated signing takes a few minutes, no human review
- Download the signed
.xpi
Keep the same extension ID. The UUID in
manifest.json'sbrowser_specific_settings.gecko.idmust stay the same across re-submissions — changing it makes Firefox treat the new build as a completely different extension and wipes stored settings. Only generate a new one if you want a clean break:python3 -c "import uuid; print('{' + str(uuid.uuid4()) + '}')"
Once the backend is running, opening its URL in a browser (e.g.
https://your-domain.example.com/mytube-sync/) serves a built-in management interface.
Enter your API token and press Enter to connect — no separate install needed.
Three fixed columns show Playlists, Watch History, and Subscriptions simultaneously.
A search bar at the top accepts a YouTube video URL or @channel handle and shows
inline actions (subscribe, like, watch later, mark watched). Data auto-refreshes every
60 seconds; the subscription feed loads in the background so the rest of the UI appears
immediately.
mytube-import.py imports your existing YouTube data from a Google Takeout export.
For subscriptions, playlists, and watch history:
- Go to takeout.google.com
- Deselect all, then select only YouTube and YouTube Music
- Under its options, make sure subscriptions, playlists, and history are included
- Export, download, and unzip the archive
For liked videos, and to fill in gaps in watch history (see "Watch history" below), get a second, separate export:
- Go to takeout.google.com
- Deselect all, then select only My Activity
- Under its options, filter to just the YouTube product
- Export, download, and unzip the archive — it can be a separate folder from the one above, or unzipped alongside it; the importer looks for both independently
No dependencies beyond Python's standard library:
python3 mytube-import.pyIt will prompt for your backend URL, API token, and the path to the unzipped export folder, then ask which data types to import (subscriptions / playlists / watch history / liked videos / all). Duplicates are silently skipped — safe to Ctrl+C and re-run.
The importer works regardless of your Google account's language — it locates the relevant files by inspecting their contents rather than assuming fixed folder/file names, so German, English, and other Takeout exports are all handled the same way.
Notes:
- Subscriptions: Takeout exports channel handles (
@ChannelName) where available. Channels that only have a rawUCxxxxxID are resolved to their@handlevia a web request to YouTube. If resolution fails, the channel is dropped — no raw IDs are stored. A dropped count is shown at the end. - Playlists: Imported as-is. "Watch Later" is mapped to the system playlist of the same name. Everything else (including "Favorites") becomes a regular playlist.
- Watch history: supports both the JSON and HTML export formats from the main Takeout category. JSON only goes back a limited window (a few months in practice); HTML covers a much longer history. On top of that, if a My Activity export (see above) is found, its watched-video entries are automatically merged in too — in practice its retention window doesn't fully overlap with the dedicated history export in either direction, so combining both gives noticeably more complete coverage than either alone. Duplicates between the two sources are merged by video, keeping whichever timestamp is more recent. Every entry carries its real per-entry timestamp, which is used for correct chronological ordering. Takeout has no watch-progress data, so all entries are imported as "completed" regardless of how much was actually watched.
- Liked videos: Takeout's "YouTube and YouTube Music" export does not include a Liked Videos list. The data does exist in your Google account, though: it's recorded in the separate My Activity export (filtered to YouTube) as individual "rated a video" activity entries, each with a real timestamp. The importer detects and imports these, populating the Liked Videos system playlist in correct chronological order. Disliked videos are also detected in the same export but not imported anywhere, since there's no "Disliked Videos" concept in MyTube Sync — they're only counted in the summary. German-language exports are confirmed working; the English wording used to detect a "liked" or "watched" entry is a best-effort guess, since no real English export was available to verify against — if detection doesn't work on your export, you can open an issue with the exact wording it uses.
- Subscription feed speed: the backend fetches YouTube RSS feeds live on every request
to
/subscriptions/feed. With many subscriptions this can be slow. No caching or background refresh is implemented. - No user management UI: creating and deleting users is done via direct API calls.
- Broad host permission: the extension requests
<all_urls>because the backend URL is entered at runtime and can't be known at build time. Firefox shows this as "Access your data for all websites" during install — this is expected. - Channel subscriptions store
@handleonly: if you visit a channel via its rawyoutube.com/channel/UCxxxxxURL and the page hasn't finished loading the canonical handle yet, the subscribe button will be temporarily disabled rather than risking storing the raw ID. Navigating to the channel's/@handleURL always works. - No dark/light mode toggle: the sidebar uses a fixed dark theme.
- Clicking a video link in the sidebar/history/playlists reloads the tab: YouTube's own
navigation links go through its internal SPA router, which stays on the page without a
reload. The video links here are plain
<a href>tags outside that router, so clicking one triggers a normal browser navigation instead — the tab reloads, and the extension reinitializes from scratch. Driving YouTube's router directly (e.g. viahistory.pushState/popstate) doesn't work reliably from a content script, since it depends on internal state and click handling that isn't exposed. A full reload is slower than an in-app transition but not broken — playback, history logging, and everything else pick back up normally afterward. - YouTube can't be installed as a real standalone app on Android in a usual way but
there's a workaround.* Requesting the desktop site (not just typing "youtube.com" on
mobile, but actually selecting "Desktop site" from the browser's page menu) serves a
different manifest that does pass Firefox's installability check, and installs as a real
standalone app. Once installed, opening it later auto-redirects to
m.youtube.comfor the actual mobile-friendly UI, but stays inside the installed PWA shell rather than reverting to a normal browser tab — so you get the real app experience (own icon, own task-switcher entry, no browser chrome) with the UI you'd actually want to use day to day.
MIT — do what you want with it, no warranty implied.