Restore the browser, gaming, development, and Windows VM menus
This commit is contained in:
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
|
||||
# blob:summary=Native messaging host: download the URL sent by the yt-dlp Chromium extension
|
||||
# blob:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_PATH="${BASH_SOURCE[0]}"
|
||||
|
||||
# The browser launches us without Blob's environment, so locate the repo from
|
||||
# our own path when BLOB_PATH isn't already set. Export it — blob-shell (used
|
||||
# by blob-osd) needs it to find the running shell, and silently no-ops without it.
|
||||
export BLOB_PATH="${BLOB_PATH:-$(cd -- "$(dirname -- "$SCRIPT_PATH")/.." && pwd)}"
|
||||
|
||||
# Make sure the Blob bin and yt-dlp are reachable when launched by the browser.
|
||||
export PATH="$BLOB_PATH/bin:/usr/local/bin:/usr/bin:$PATH"
|
||||
|
||||
DOWNLOAD_DIR="${BLOB_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() {
|
||||
blob-osd -i -p "$1" -d 8000 >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
osd_close() {
|
||||
blob-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
|
||||
blob-notify-send -u critical -g "No video found for download" "$url"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
osd_progress 0
|
||||
|
||||
# Stream the download: BLOB_PROG carries the percent (drives the OSD), and
|
||||
# BLOB_FILE and BLOB_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
|
||||
BLOB_PROG*)
|
||||
pct=${line#BLOB_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"
|
||||
;;
|
||||
BLOB_FILE*)
|
||||
resolved=$(resolve_download_file "${line#BLOB_FILE$'\t'}") || continue
|
||||
filepath=$resolved
|
||||
;;
|
||||
BLOB_TITLE*)
|
||||
title=$(decode_title "${line#BLOB_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:BLOB_PROG\t%(progress._percent_str)s' \
|
||||
--paths "$DOWNLOAD_DIR" -o '%(title)s.%(ext)s' \
|
||||
--print $'after_move:BLOB_FILE\t%(filepath)s' \
|
||||
--print $'after_move:BLOB_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.
|
||||
blob-notify-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
|
||||
blob-notify-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
|
||||
Reference in New Issue
Block a user