William Furney

Trying again to make Youtube great
software, internet

Youtube still has some problems which is slowly degrading the experience as well as my sanity. This makes sense if you consider Youtube's motivations towards improving the experience of the average user. But frankly, that isn't me. I could not care less about having video games, shorts or hell even really a recommendation feed. What I need is my subscriptions and a distraction free, clean browsing experience.

I am going to try and use freetube to solve this.

Build or install FreeTube

If you don't have freetube you can build it from source or use the flathub image. I like building my own AppImage with a docker file 1.

Export subscriptions from Google

We can export all my subscriptions and import them into freetube. Luckily google lets you do this easily with https://takeout.google.com.

Once you have it downloaded, extract out the relevant file from the archive: mkdir ~/yt-data; cd ~/yt-data; 7z e ~/Downloads/takeout-20260706T164038Z-3-001.zip 'Takeout/YouTube and YouTube Music/subscriptions/subscriptions.csv'

Here's some python code that imports the CSV file into the FreeTube config's profiles.db:

#!/usr/bin/env python3
import csv
import json
import re
import sys
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

CSV_PATH = "~/yt-data/subscriptions.csv"
DB_PATH = "~/.config/FreeTube/profiles.db"
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
OG_RE = re.compile(r'<meta property="og:image" content="([^"]+)"')


def fetch_thumbnail(channel_id):
    url = f"https://www.youtube.com/channel/{channel_id}"
    try:
        req = urllib.request.Request(url, headers={"User-Agent": UA})
        with urllib.request.urlopen(req, timeout=20) as resp:
            html = resp.read(200_000).decode("utf-8", errors="replace")
        m = OG_RE.search(html)
        if m:
            # match the size FreeTube uses for existing entries
            return m.group(1).replace("=s900-", "=s176-")
    except Exception as e:
        print(f"  thumbnail failed for {channel_id}: {e}", file=sys.stderr)
    return None


with open(CSV_PATH, newline="", encoding="utf-8") as f:
    rows = [r for r in csv.DictReader(f) if r.get("Channel Id")]

profiles = [json.loads(line) for line in DB_PATH.read_text().splitlines() if line.strip()]
existing = {s["id"] for p in profiles for s in p["subscriptions"]}
new_rows = [r for r in rows if r["Channel Id"] not in existing]
print(f"CSV channels: {len(rows)}, already subscribed: {len(rows) - len(new_rows)}, to add: {len(new_rows)}")

with ThreadPoolExecutor(max_workers=12) as pool:
    thumbs = list(pool.map(fetch_thumbnail, [r["Channel Id"] for r in new_rows]))

new_subs = [
    {"id": r["Channel Id"], "name": r["Channel Title"], "thumbnail": t}
    for r, t in zip(new_rows, thumbs)
]
missing = sum(1 for t in thumbs if t is None)
print(f"thumbnails fetched: {len(thumbs) - missing}, missing: {missing}")

for p in profiles:
    have = {s["id"] for s in p["subscriptions"]}
    p["subscriptions"].extend(s for s in new_subs if s["id"] not in have)
    p["subscriptions"].sort(key=lambda s: s["name"].lower())
    label = p.get("_id", p["name"])
    print(f"profile {label!r}: now {len(p['subscriptions'])} subscriptions")

DB_PATH.write_text("".join(json.dumps(p, ensure_ascii=False) + "\n" for p in profiles))
print("profiles.db updated")

Settings & Tweaks

Luckily these are already built into freetube, so enabling them is as simple as checking the boxes.

Built in proxy

If you so desire, freetube comes built in with proxy support wired up, that is, it can connect to a proxy you are running. I have found that using a shared IP seems to prevent videos from loading entirely.

Change whatever else you want

What I love about this setup is if I want something different I can just change it.

For example, I found that there was no built in yt-dlp support, so I just updated the code to add a button to call my yt-dlp executable.

And now I can download videos with the click of a button. The only limit here is your imagination, and I guess whatever Youtube does to roadblock users from controlling their own UI. All I know is now I have a moment to browse Youtube freely and not see shorts/video games and other crap Youtube thinks you want.

I also added Vimium style link highlighting and selection2. You can try out my fork with these features on my Codeberg here.


  1. I have a docker file/docker compose avaialable here for easier building. The process would be to build freetube in the container and then deploy it to a folder on your path. For Mac this would be /Applications and on Linux I default to ~/.local/bin. 

  2. See the screenshot in the readme for what this looks like here. 

* * *