Playlist Mirror: Copy a YouTube Channel Into Your Playlist

Published today

YouTube has no button for this. There is no way to say “put every video of this channel into a playlist”, and no way to copy a playlist somebody else made into one of your own. The channel page is a wall of thumbnails with no memory, and a playlist you don’t own is read-only — you can save it, but you can’t touch its contents.

A playlist of your own can do the thing I actually want: work through it and delete each video once it’s watched, so what’s left is what’s left. That is not available for a channel’s uploads, and it is not available for someone else’s playlist either. The only way to get there is to build the playlist yourself, video by video.

I first went at this ten years ago with a bit of JavaScript in the browser console, which loaded all the videos on a channel page and did nothing else useful with them. This time I wrote the thing properly: Playlist Mirror, a self-hosted single-container app that copies a channel — or any playlist you can view — into a private playlist on your own account.

Register a Source, Then Walk Away

You paste a channel URL, an @handle, a channel ID, a playlist URL or a playlist ID. The app reads the source, tells you how many videos are in it and roughly how long the copy will take, and creates a private playlist once you agree. Watch Later and Liked videos are the exception — YouTube serves those to no outside app at all, so the app says so at the input rather than failing later.

From there it is meant to be left alone. Tick run this job daily, when credits allow and the job starts itself each day, adds what it can, and holds until tomorrow. If you’ve set up the optional Telegram bot, you get one message a day telling you what happened. A large channel copies itself over a week while you do something else, and the only thing you have to do is look at your phone occasionally.

That is the whole intended experience: register a source, get told when it’s done.

Why It Takes Several Days

Copying a playlist means asking Google to do it, and the YouTube Data API’s free tier is capped per day. Adding one video costs 50 of a project’s 10,000 daily units, which works out at roughly 200 videos a day before the API starts refusing. A 600-video channel is a three-day job no matter how the tool is written. There is no way to buy past it that’s worth doing for a personal app, and no way to hurry it.

So the constraint isn’t really negotiable, and the app is built around it rather than against it: it works out the cost before creating anything, spends the day’s allowance, stops on purpose when it runs out, and picks the job back up the next day. What would otherwise be a tool that dies at video 200 with a wall of errors is instead a job that takes a week and needs no attention.

The one caveat worth knowing is that the remaining-budget figure is an estimate — the API publishes no endpoint for it, so the app counts its own calls. Point a second tool at the same Google Cloud project and the count reads high, and jobs stop earlier than predicted.

Keeping Track

Every video is recorded in SQLite as it goes in, which is what makes the multi-day thing work at all. Resuming continues into the already created playlist and skips everything already in it: no duplicates, no second stray playlist, no lost progress — after a pause, a crash or a container restart alike.

Two smaller things fall out of keeping that state:

  • Re-check source picks up whatever the channel has uploaded since you started and queues it into the same mirror. There is no continuous syncing; it happens when you ask, or on the next daily run.
  • Deleted and private videos are skipped, not failed. A playlist routinely lists entries the source has since removed. They’re detected while reading the source, never inserted, never charged for and excluded from the target count — so a copy missing only those still reads as complete, which is the truthful answer.

The Rules Around Running Unattended

A daily run is the only place the app does anything without being asked at that moment, so the rules around it are deliberately narrow:

  • Opt in, per job. Nothing runs unattended unless you tick its box.
  • Once per day. One job cannot spend a day’s allowance twice.
  • Never against a fault. A failed job is skipped and flagged for you to look at, rather than being retried every morning into the same wall.
  • Never behind your back. A hand-started run always wins, and pressing Stop lifts the daily run.

The loop that checks all this is free — it reads the database and the app’s own ledger, never the API — so it can run every 60 seconds and usually find nothing to do.

Getting Told Over Telegram

A job that runs itself runs while nobody is watching, so being told what it did is the other half of the feature. There’s an optional Telegram bot for it, off until you set it up.

You get one message a day, not one per job: what ran, how far each job got, and why it stopped. Setting it up is making a bot with BotFather, pasting the token into the app, and sending your bot a message so it can find the chat by itself — no third-party bot involved, and no token in a config file.

Setup

docker compose up -d, open http://localhost:8000, and the app walks you through the Google Cloud side of it — project, YouTube Data API v3, OAuth client, sign-in. There’s no config file to edit by hand, and everything the app remembers lives in ./data, which is the only thing to back up.

There’s a Helm chart in itobey/charts if you’d rather run it on Kubernetes.

Running it anywhere other than localhost — a home server, a VPN box, a cluster — needs a slightly different Google setup, and getting it wrong is where an evening goes. The app’s own Account page tells you what to change, and the documentation covers it properly. Read it before you start rather than after the first failed sign-in.

A Word on Security

The app has no login of its own. Anyone who can reach it can use your YouTube account — create playlists, add videos, read your job history. It’s single-user by design and there’s no account system to add.

Keep it on your home network, behind a VPN, or behind a reverse proxy that does the authenticating. This is the sharpest edge in the whole project and I’d rather over-state it than have someone put it on a public IP because the docs were coy about it.

What Leaves the Container

Google, Telegram if you set it up, and one small anonymous ping — at startup and once a day after that — so I have some idea whether anyone is running this.

It’s counts and flags only: a random UUID for the installation, the app version, whether the sign-in was completed, how many jobs exist, how many videos have been copied, units charged, the daily limit in force, and whether Telegram is on. No account or email, no channel, playlist or video ids, no titles, no URLs, no bot token. The instance id is a random UUID rather than a hash of anything — a hash of an enumerable identifier can be confirmed by anyone holding the hash and a candidate list, and a random UUID has no preimage to test. The app only ever asks for the youtube scope, so it never learns your email address in the first place.

The current version is 0.1.1.

Comments

Comments are hosted on comments.itobey.dev. Loading them sets cookies and shares your IP address with that service.