screen-toolkit : full screen utility (#221)
* screen-toolkit: add Noctalia v5 plugin * screen-toolkit: credit original plugin * screen-toolkit: fix recording, OCR capture, and result panel UX * screen-toolkit: rename record tool labels to Record / Rec FS * screen-toolkit: update plugin thumbnail * screen-toolkit: fix README validation (backtick deps, result panel toggle) * screen-toolkit: fix OCR results and dependencies * screen-toolkit: backtick recording and annotator deps in README --------- Co-authored-by: Ahmed5Emad <ahmed5emad@users.noreply.github.com>
This commit is contained in:
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# capture.sh <action> [args...]
|
||||
#
|
||||
# Actions:
|
||||
# annotate-window — capture the focused Hyprland window with grim
|
||||
# output: /tmp/screen-toolkit-annotate.png
|
||||
# stdout: "X,Y WxH" geometry string
|
||||
# palette <geometry> — extract 8 dominant hex colours from a captured region
|
||||
# stdout: one "#RRGGBB" per line
|
||||
# qr <geometry> — capture a region and decode any QR / barcode found
|
||||
# stdout: decoded text
|
||||
#
|
||||
# Exit codes:
|
||||
# 1 — missing / invalid arguments
|
||||
# 2 — capture or decode failed
|
||||
# 3 — missing dependency (hyprctl, jq, grim, magick, zbarimg)
|
||||
#
|
||||
# Used by: service.luau
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ACTION="${1:-}"
|
||||
GRIM_CURSOR_ARGS=()
|
||||
[ "${SCREEN_TOOLKIT_CAPTURE_CURSOR:-0}" = "1" ] && GRIM_CURSOR_ARGS+=(-c)
|
||||
|
||||
_require() {
|
||||
command -v "$1" >/dev/null 2>&1 \
|
||||
|| { echo "ERROR: missing dependency: $1" >&2; exit 3; }
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
|
||||
annotate-window)
|
||||
_require slurp
|
||||
_require hyprctl
|
||||
_require jq
|
||||
_require grim
|
||||
# Crosshair: click the window you want to annotate. slurp blocks until the
|
||||
# click, so there is no timeout and its overlay is never captured.
|
||||
PT=$(slurp -p 2>/dev/null) || { echo "ERROR: cancelled" >&2; exit 1; }
|
||||
X=$(printf '%s' "$PT" | awk -F'[, ]+' '{print int($1)}')
|
||||
Y=$(printf '%s' "$PT" | awk -F'[, ]+' '{print int($2)}')
|
||||
# Snap to the smallest mapped window containing the clicked point.
|
||||
WIN=$(hyprctl clients -j 2>/dev/null | jq -c --argjson x "$X" --argjson y "$Y" '
|
||||
[ .[] | select(.mapped == true)
|
||||
| { at: (.at // [0, 0]), size: (.size // [0, 0]) }
|
||||
| select(.at[0] <= $x and (.at[0] + .size[0]) >= $x
|
||||
and .at[1] <= $y and (.at[1] + .size[1]) >= $y) ]
|
||||
| sort_by(.size[0] * .size[1]) | first' 2>/dev/null)
|
||||
[ -n "$WIN" ] && [ "$WIN" != "null" ] \
|
||||
|| { echo "ERROR: no window at that point" >&2; exit 2; }
|
||||
GEOM=$(printf '%s' "$WIN" | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' 2>/dev/null)
|
||||
[ -n "$GEOM" ] \
|
||||
|| { echo "ERROR: could not parse window geometry" >&2; exit 2; }
|
||||
# grim excludes the cursor by default; moving it would make the picker
|
||||
# unreliable and is compositor-specific.
|
||||
sleep 0.15
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "$GEOM" /tmp/screen-toolkit-annotate.png 2>/dev/null \
|
||||
|| { echo "ERROR: grim capture failed" >&2; exit 2; }
|
||||
printf '%s\n' "$GEOM"
|
||||
;;
|
||||
|
||||
palette)
|
||||
GEOMETRY="${2:-}"
|
||||
[ -n "$GEOMETRY" ] || { echo "ERROR: palette: missing <geometry>" >&2; exit 1; }
|
||||
_require grim
|
||||
_require magick
|
||||
|
||||
FILE="/tmp/screen-toolkit-palette.png"
|
||||
|
||||
sleep 0.15
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "$GEOMETRY" "$FILE" 2>/dev/null \
|
||||
|| { echo "ERROR: palette: grim capture failed" >&2; exit 2; }
|
||||
|
||||
magick "$FILE" -alpha off +dither -colors 8 -unique-colors txt:- 2>/dev/null \
|
||||
| grep -v '^#' \
|
||||
| grep -oP '#[0-9a-fA-F]{6}' \
|
||||
| head -8
|
||||
;;
|
||||
|
||||
qr)
|
||||
GEOMETRY="${2:-}"
|
||||
[ -n "$GEOMETRY" ] || { echo "ERROR: qr: missing <geometry>" >&2; exit 1; }
|
||||
_require grim
|
||||
_require zbarimg
|
||||
|
||||
sleep 0.15
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "$GEOMETRY" /tmp/screen-toolkit-qr.png 2>/dev/null \
|
||||
|| { echo "ERROR: qr: grim capture failed" >&2; exit 2; }
|
||||
|
||||
zbarimg -q --raw /tmp/screen-toolkit-qr.png 2>/dev/null
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "ERROR: unknown action '${ACTION}'. Expected: annotate-window | palette | qr" >&2
|
||||
exit 1
|
||||
;;
|
||||
|
||||
esac
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# color-picker.sh <output-png>
|
||||
# Picks a color from screen, outputs "R G B" to stdout.
|
||||
# Uses hyprpicker (preferred) or falls back to slurp+grim.
|
||||
FILE="$1"
|
||||
[ -z "$FILE" ] && exit 1
|
||||
GRIM_CURSOR_ARGS=()
|
||||
[ "${SCREEN_TOOLKIT_CAPTURE_CURSOR:-0}" = "1" ] && GRIM_CURSOR_ARGS+=(-c)
|
||||
# ── hyprpicker ────────────────────────────────────────────────────────────────
|
||||
if command -v hyprpicker >/dev/null 2>&1; then
|
||||
HYPRCTL=$(command -v hyprctl 2>/dev/null || true)
|
||||
if [ -n "$HYPRCTL" ]; then
|
||||
PRE_POS=$(hyprctl cursorpos 2>/dev/null)
|
||||
fi
|
||||
if hyprpicker --help 2>&1 | grep -q '\-\-radius'; then
|
||||
HEX=$(hyprpicker --no-fancy --format=hex --radius=65 2>/dev/null) || exit 1
|
||||
else
|
||||
HEX=$(hyprpicker --no-fancy --format=hex 2>/dev/null) || exit 1
|
||||
fi
|
||||
HEX="${HEX#\#}"
|
||||
[ ${#HEX} -eq 6 ] || exit 1
|
||||
R=$((16#${HEX:0:2}))
|
||||
G=$((16#${HEX:2:2}))
|
||||
B=$((16#${HEX:4:2}))
|
||||
if [ -n "$HYPRCTL" ]; then
|
||||
sleep 0.08
|
||||
POST_POS=$(hyprctl cursorpos 2>/dev/null)
|
||||
fi
|
||||
CAPTURED=0
|
||||
if [ -n "$HYPRCTL" ] && command -v grim >/dev/null 2>&1; then
|
||||
# If cursor moved, we have the real pick position
|
||||
# If it didn't move, hyprland restored it — coordinates are useless, skip capture
|
||||
if [ "$PRE_POS" != "$POST_POS" ] && [ -n "$POST_POS" ]; then
|
||||
X=$(echo "$POST_POS" | awk -F'[, ]+' '{print int($1)}')
|
||||
Y=$(echo "$POST_POS" | awk -F'[, ]+' '{print int($2)}')
|
||||
if [ -n "$X" ] && [ -n "$Y" ] && [ "$X" -ge 0 ] && [ "$Y" -ge 0 ] 2>/dev/null; then
|
||||
# Capture 21x21 area centered on picked pixel for more context
|
||||
GX=$((X > 10 ? X - 10 : 0))
|
||||
GY=$((Y > 10 ? Y - 10 : 0))
|
||||
sleep 0.15
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "${GX},${GY} 21x21" "$FILE" 2>/dev/null && CAPTURED=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# Fallback: solid swatch
|
||||
if [ "$CAPTURED" -eq 0 ] && command -v magick >/dev/null 2>&1; then
|
||||
magick -size 21x21 "xc:rgb($R,$G,$B)" "$FILE" 2>/dev/null
|
||||
fi
|
||||
printf '%d %d %d\n' "$R" "$G" "$B"
|
||||
exit 0
|
||||
fi
|
||||
# ── fallback: slurp + grim + magick ──────────────────────────────────────────
|
||||
for dep in slurp grim magick; do
|
||||
command -v "$dep" >/dev/null 2>&1 || exit 1
|
||||
done
|
||||
COORDS=$(slurp -p 2>/dev/null) || exit 1
|
||||
X=${COORDS%%,*}; REST=${COORDS#*,}; Y=${REST%% *}
|
||||
GX=$((X > 10 ? X - 10 : 0)); GY=$((Y > 10 ? Y - 10 : 0))
|
||||
sleep 0.15
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "${GX},${GY} 21x21" "$FILE" 2>/dev/null || exit 1
|
||||
magick "$FILE" -alpha off \
|
||||
-format '%[fx:int(255*u.p{10,10}.r)] %[fx:int(255*u.p{10,10}.g)] %[fx:int(255*u.p{10,10}.b)]' \
|
||||
info:- 2>/dev/null
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Args: $1=gx $2=gy $3=gw $4=gh
|
||||
# Captures a region and opens it in Google Lens via a temporary upload.
|
||||
# Exit 1 — missing dependency (dep name written to stdout)
|
||||
# Exit 2 — capture failed
|
||||
# Exit 3 — upload failed
|
||||
|
||||
GX="$1"; GY="$2"; GW="$3"; GH="$4"
|
||||
FILE="/tmp/screen-toolkit-lens.png"
|
||||
GRIM_CURSOR_ARGS=()
|
||||
[ "${SCREEN_TOOLKIT_CAPTURE_CURSOR:-0}" = "1" ] && GRIM_CURSOR_ARGS+=(-c)
|
||||
|
||||
for dep in grim curl jq xdg-open; do
|
||||
command -v "$dep" >/dev/null 2>&1 || { echo "$dep"; exit 1; }
|
||||
done
|
||||
|
||||
sleep 0.15
|
||||
|
||||
# Capture the requested region. Without this the upload would send whatever
|
||||
# stale image happens to be left at $FILE (or nothing at all).
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "${GX},${GY} ${GW}x${GH}" "$FILE" 2>/dev/null \
|
||||
|| { echo "ERROR: grim capture failed" >&2; exit 2; }
|
||||
|
||||
RESP=$(curl -sS -f -A 'Mozilla/5.0' --connect-timeout 20 --max-time 60 \
|
||||
-F "files[]=@$FILE" 'https://uguu.se/upload' 2>/dev/null) || \
|
||||
RESP=$(curl -sS -A 'Mozilla/5.0' --connect-timeout 20 --max-time 60 \
|
||||
-F "files[]=@$FILE" 'https://uguu.se/upload.php' 2>/dev/null)
|
||||
|
||||
rm -f "$FILE"
|
||||
|
||||
URL=$(printf '%s' "$RESP" | jq -r '.files[0].url // empty' 2>/dev/null)
|
||||
if [ -n "$URL" ] && [[ "$URL" == http* ]]; then
|
||||
xdg-open "https://lens.google.com/uploadbyurl?url=$URL" >/dev/null 2>&1 &
|
||||
else
|
||||
exit 3
|
||||
fi
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# Args: $1=gx $2=gy $3=gw $4=gh $5=lang $6=upscale_flag $7=psm $8=output_path
|
||||
#
|
||||
# Exit codes:
|
||||
# 1 — missing dependency (dep name written to stdout)
|
||||
# 2 — bad/missing args
|
||||
# 3 — capture failed
|
||||
# 4 — image processing failed
|
||||
# Exit 0 with empty stdout = no text found (service handles this case)
|
||||
|
||||
GX="$1"; GY="$2"; GW="$3"; GH="$4"
|
||||
RAW_LANG="${5:-eng}"
|
||||
UPSCALE="$6"
|
||||
USER_PSM="${7:-3}"
|
||||
FILE="${8:-/tmp/screen-toolkit-ocr.png}"
|
||||
TMP_BASE="/tmp/screen-toolkit-ocr-work-$$"
|
||||
TMP="${TMP_BASE}.pnm"
|
||||
TMP_NOISE="${TMP_BASE}-nr.pnm"
|
||||
GRIM_CURSOR_ARGS=()
|
||||
[ "${SCREEN_TOOLKIT_CAPTURE_CURSOR:-0}" = "1" ] && GRIM_CURSOR_ARGS+=(-c)
|
||||
|
||||
cleanup() { rm -f "$TMP" "$TMP_NOISE"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
for dep in grim magick tesseract; do
|
||||
command -v "$dep" >/dev/null 2>&1 || { echo "$dep"; exit 1; }
|
||||
done
|
||||
|
||||
[ -z "$GX" ] || [ -z "$GY" ] || [ -z "$GW" ] || [ -z "$GH" ] && exit 2
|
||||
|
||||
LANG=$(echo "$RAW_LANG" | tr '+' '\n' \
|
||||
| grep -v '^osd$' \
|
||||
| grep -v '^$' \
|
||||
| tr '\n' '+' \
|
||||
| sed 's/+$//')
|
||||
[ -z "$LANG" ] && LANG="eng"
|
||||
|
||||
AVAILABLE=$(tesseract --list-langs 2>/dev/null | tail -n +2)
|
||||
VALID_LANGS=""
|
||||
IFS='+' read -ra LANG_PARTS <<< "$LANG"
|
||||
for l in "${LANG_PARTS[@]}"; do
|
||||
if echo "$AVAILABLE" | grep -qx "$l"; then
|
||||
VALID_LANGS="${VALID_LANGS}+${l}"
|
||||
fi
|
||||
done
|
||||
LANG="${VALID_LANGS#+}"
|
||||
[ -z "$LANG" ] && LANG="eng"
|
||||
|
||||
sleep 0.15
|
||||
|
||||
# Capture the requested region. Without this the script would keep re-reading
|
||||
# whatever stale image happens to be left at $FILE (stale OCR results).
|
||||
grim "${GRIM_CURSOR_ARGS[@]}" -g "${GX},${GY} ${GW}x${GH}" "$FILE" 2>/dev/null \
|
||||
|| { echo "ERROR: grim capture failed" >&2; exit 3; }
|
||||
|
||||
if [ -z "$UPSCALE" ] && [ "$GW" -lt 200 ] 2>/dev/null; then
|
||||
SCALE=$(awk "BEGIN{printf \"%.0f\", 300 / $GW}")
|
||||
UPSCALE="-scale ${SCALE}00%"
|
||||
fi
|
||||
|
||||
magick "$FILE" $UPSCALE \
|
||||
-colorspace Gray \
|
||||
-normalize \
|
||||
-contrast-stretch 2%x1% \
|
||||
-sharpen 0x1.5 \
|
||||
+repage \
|
||||
"$TMP" 2>/dev/null || exit 4
|
||||
|
||||
MEAN=$(magick "$TMP" -format '%[fx:mean]' info: 2>/dev/null)
|
||||
if awk "BEGIN{exit !($MEAN < 0.4)}"; then
|
||||
magick "$TMP" -negate "$TMP" 2>/dev/null
|
||||
fi
|
||||
magick "$TMP" -median 1 "$TMP_NOISE" 2>/dev/null
|
||||
|
||||
run_ocr() { tesseract "$1" stdout -l "$LANG" --psm "$2" --oem 1 2>/dev/null; }
|
||||
count_chars() { printf '%s' "$1" | tr -d '[:space:]' | wc -c; }
|
||||
|
||||
TEXT=$(run_ocr "$TMP" "$USER_PSM")
|
||||
BEST_LEN=$(count_chars "$TEXT")
|
||||
BEST_TEXT="$TEXT"
|
||||
|
||||
if [ "$BEST_LEN" -lt 4 ] || [ "$USER_PSM" -ne 6 ]; then
|
||||
TEXT2=$(run_ocr "$TMP_NOISE" 6)
|
||||
LEN2=$(count_chars "$TEXT2")
|
||||
[ "$LEN2" -gt "$BEST_LEN" ] && { BEST_LEN=$LEN2; BEST_TEXT="$TEXT2"; }
|
||||
fi
|
||||
if [ "$BEST_LEN" -lt 4 ]; then
|
||||
TEXT3=$(run_ocr "$TMP_NOISE" 4)
|
||||
LEN3=$(count_chars "$TEXT3")
|
||||
[ "$LEN3" -gt "$BEST_LEN" ] && { BEST_LEN=$LEN3; BEST_TEXT="$TEXT3"; }
|
||||
fi
|
||||
if [ "$BEST_LEN" -lt 4 ]; then
|
||||
TEXT4=$(magick "$TMP" -threshold 85% stdout 2>/dev/null \
|
||||
| tesseract - stdout -l "$LANG" --psm 11 --oem 1 2>/dev/null)
|
||||
LEN4=$(count_chars "$TEXT4")
|
||||
[ "$LEN4" -gt "$BEST_LEN" ] && BEST_TEXT="$TEXT4"
|
||||
fi
|
||||
|
||||
printf '%s' "$BEST_TEXT"
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# record.sh <action> [args...]
|
||||
#
|
||||
# Actions:
|
||||
# thumb <src> — extract mid-frame thumbnail → /tmp/screen-toolkit-record-thumb.png
|
||||
# convert-mp4 <input> <output> — finalize MP4: stream-copy (fast path)
|
||||
# convert-mp4 <input> <output> --recode — finalize MP4: re-encode audio to AAC 128k + faststart
|
||||
# convert-gif <input> <output> [maxsec] — convert MP4 → palette-optimized GIF at 15 fps
|
||||
# convert-mov <input> <output> — remux MP4 → MOV (stream copy)
|
||||
# stop <recorder-bin> — send SIGINT to the named recorder process
|
||||
#
|
||||
# Exit codes:
|
||||
# 1 — missing / invalid arguments
|
||||
# 2 — input file not found
|
||||
# 3 — missing dependency (ffmpeg, ffprobe, pkill)
|
||||
# 4 — conversion or process command failed
|
||||
#
|
||||
# Used by: service.luau
|
||||
set -euo pipefail
|
||||
ACTION="${1:-}"
|
||||
THUMB_OUT="/tmp/screen-toolkit-record-thumb.png"
|
||||
PALETTE="/tmp/screen-toolkit-record-palette.png"
|
||||
_require() {
|
||||
command -v "$1" >/dev/null 2>&1 \
|
||||
|| { echo "ERROR: missing dependency: $1" >&2; exit 3; }
|
||||
}
|
||||
_thumb() {
|
||||
local src="$1"
|
||||
_require ffprobe
|
||||
_require ffmpeg
|
||||
local dur
|
||||
dur=$(ffprobe -v error \
|
||||
-show_entries format=duration \
|
||||
-of default=noprint_wrappers=1:nokey=1 \
|
||||
"$src" 2>/dev/null) || dur=""
|
||||
[[ -z "$dur" || "$dur" == "N/A" ]] && dur=1
|
||||
local mid
|
||||
mid=$(echo "$dur / 2" | bc -l 2>/dev/null) || mid="0.5"
|
||||
ffmpeg -y -ss "$mid" -i "$src" -frames:v 1 "$THUMB_OUT" 2>/dev/null
|
||||
}
|
||||
case "$ACTION" in
|
||||
thumb)
|
||||
SRC="${2:-}"
|
||||
[ -n "$SRC" ] || { echo "ERROR: thumb: missing <src>" >&2; exit 1; }
|
||||
[ -f "$SRC" ] || { echo "ERROR: thumb: file not found: $SRC" >&2; exit 2; }
|
||||
_thumb "$SRC"
|
||||
;;
|
||||
convert-mp4)
|
||||
INPUT="${2:-}"
|
||||
OUTPUT="${3:-}"
|
||||
RECODE="${4:-}"
|
||||
[ -n "$INPUT" ] || { echo "ERROR: convert-mp4: missing <input>" >&2; exit 1; }
|
||||
[ -n "$OUTPUT" ] || { echo "ERROR: convert-mp4: missing <output>" >&2; exit 1; }
|
||||
[ -f "$INPUT" ] || { echo "ERROR: convert-mp4: file not found: $INPUT" >&2; exit 2; }
|
||||
_require ffmpeg
|
||||
if [ "$RECODE" = "--recode" ]; then
|
||||
ffmpeg -y -i "$INPUT" \
|
||||
-c:v copy -c:a aac -b:a 128k -movflags +faststart \
|
||||
"$OUTPUT" 2>/dev/null \
|
||||
|| { echo "ERROR: convert-mp4: ffmpeg recode failed" >&2; exit 4; }
|
||||
rm -f "$INPUT"
|
||||
else
|
||||
mv "$INPUT" "$OUTPUT" \
|
||||
|| { echo "ERROR: convert-mp4: mv failed" >&2; exit 4; }
|
||||
fi
|
||||
_thumb "$OUTPUT"
|
||||
;;
|
||||
convert-gif)
|
||||
INPUT="${2:-}"
|
||||
OUTPUT="${3:-}"
|
||||
MAXSEC="${4:-}"
|
||||
[ -n "$INPUT" ] || { echo "ERROR: convert-gif: missing <input>" >&2; exit 1; }
|
||||
[ -n "$OUTPUT" ] || { echo "ERROR: convert-gif: missing <output>" >&2; exit 1; }
|
||||
[ -f "$INPUT" ] || { echo "ERROR: convert-gif: file not found: $INPUT" >&2; exit 2; }
|
||||
_require ffmpeg
|
||||
DURATION=""
|
||||
if [ -n "$MAXSEC" ] && [ "$MAXSEC" -gt 0 ] 2>/dev/null; then
|
||||
DURATION="-t $MAXSEC"
|
||||
fi
|
||||
ffmpeg -y $DURATION -i "$INPUT" \
|
||||
-vf 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos,palettegen' \
|
||||
"$PALETTE" 2>/dev/null \
|
||||
|| { echo "ERROR: convert-gif: palettegen pass failed" >&2; exit 4; }
|
||||
ffmpeg -y $DURATION -i "$INPUT" -i "$PALETTE" \
|
||||
-lavfi 'fps=15,scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos[x];[x][1:v]paletteuse' \
|
||||
"$OUTPUT" 2>/dev/null \
|
||||
|| { echo "ERROR: convert-gif: paletteuse pass failed" >&2; exit 4; }
|
||||
rm -f "$PALETTE" "$INPUT"
|
||||
_thumb "$OUTPUT"
|
||||
;;
|
||||
convert-mov)
|
||||
INPUT="${2:-}"
|
||||
OUTPUT="${3:-}"
|
||||
[ -n "$INPUT" ] || { echo "ERROR: convert-mov: missing <input>" >&2; exit 1; }
|
||||
[ -n "$OUTPUT" ] || { echo "ERROR: convert-mov: missing <output>" >&2; exit 1; }
|
||||
[ -f "$INPUT" ] || { echo "ERROR: convert-mov: file not found: $INPUT" >&2; exit 2; }
|
||||
_require ffmpeg
|
||||
ffmpeg -y -i "$INPUT" -c copy -movflags +faststart "$OUTPUT" 2>/dev/null \
|
||||
|| { echo "ERROR: convert-mov: ffmpeg failed" >&2; exit 4; }
|
||||
rm -f "$INPUT"
|
||||
_thumb "$OUTPUT"
|
||||
;;
|
||||
stop)
|
||||
BIN="${2:-}"
|
||||
[ -n "$BIN" ] || { echo "ERROR: stop: missing <recorder-bin>" >&2; exit 1; }
|
||||
_require pkill
|
||||
pkill -INT "$BIN" 2>/dev/null || true
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown action '${ACTION}'. Expected: thumb | convert-mp4 | convert-gif | convert-mov | stop" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# share-upload.sh <file> [api_key] [expiry]
|
||||
# api_key: X02 API key — if empty, falls back to uguu.se (anonymous, 3h, 128MB max)
|
||||
# expiry: 1h | 1d | 7d | 30d | permanent (X02 only, default: 7d)
|
||||
# Prints URL to stdout on success, exits non-zero on failure
|
||||
# Exit codes:
|
||||
# 1 — invalid arguments (no file given)
|
||||
# 2 — file not found
|
||||
# 3 — missing dependency
|
||||
# 4 — upload request failed
|
||||
# 5 — invalid or empty response
|
||||
# 6 — file too large
|
||||
# Used by: service.luau
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FILE="${1:-}"
|
||||
API_KEY="${2:-}"
|
||||
EXPIRY="${3:-7d}"
|
||||
|
||||
UGUU_MAX_BYTES=$((128 * 1024 * 1024)) # 128 MB
|
||||
|
||||
[ -n "$FILE" ] || { echo "ERROR: no file given" >&2; exit 1; }
|
||||
[ -f "$FILE" ] || { echo "ERROR: file not found: $FILE" >&2; exit 2; }
|
||||
|
||||
command -v curl >/dev/null 2>&1 || { echo "ERROR: missing dependency: curl" >&2; exit 3; }
|
||||
|
||||
# ── X02 (authenticated) ───────────────────────────────────────────────────────
|
||||
if [ -n "$API_KEY" ]; then
|
||||
EXPIRY_FLAG=()
|
||||
if [ "$EXPIRY" != "permanent" ] && [ -n "$EXPIRY" ]; then
|
||||
EXPIRY_FLAG=(-F "expiry=${EXPIRY}")
|
||||
fi
|
||||
|
||||
URL=$(curl -sS -f \
|
||||
-X POST "https://up.x02.me/api/upload" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-F "file=@${FILE}" \
|
||||
"${EXPIRY_FLAG[@]}" \
|
||||
--connect-timeout 20 \
|
||||
--max-time 120 \
|
||||
2>/dev/null) \
|
||||
|| { echo "ERROR: X02 upload request failed" >&2; exit 4; }
|
||||
|
||||
if [ -n "$URL" ] && [[ "$URL" == http* ]]; then
|
||||
printf '%s\n' "$URL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ERROR: X02: unexpected response: $URL" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# ── uguu.se (anonymous fallback) ──────────────────────────────────────────────
|
||||
command -v jq >/dev/null 2>&1 || { echo "ERROR: missing dependency: jq" >&2; exit 3; }
|
||||
|
||||
FILE_SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE" 2>/dev/null || echo 0)
|
||||
if [ "$FILE_SIZE" -gt "$UGUU_MAX_BYTES" ]; then
|
||||
echo "ERROR: file too large for anonymous upload (128MB max). Add an X02 API key for larger files." >&2
|
||||
exit 6
|
||||
fi
|
||||
|
||||
RESP=$(curl -sS -f -A 'Mozilla/5.0' \
|
||||
--connect-timeout 20 --max-time 60 \
|
||||
-F "files[]=@${FILE}" \
|
||||
'https://uguu.se/upload' 2>/dev/null) \
|
||||
|| RESP=$(curl -sS -A 'Mozilla/5.0' \
|
||||
--connect-timeout 20 --max-time 60 \
|
||||
-F "files[]=@${FILE}" \
|
||||
'https://uguu.se/upload.php' 2>/dev/null) \
|
||||
|| { echo "ERROR: uguu.se upload request failed" >&2; exit 4; }
|
||||
|
||||
URL=$(printf '%s' "$RESP" | jq -r '.files[0].url // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$URL" ] && [[ "$URL" == http* ]]; then
|
||||
printf '%s\n' "$URL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ERROR: uguu.se: no valid URL in response" >&2
|
||||
exit 5
|
||||
Reference in New Issue
Block a user