© 2026 Naveen Kumar Pendyam. All rights reserved.

contact@nkpendyam.me
GitHubLinkedInBlogContact
    All posts
    python
    cli
    fastapi
    tools
    student-journey

    Kyro Downloader: one engine, four UIs, and a lot of learning about contracts

    Naveen Kumar Pendyam
    Monday, April 6, 2026
    4 min read

    Kyro Downloader: one engine, four UIs, and a lot of learning about contracts

    Kyro Downloader is a Python media downloader wrapped around yt-dlp. The thing that makes it interesting (to me anyway) is that it has four entry points — CLI, TUI, desktop GUI, and a web UI — and they all share the same core download engine.

    Why did I do this to myself? Because I kept reading tutorials where the CLI version and the GUI version of the same tool drifted apart over time, and I wanted to know if it was avoidable.

    It is avoidable. It's just a lot of work.

    The shape

      downloader/
        core/        ← the engine. knows about yt-dlp, presets, queues.
        cli/         ← Click-style commands.
        tui/         ← Textual app.
        gui/         ← CustomTkinter.
        web/         ← FastAPI + WebSockets.
    

    Every UI calls the same core functions. None of them know about yt-dlp directly. If I want to swap engines tomorrow, I rewrite core/ and the four UIs come along for free.

    The lesson I learned the hard way

    The first version did not look like that. The first version had the CLI call yt-dlp directly and the GUI call it too, and the web UI had its own version of "is this a playlist." When I tried to add a feature (subtitle downloads), I had to change four things. I forgot one. Things broke. I learned.

    I rewrote it with a core/ module and a single Downloader class, and every UI is now a thin wrapper that:

    1. Parses input (URL, flags, form fields, button clicks).
    2. Calls core.download(...) with a typed config object.
    3. Subscribes to progress events.
    4. Renders progress its own way.

    That's it. Four UIs, one engine.

    Presets: the feature I'm most happy with

    There are presets like "voice memo" (low-bitrate mono MP3), "podcast" (medium-bitrate stereo), and "lossless" (FLAC). Each preset is a single dataclass in core/presets.py. The CLI exposes them as flags. The web UI exposes them as a dropdown. The GUI exposes them as buttons. Same presets everywhere — change the dataclass, every UI updates.

    This is the kind of thing AI is really good for. I described what I wanted ("presets that work the same way in every interface") to Claude and it suggested the dataclass + registry pattern. Once I saw it, it was obvious. But I needed to see it.

    The WebSocket part

    The web UI needs progress updates in real time. FastAPI + WebSockets does this nicely, but I'd never done WebSocket plumbing before. The pattern that worked:

    • Core engine emits progress events to an in-memory pub/sub.
    • WebSocket handler subscribes and forwards to the connected client.
    • Client renders.

    Nothing fancy. The mistake I made the first time was tying the WebSocket directly to the download — so if the client disconnected, the download stopped. Splitting them with a pub/sub means the download keeps going and reconnecting clients catch up.

    What AI helped with

    • Naming the engine boundary. I went back and forth on whether it was a "Downloader," "Engine," "Service," etc. AI didn't pick the name for me but it helped me articulate why each name implied different things.
    • Textual quirks. Textual is a great TUI framework but the docs are still evolving. AI was useful for "I want a progress bar inside a scrollable list, here's my markup, why won't it render."
    • Async/await sanity checks. Python async is a footgun. I'd write something, Claude would say "you're awaiting a sync function, this does nothing," and I'd be embarrassed for ten seconds and then fix it.

    What I'd do differently

    The packaging story is rough. I have a pyproject.toml but I don't have proper platform builds for the GUI (PyInstaller stuff). That's the next big chunk of work. A user on Windows shouldn't have to pip install and pray.

    The honest "why bother"

    Could I have just used yt-dlp directly from the terminal? Sure. Most days I do. But building this taught me about engine boundaries in a way no tutorial did. The next time I write a tool that needs multiple interfaces, I won't have to think hard about how to share the core — I already know the shape.

    Related posts

    Credit risk analysis as a BCA student: turning a Kaggle dataset into something a business person could read

    What I learned building a credit card default risk pipeline — why ROC-AUC alone isn't enough, why risk bands matter, and how AI helped me think like an analyst instead of just a coder.

    Lung cancer prediction from survey data: what a small, imbalanced dataset taught me

    A Jupyter notebook walk-through of classifying lung cancer risk from survey data, with SMOTE, feature engineering, and seven models compared. Plus everything I had to unlearn about accuracy.

    How I built a brain tumor detector (with a lot of AI help)

    A BCA student's honest write-up of building a multi-stage MRI analysis pipeline — YOLO, an ensemble of vision models, Grad-CAM, and a healthy fear of saying it's a medical device.

    Back to all posts