Skip to content

seyallius/nomnom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

10 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

nomnom 🍽️

feeds it URLs. spits out videos. no questions asked.

A desktop GUI wrapper for yt-dlp built with Dioxus in Rust.
No more memorizing flags. No more copy-pasting commands. Just paste, click, eat.

Rust Dioxus Platform License


What is this?

nomnom is a point-and-click frontend for yt-dlp.
Instead of typing (and forgetting) flags every time, you:

  1. Paste a URL
  2. Pick a preset or toggle individual flags
  3. Choose where to save
  4. Hit download

That's it. It streams the output live so you can watch it nom.


Prerequisites

You need yt-dlp installed and available in your PATH.

# macOS
brew install yt-dlp

# Linux
pip install yt-dlp
# or grab the binary: https://github.com/yt-dlp/yt-dlp/releases

# Windows
winget install yt-dlp

Then clone and run:

git clone https://github.com/seyallius/nomnom
cd nomnom
cargo run --release

Features

Thing Details
Presets One-click configs for common tasks (Best Video, Audio Only, Playlist, Subtitles, Full Archive)
Flag toggles Every major yt-dlp flag as a button, grouped by category
Command preview See the exact command that will run, live, before you hit download
Folder picker Native OS folder dialog β€” no typing paths
Terminal panel Drop down to raw yt-dlp commands when you need full control
Live log Streamed stdout/stderr with color-coded lines
Async Downloads run in the background; UI never freezes

Project Structure

nomnom/
β”œβ”€β”€ Cargo.toml
└── src/
β”œβ”€β”€ main.rs             # window config, app launch
β”œβ”€β”€ app.rs              # root component, all shared state lives here
β”‚
β”œβ”€β”€ core/               # pure logic, no UI
β”‚ β”œβ”€β”€ mod.rs
β”‚ β”œβ”€β”€ flags.rs          # every yt-dlp flag definition
β”‚ β”œβ”€β”€ presets.rs        # preset collections (bundles of flags)
β”‚ └── runner.rs         # spawns yt-dlp, streams output
β”‚
└── components/         # UI components, one per concern
β”œβ”€β”€ mod.rs
β”œβ”€β”€ flag_panel.rs       # left sidebar: flag toggle buttons
└── output_log.rs       # bottom: live log output
β”œβ”€β”€ preset_panel.rs     # left sidebar: preset cards
β”œβ”€β”€ terminal_panel.rs   # middle: raw command input
β”œβ”€β”€ url_bar.rs          # top: URL input, folder picker, download button

How data flows

app.rs  (owns all Signals)
β”‚
β”œβ”€β”€β–Ά preset_panel reads/writes active_preset, active_flags
β”œβ”€β”€β–Ά flag_panel reads/writes active_flags
β”œβ”€β”€β–Ά url_bar reads/writes url, output_dir
β”‚    reads active_flags, built_command
β”‚    writes log_lines, is_running
β”œβ”€β”€β–Ά terminal_panel reads/writes is_running, log_lines
└──▢ output_log reads log_lines

All state is Signal<T> defined in app.rs and passed down as props.
No global state, no context magic. If you can read props, you can read the app.


How to Extend

This section is the whole point of this README.
Each extension has exactly one place to touch.


Add a new flag

Open src/core/flags.rs.
Find the all_flags() function. Add your flag to the right category:

Flag {
    flag: "--sponsorblock-remove all",
    label: "Skip Sponsors",
    description: "Remove sponsor segments via SponsorBlock",
    category: FlagCategory::Misc,
},

That's it. It will automatically appear as a toggle button in the UI under the correct category group.
No component changes needed.


Add a new flag category

Still in src/core/flags.rs:

  1. Add a variant to the FlagCategory enum:
pub enum FlagCategory {
    // ...existing...
    Chapters, // πŸ‘ˆ new
}
  1. Give it a label in the label() method:
FlagCategory::Chapters => "πŸ“‘ Chapters",
  1. Add it to the ordered list in src/components/flag_panel.rs:
let categories: Vec<FlagCategory> = vec![
    // ...existing...
    FlagCategory::Chapters, // πŸ‘ˆ add it where you want it to appear
];
  1. Tag your new flags with category: FlagCategory::Chapters in flags.rs.

Done. The panel renders it as a new group automatically.


Add a new preset

Open src/core/presets.rs.
Add a new Preset to the all_presets() vec:

Preset {
    id: "music_video", // unique, used for equality checks
    label: "Music Video",
    description: "Best video + audio, embed thumbnail, no playlist",
    icon: "🎸",
    flag_keys: vec![
        "--add-metadata",
        "--add-thumbnail",
        "--no-playlist",
        "--merge-output-format mp4",
    ],
},

flag_keys are matched against Flag::flag strings defined in flags.rs.
The preset card appears in the sidebar automatically. No UI code to touch.

Tip: if you reference a flag key that doesn't exist in all_flags(),
it is silently skipped. resolve_preset_flags() uses filter_map.


Change the default preset on startup

In src/core/presets.rs, default_preset() returns the first preset:

pub fn default_preset() -> Preset {
    all_presets().into_iter().next().unwrap()
}

Change .next() to .find(|p| p.id == "audio_only") to boot into a different preset:

pub fn default_preset() -> Preset {
    all_presets()
    .into_iter()
    .find(|p| p.id == "audio_only")
    .unwrap()
}

Change how the command is built

Open src/core/runner.rs.
build_command_string() builds the preview string.
run_download() builds the actual args: Vec<String> passed to the subprocess.

Both live next to each other. If you want to add a fixed flag that always runs
(e.g., always pass --no-warnings), add it to the args vec in run_download():

let mut args: Vec<String> = vec![
    "-o".to_string(),
    output_template,
    "--no-warnings".to_string(), // πŸ‘ˆ always present
];

Add a new UI panel

  1. Create src/components/your_panel.rs
  2. Define a #[component] function with a Props struct
  3. Register it in src/components/mod.rs:
pub mod your_panel;
  1. Import and place it in src/app.rs where you want it in the layout.
    Pass whatever Signal<T> it needs as props β€” they're all defined at the top of App().

The layout in app.rs is plain flexbox divs. The left sidebar and right column
are clearly commented. Drop your component where it makes visual sense.


Persist settings between sessions

Nothing is persisted yet. A good place to add it:

  • Read on startup: inside App() in app.rs, replace the use_signal(|| ...) defaults
    with values loaded from a config file.
  • Write on change: use use_effect in app.rs to watch signals and serialize on change.
  • Config format suggestion: ~/.config/nomnom/config.json via dirs::config_dir() (already in deps).

The Flag, Preset, and their fields all derive Serialize / Deserialize already.


Change the output filename template

In src/core/runner.rs, find:

rust let output_template = format!("{}/%(title)s.%(ext)s", output_dir.trim_end_matches('/'));

%(title)s, %(uploader)s, %(upload_date)s, %(id)s etc. are yt-dlp
output template fields.
Change the format string to whatever naming convention you want.
A future extension: expose this as a text input in url_bar.rs.


Architecture Philosophy

  • core/ knows nothing about Dioxus. It is plain Rust logic.
    You can test it, reuse it, or swap the frontend without touching it.

  • components/ know nothing about yt-dlp internals.
    They render state and fire events. That's all.

  • app.rs is the glue. It owns state and wires components together.
    If something feels hard to connect, the answer is usually: lift the signal to app.rs.

  • Signals flow down, events bubble up via callbacks or by passing writable signals as props.


License

MIT. eat freely.

About

YT-DLP GUI Wrapper, but it eats URLs and spits out videos. simple.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors