From e5290b0a12c556cf2bbecb01e3441605dce4d602 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 4 Jun 2026 11:15:28 +0200 Subject: [PATCH] Add yt-dlp "Download Video" Chromium extension Download the current page's video with yt-dlp via Alt+Shift+D or a click on the toolbar icon. A native-messaging host runs the download, shows live progress on the Quickshell OSD, and posts a clickable "Download complete" toast that opens the file in mpv. - Extension: pinned key for a stable id, green download-video icon, keyboard command + toolbar action (reads the active tab URL). - Native host (omarchy-chromium-ytdlp-host): verifies the URL with yt-dlp --simulate (else "No video found"), streams progress to the OSD (time-throttled to ~4/s), saves to ~/Videos, opens mpv on click. - Installer (omarchy-install-chromium-ytdlp) writes the native-messaging manifest into installed Chromium/Chrome/Brave/Edge profiles; wired into browser install and chromium refresh, with a migration for existing users. - omarchy-osd: add -d/--duration so the OSD can persist during a download. - Add yt-dlp to base packages, load the extension via --load-extension, and document the Alt+Shift+D binding. Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/omarchy-chromium-ytdlp-host | 136 ++++++++++++++++++ bin/omarchy-install-browser | 4 +- bin/omarchy-install-chromium-ytdlp | 29 ++++ bin/omarchy-menu-keybindings | 1 + bin/omarchy-osd | 7 +- bin/omarchy-refresh-chromium | 3 + config/chromium-flags.conf | 2 +- .../chromium/extensions/yt-dlp/background.js | 41 ++++++ .../extensions/yt-dlp/download-video.svg | 1 + default/chromium/extensions/yt-dlp/icon.png | Bin 0 -> 13630 bytes .../chromium/extensions/yt-dlp/manifest.json | 23 +++ .../com.omarchy.ytdlp.json | 9 ++ install/omarchy-base.packages | 1 + migrations/1780517689.sh | 25 ++++ test/shell.d/chromium-ytdlp-test.sh | 50 +++++++ 15 files changed, 328 insertions(+), 4 deletions(-) create mode 100755 bin/omarchy-chromium-ytdlp-host create mode 100755 bin/omarchy-install-chromium-ytdlp create mode 100644 default/chromium/extensions/yt-dlp/background.js create mode 100644 default/chromium/extensions/yt-dlp/download-video.svg create mode 100644 default/chromium/extensions/yt-dlp/icon.png create mode 100644 default/chromium/extensions/yt-dlp/manifest.json create mode 100644 default/chromium/native-messaging-hosts/com.omarchy.ytdlp.json create mode 100644 migrations/1780517689.sh create mode 100755 test/shell.d/chromium-ytdlp-test.sh diff --git a/bin/omarchy-chromium-ytdlp-host b/bin/omarchy-chromium-ytdlp-host new file mode 100755 index 00000000..5fc5842d --- /dev/null +++ b/bin/omarchy-chromium-ytdlp-host @@ -0,0 +1,136 @@ +#!/bin/bash + +# omarchy:summary=Native messaging host: download the URL sent by the yt-dlp Chromium extension +# omarchy:hidden=true + +set -euo pipefail + +SCRIPT_PATH="${BASH_SOURCE[0]}" + +# The browser launches us without Omarchy's environment, so locate the repo from +# our own path when OMARCHY_PATH isn't already set. Export it — omarchy-shell (used +# by omarchy-osd) needs it to find the running shell, and silently no-ops without it. +export OMARCHY_PATH="${OMARCHY_PATH:-$(cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)}" + +# Make sure the Omarchy bin and yt-dlp are reachable when launched by the browser. +export PATH="$OMARCHY_PATH/bin:/usr/local/bin:/usr/bin:$PATH" + +DOWNLOAD_DIR="${OMARCHY_YTDLP_DIR:-$HOME/Videos}" + +parse_url() { + jq -r '.url // empty' 2>/dev/null <<<"$1" || true +} + +valid_url() { + [[ $1 =~ ^https?:// ]] +} + +# Drive the Quickshell OSD — a single overlay that updates in place (like the +# volume/brightness bar), so download progress never stacks like notifications. +osd_progress() { + omarchy-osd -i 󰇚 -p "$1" -d 8000 >/dev/null 2>&1 || true +} + +osd_close() { + omarchy-shell -q osd close >/dev/null 2>&1 || true +} + +download_url() { + local url="$1" + + mkdir -p "$DOWNLOAD_DIR" + + # Don't show anything until yt-dlp confirms there's actually a video to grab. + if ! yt-dlp --no-playlist --simulate --quiet --no-warnings "$url" >/dev/null 2>&1; then + omarchy-notification-send -u critical -g 󰅖 "No video found for download" "$url" + exit 0 + fi + + osd_progress 0 + + # Stream the download: OMARCHY_PROG carries the percent (drives the OSD), + # OMARCHY_FILE (printed only after a successful move) carries title + path. + local line rest pct intpct last="" er nowms lastms=0 title="" filepath="" + while IFS= read -r line; do + case $line in + OMARCHY_PROG*) + pct=${line#OMARCHY_PROG$'\t'} + intpct=${pct%%.*} + intpct=${intpct//[^0-9]/} + [[ -n $intpct && $intpct != "$last" ]] || continue # skip no-op repeats + # Throttle to ~4 redraws/sec so fast downloads don't spawn a flurry of processes. + er=$EPOCHREALTIME + nowms=$((${er%[.,]*} * 1000 + 10#${er##*[.,]} / 1000)) + ((nowms - lastms >= 250)) || continue + last=$intpct + lastms=$nowms + osd_progress "$intpct" + ;; + OMARCHY_FILE*) + rest=${line#OMARCHY_FILE$'\t'} + title=${rest%%$'\t'*} + filepath=${rest#*$'\t'} + ;; + esac + done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --restrict-filenames --no-simulate \ + --quiet --no-warnings --progress --newline \ + --progress-template $'download:OMARCHY_PROG\t%(progress._percent_str)s' \ + --paths "$DOWNLOAD_DIR" -o '%(title)s [%(id)s].%(ext)s' \ + --print $'after_move:OMARCHY_FILE\t%(title)s\t%(filepath)s' \ + "$url" 2>&1) + + osd_close + + # after_move only prints on a successful download+move, so a captured path == success. + if [[ -n $filepath ]]; then + ((${#title} > 50)) && title="${title:0:50}…" # keep the toast compact + + # Square, center-cropped thumbnail so the notification preview isn't stretched. + local preview + preview="$(mktemp --suffix=.jpg)" + ffmpeg -y -i "$filepath" -ss 00:00:00.1 -vframes 1 \ + -vf "crop='min(iw,ih)':'min(iw,ih)',scale=256:256" -q:v 2 \ + "$preview" -loglevel quiet 2>/dev/null || true + + # Clicking the toast opens the video in mpv (-a blocks until clicked or timed out). + ( + if [[ -n $(omarchy-notification-send -g 󰄬 "Download complete" "$title" -t 10000 --image "${preview:-$filepath}" -a) ]]; then + mpv "$filepath" + fi + rm -f "$preview" + ) & + else + omarchy-notification-send -u critical -g 󰅖 "Download failed" "$url" + fi + + exit 0 +} + +main() { + local length payload url + + # Detached worker: this is what actually runs yt-dlp and fires notifications. + if [[ "${1:-}" == "--download" ]]; then + download_url "$2" + fi + + # Native messaging frame: 4-byte little-endian length prefix, then UTF-8 JSON. + length=$(head -c4 | od -An -v -tu4 --endian=little | tr -d ' ') + [[ -n ${length:-} ]] && ((length > 0)) || exit 0 + + payload=$(head -c "$length") + + # Ack with an empty message so the extension's sendNativeMessage callback resolves cleanly. + printf '\x02\x00\x00\x00{}' + + url=$(parse_url "$payload") + [[ -n $url ]] || exit 0 + valid_url "$url" || exit 0 + + # Detach the download so this host exits promptly and frees the browser's port. + setsid -f "$SCRIPT_PATH" --download "$url" /dev/null 2>&1 +} + +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + main "$@" +fi diff --git a/bin/omarchy-install-browser b/bin/omarchy-install-browser index 3359d922..f5a187df 100755 --- a/bin/omarchy-install-browser +++ b/bin/omarchy-install-browser @@ -17,6 +17,7 @@ announce_browser_installed() { copy_chromium_flags() { mkdir -p ~/.config cp -f "$OMARCHY_PATH/config/chromium-flags.conf" "$1" + omarchy-install-chromium-ytdlp } setup_firefox_preferences() { @@ -66,7 +67,8 @@ brave-origin) setup_policy_directory /etc/brave/policies/managed mkdir -p ~/.config # FIXME: Use normal chromium flags when Brave Origin wrapper has been fixed - echo "--load-extension=~/.local/share/omarchy/default/chromium/extensions/copy-url" > ~/.config/brave-origin-beta-flags.conf + echo "--load-extension=~/.local/share/omarchy/default/chromium/extensions/copy-url,~/.local/share/omarchy/default/chromium/extensions/yt-dlp" > ~/.config/brave-origin-beta-flags.conf + omarchy-install-chromium-ytdlp omarchy-theme-set-browser announce_browser_installed "Brave Origin" ;; diff --git a/bin/omarchy-install-chromium-ytdlp b/bin/omarchy-install-chromium-ytdlp new file mode 100755 index 00000000..c700578c --- /dev/null +++ b/bin/omarchy-install-chromium-ytdlp @@ -0,0 +1,29 @@ +#!/bin/bash + +# omarchy:summary=Install the native messaging host for the yt-dlp Chromium extension + +set -euo pipefail + +HOST_NAME="com.omarchy.ytdlp" +HOST_PATH="$OMARCHY_PATH/bin/omarchy-chromium-ytdlp-host" +TEMPLATE="$OMARCHY_PATH/default/chromium/native-messaging-hosts/$HOST_NAME.json" + +# Chromium-based browser profile roots that use the NativeMessagingHosts layout. +browser_dirs=( + "$HOME/.config/chromium" + "$HOME/.config/google-chrome" + "$HOME/.config/google-chrome-beta" + "$HOME/.config/google-chrome-unstable" + "$HOME/.config/BraveSoftware/Brave-Browser" + "$HOME/.config/BraveSoftware/Brave-Browser-Beta" + "$HOME/.config/BraveSoftware/Brave-Browser-Nightly" + "$HOME/.config/microsoft-edge" + "$HOME/.config/microsoft-edge-dev" +) + +manifest=$(sed "s|__HOST_PATH__|$HOST_PATH|g" "$TEMPLATE") + +for dir in "${browser_dirs[@]}"; do + mkdir -p "$dir/NativeMessagingHosts" + printf '%s\n' "$manifest" >"$dir/NativeMessagingHosts/$HOST_NAME.json" +done diff --git a/bin/omarchy-menu-keybindings b/bin/omarchy-menu-keybindings index 5ca6ca88..f2d82d1b 100755 --- a/bin/omarchy-menu-keybindings +++ b/bin/omarchy-menu-keybindings @@ -379,6 +379,7 @@ dynamic_bindings() { # Hardcoded bindings, like the copy-url extension and such static_bindings() { echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L," + echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D," } # Parse and format keybindings diff --git a/bin/omarchy-osd b/bin/omarchy-osd index 95af756f..99442989 100755 --- a/bin/omarchy-osd +++ b/bin/omarchy-osd @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Show the Omarchy Quickshell on-screen display -# omarchy:args=[-i|--icon ] [-m|--message ] [-p|--progress <0-100>] +# omarchy:args=[-i|--icon ] [-m|--message ] [-p|--progress <0-100>] [-d|--duration ] # omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello" set -euo pipefail @@ -11,12 +11,14 @@ message="" progress="" progress_text="" max="100" +duration="" while (($#)); do case $1 in -i|--icon) icon="${2:-}"; shift 2 ;; -m|--message) message="${2:-}"; shift 2 ;; -p|--progress) progress="${2:-}"; shift 2 ;; + -d|--duration) duration="${2:-}"; shift 2 ;; -h|--help) omarchy osd --help; exit 0 ;; *) echo "Unknown OSD option: $1" >&2; exit 1 ;; esac @@ -32,6 +34,7 @@ payload=$(jq -cn \ --arg value "$progress" \ --arg progressText "$progress_text" \ --arg max "$max" \ - '{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max}') + --arg duration "$duration" \ + '{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration}') omarchy-shell -q osd show "$payload" diff --git a/bin/omarchy-refresh-chromium b/bin/omarchy-refresh-chromium index f8d13f73..77165837 100755 --- a/bin/omarchy-refresh-chromium +++ b/bin/omarchy-refresh-chromium @@ -15,6 +15,9 @@ fi # Refresh the Chromium configuration omarchy-refresh-config chromium-flags.conf +# Install/refresh the native messaging host used by the yt-dlp extension +omarchy-install-chromium-ytdlp + # Re-install Google accounts if previously configured if [[ $INSTALL_GOOGLE_ACCOUNTS == "true" ]]; then omarchy-install-chromium-google-account diff --git a/config/chromium-flags.conf b/config/chromium-flags.conf index 88c8082b..78c39070 100644 --- a/config/chromium-flags.conf +++ b/config/chromium-flags.conf @@ -1,4 +1,4 @@ --ozone-platform=wayland --ozone-platform-hint=wayland --enable-features=TouchpadOverscrollHistoryNavigation ---load-extension=~/.local/share/omarchy/default/chromium/extensions/copy-url +--load-extension=~/.local/share/omarchy/default/chromium/extensions/copy-url,~/.local/share/omarchy/default/chromium/extensions/yt-dlp diff --git a/default/chromium/extensions/yt-dlp/background.js b/default/chromium/extensions/yt-dlp/background.js new file mode 100644 index 00000000..07006501 --- /dev/null +++ b/default/chromium/extensions/yt-dlp/background.js @@ -0,0 +1,41 @@ +function sendUrl(url) { + if (!url || !/^https?:/i.test(url)) return; + + // The native messaging host runs yt-dlp and owns all the desktop + // notifications, so we just hand off the URL and ignore the reply. + chrome.runtime.sendNativeMessage('com.omarchy.ytdlp', { url }, () => { + void chrome.runtime.lastError; + }); +} + +function triggerDownload(tab) { + if (!tab) return; + + // The activeTab permission exposes tab.url whenever the user invokes the + // extension — both via the toolbar click and the keyboard shortcut. + if (tab.url) { + sendUrl(tab.url); + return; + } + + // Fallback: read the URL straight from the page. + if (tab.id === undefined) return; + chrome.scripting + .executeScript({ target: { tabId: tab.id }, func: () => location.href }) + .then((results) => sendUrl(results && results[0] && results[0].result)) + .catch(() => {}); +} + +// Keyboard shortcut (Alt+Shift+D). +chrome.commands.onCommand.addListener((command) => { + if (command === 'download-video') { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + triggerDownload(tabs[0]); + }); + } +}); + +// Clicking the extension's toolbar icon. +chrome.action.onClicked.addListener((tab) => { + triggerDownload(tab); +}); diff --git a/default/chromium/extensions/yt-dlp/download-video.svg b/default/chromium/extensions/yt-dlp/download-video.svg new file mode 100644 index 00000000..d3731d19 --- /dev/null +++ b/default/chromium/extensions/yt-dlp/download-video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/default/chromium/extensions/yt-dlp/icon.png b/default/chromium/extensions/yt-dlp/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..838c9e77e50e8bd33e2116ded0fa3ff6cd48867c GIT binary patch literal 13630 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"$file" + fi +} + +for conf in chromium chrome google-chrome brave brave-beta brave-nightly brave-origin-beta microsoft-edge-stable; do + add_ytdlp_extension "$HOME/.config/$conf-flags.conf" +done + +omarchy-pkg-add yt-dlp + +# Register the native messaging host that runs yt-dlp for the extension. +omarchy-install-chromium-ytdlp || true diff --git a/test/shell.d/chromium-ytdlp-test.sh b/test/shell.d/chromium-ytdlp-test.sh new file mode 100755 index 00000000..3d33494f --- /dev/null +++ b/test/shell.d/chromium-ytdlp-test.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +TMPDIR="" + +export PATH="$ROOT/bin:$PATH" + +cleanup() { + [[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR" +} +trap cleanup EXIT + +require_command jq + +TMPDIR=$(mktemp -d) +test_home="$TMPDIR/home" +manifest_path="$test_home/.config/chromium/NativeMessagingHosts/com.omarchy.ytdlp.json" + +HOME="$test_home" OMARCHY_PATH="$ROOT" omarchy-install-chromium-ytdlp + +[[ -f $manifest_path ]] || fail "yt-dlp native host installer creates fresh Chromium profile root" +pass "yt-dlp native host installer creates fresh Chromium profile root" + +jq -e --arg path "$ROOT/bin/omarchy-chromium-ytdlp-host" ' + .name == "com.omarchy.ytdlp" and + .path == $path and + (.allowed_origins | index("chrome-extension://dedjgknigfeelejglamclffonmophnfl/")) +' "$manifest_path" >/dev/null +pass "yt-dlp native host manifest uses Omarchy host path and extension id" + +parse_result=$(bash -c ' + OMARCHY_PATH="$3" + source "$1" + parse_url "$2" +' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" '{"url":"https://example.test/watch?v=\"quoted\"&name=a\\b"}' "$ROOT") + +[[ $parse_result == "https://example.test/watch?v=\"quoted\"&name=a\\b" ]] || + fail "yt-dlp native host parses escaped JSON URLs" "$parse_result" +pass "yt-dlp native host parses escaped JSON URLs" + +bash -c ' + OMARCHY_PATH="$3" + source "$1" + valid_url "$2" +' bash "$ROOT/bin/omarchy-chromium-ytdlp-host" "javascript:alert(1)" "$ROOT" && + fail "yt-dlp native host rejects non-web URLs" +pass "yt-dlp native host rejects non-web URLs"