#!/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?:// ]] } # A printed path is only usable if it is a regular file inside DOWNLOAD_DIR. # Forged records (leading-dash mpv options, paths with control chars, or # anything that escaped the download directory) must not reach the click command. resolve_download_file() { local candidate=$1 file_real dir_real [[ -n $candidate ]] || return 1 [[ $candidate != *$'\n'* && $candidate != *$'\r'* && $candidate != *$'\t'* ]] || return 1 [[ -f $candidate ]] || return 1 # Read to a NUL: command substitution strips trailing newlines, which would # resolve a name ending in one to a different file that may well exist. IFS= read -r -d '' file_real < <(realpath -ze -- "$candidate") || return 1 IFS= read -r -d '' dir_real < <(realpath -ze -- "$DOWNLOAD_DIR") || return 1 [[ $file_real != *$'\n'* && $file_real != *$'\r'* && $file_real != *$'\t'* ]] || return 1 # Trim the slash so a download directory of "/" still leaves a usable prefix. [[ $file_real == "${dir_real%/}"/* ]] || return 1 printf '%s' "$file_real" } # yt-dlp prints the title JSON-encoded, so a newline or tab in page metadata is an # escape sequence rather than a record boundary. This is toast text, never a command. decode_title() { local decoded decoded=$(jq -r 'if type == "string" then . else empty end' <<<"$1" 2>/dev/null) || return 1 decoded=${decoded%%[[:cntrl:]]*} # keep what a person would read, drop the forgery [[ -n $decoded && $decoded != -* ]] || return 1 printf '%s' "$decoded" } title_from_file() { local name=${1##*/} name=${name%.*} name=${name//[$'\n\r\t']/} if [[ -z $name || $name == -* ]]; then printf '%s' "Video" else printf '%s' "$name" fi } # 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 --no-exec --no-exec-before-download -- "$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), and # OMARCHY_FILE and OMARCHY_TITLE (printed only after a successful move) carry the # path and the title. The title is JSON-encoded so metadata cannot forge a record, # and the file is named after it: yt-dlp strips control characters from a filename # with or without --restrict-filenames, so a record is still only ever one line. local line pct intpct last="" er nowms lastms=0 title="" filepath="" resolved 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*) resolved=$(resolve_download_file "${line#OMARCHY_FILE$'\t'}") || continue filepath=$resolved ;; OMARCHY_TITLE*) title=$(decode_title "${line#OMARCHY_TITLE$'\t'}") || title="" ;; esac done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --no-simulate \ --quiet --no-warnings --no-exec --no-exec-before-download --progress --newline \ --progress-template $'download:OMARCHY_PROG\t%(progress._percent_str)s' \ --paths "$DOWNLOAD_DIR" -o '%(title)s.%(ext)s' \ --print $'after_move:OMARCHY_FILE\t%(filepath)s' \ --print $'after_move:OMARCHY_TITLE\t%(title)j' \ -- "$url" 2>&1) osd_close # after_move only prints on a successful download+move, so a captured path == success. if [[ -n $filepath ]]; then [[ -n $title ]] || title=$(title_from_file "$filepath") ((${#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 # Best-effort: the download already succeeded, and under `set -e` a failed # toast would exit before the thumbnail cleanup below is ever scheduled. # `--` keeps mpv from parsing a leading-dash filename as an option; the path # is one discrete argument, so it never reaches a shell. omarchy-notification-send -g 󰄬 "Download complete" "$title" \ -t 10000 --image "${preview:-$filepath}" \ --exec mpv -- "$filepath" || true # The shell loads the thumbnail into memory when the toast appears and never # re-reads the file, so the preview only has to outlive that load, not the # toast. ( sleep 2 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