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) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-06-04 11:15:28 +02:00
co-authored by Claude Opus 4.8
parent 1d6467c91c
commit e5290b0a12
15 changed files with 328 additions and 4 deletions
+136
View File
@@ -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 >/dev/null 2>&1
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
+3 -1
View File
@@ -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"
;;
+29
View File
@@ -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
+1
View File
@@ -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
+5 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# omarchy:summary=Show the Omarchy Quickshell on-screen display
# omarchy:args=[-i|--icon <icon>] [-m|--message <text>] [-p|--progress <0-100>]
# omarchy:args=[-i|--icon <icon>] [-m|--message <text>] [-p|--progress <0-100>] [-d|--duration <ms>]
# 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"
+3
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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);
});
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 468.553"><path fill-rule="nonzero" d="M97.632 0h316.736c26.861 0 51.272 10.989 68.953 28.673C501.017 46.363 512 70.787 512 97.632v273.293c0 26.841-10.995 51.255-28.682 68.945-17.691 17.691-42.112 28.683-68.95 28.683H97.632c-26.845 0-51.269-10.986-68.959-28.676C10.989 422.193 0 397.785 0 370.925V97.632c0-26.867 10.983-51.281 28.667-68.965C46.351 10.983 70.765 0 97.632 0zm140.509 314.599l-3.333-105.58 36.408-1.151 3.276 103.999 42.188-43.498 26.112 25.404-87.819 90.577-85.765-88.532 26.125-25.404 42.808 44.188v-.003zM28.619 122.962H88.37l36.214-92.199c.286-.726.62-1.441.999-2.144H97.632c-18.963 0-36.218 7.77-48.731 20.282-12.512 12.513-20.282 29.768-20.282 48.731v25.33zm122.084-94.343l-37.057 94.343h108.69l36.129-91.983c.315-.798.687-1.587 1.116-2.36H150.703zm133.744 0l-37.054 94.343h105.793l36.215-92.199c.286-.726.62-1.441.998-2.144H284.447zm131.067.009l-37.051 94.334h104.918v-25.33c0-18.947-7.776-36.205-20.295-48.724-12.258-12.261-29.068-19.974-47.572-20.28zm67.867 122.947H28.619v219.35c0 18.956 7.776 36.204 20.289 48.717 12.519 12.518 29.777 20.292 48.724 20.292h316.736c18.941 0 36.196-7.78 48.715-20.299 12.518-12.518 20.298-29.773 20.298-48.71v-219.35z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,23 @@
{
"manifest_version": 3,
"name": "Download Video",
"version": "1.0",
"description": "Download the current page's video, this extension is installed by Omarchy",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4fww2jejSazrS0dpOBYyx8IfWRkRmek213PnIOaqzAuLK1jE1T+rR+SqBaKnC9j311O6sGrQ+MZJHotJfZyXoKBvSlfiay2z3NU0+Qv1Xa2VaRDuFoHzpfc4OcPvN1B/dGutLCxcLjvSBWEomGVSV1RpK+fgcOeJPFOjKLVrajedv9RnV3JE/Mz3VqBIpkzCUBtsM2eEpZZYWOWuyyk+h6RlbGkHXwudzizf7uJ7bA6RzZKF92wqnbgKDvGji0TwolzAJKALFTul44JzYNiU3Cy7FJXv/aXAKXwAf1VOXXMKie6udXv/M0E1vZAIWHv1sGhkxZZFxZ3C33svlXfg7wIDAQAB",
"permissions": ["activeTab", "scripting", "nativeMessaging"],
"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
},
"action": {
"default_title": "Download Video"
},
"commands": {
"download-video": {
"suggested_key": {"default": "Alt+Shift+D"},
"description": "Download Video"
}
},
"background": {"service_worker": "background.js"}
}
@@ -0,0 +1,9 @@
{
"name": "com.omarchy.ytdlp",
"description": "Omarchy yt-dlp download host",
"path": "__HOST_PATH__",
"type": "stdio",
"allowed_origins": [
"chrome-extension://dedjgknigfeelejglamclffonmophnfl/"
]
}
+1
View File
@@ -139,4 +139,5 @@ xmlstarlet
xournalpp
yaru-icon-theme
yay
yt-dlp
zoxide
+25
View File
@@ -0,0 +1,25 @@
echo "Add yt-dlp download extension (Alt+Shift+D) to Chromium-based browsers"
YTDLP_EXT="$OMARCHY_PATH/default/chromium/extensions/yt-dlp"
add_ytdlp_extension() {
local file=$1
[[ -f $file ]] || return 0
grep -q "extensions/yt-dlp" "$file" && return 0
if grep -q "^--load-extension=" "$file"; then
sed -i --follow-symlinks "s|^--load-extension=\(.*\)$|--load-extension=\1,$YTDLP_EXT|" "$file"
else
echo "--load-extension=$YTDLP_EXT" >>"$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
+50
View File
@@ -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"