commit 9218a7d
uint
·
2026-03-09 22:45:10 +0000 UTC
parent 1327bb8
shrados: add REPL parados client shrados is a simple sh script which interacts with parados just by using the shell.
2 files changed,
+422,
-0
+1,
-0
1@@ -5,6 +5,7 @@ parados
2 *.o
3 *.swp
4 *.sh
5+!clients/shrados.sh
6
7 # gorados
8 clients/gorados/gorados
+421,
-0
1@@ -0,0 +1,421 @@
2+#!/bin/sh
3+#
4+# shrados
5+# A REPL client for parados
6+
7+set -u
8+
9+# Options:
10+# CACHE_LOGIN: 1 to cache auth in ~/.cache/shrados.auth, 0 to disable.
11+# VIDEO_PLAYER: player command used by "watch n".
12+# VIDEO_PLAYER_ARGS: extra args appended before "-" stdin input.
13+
14+CACHE_LOGIN=${CACHE_LOGIN:-1}
15+VIDEO_PLAYER=${VIDEO_PLAYER:-mpv}
16+VIDEO_PLAYER_ARGS=${VIDEO_PLAYER_ARGS:-}
17+PARADOS_URL=${PARADOS_URL:-http://127.0.0.1:8088}
18+PARADOS_CURL=${PARADOS_CURL:-curl}
19+
20+TAB=$(printf '\t')
21+CUR_DIR=""
22+AUTH=""
23+C_RESET=""
24+C_DIM=""
25+C_PROMPT=""
26+C_DIR=""
27+C_VID=""
28+
29+CACHE_DIR=${XDG_CACHE_HOME:-"$HOME/.cache"}
30+AUTH_FILE="$CACHE_DIR/shrados.auth"
31+
32+TMP_DIR=${TMPDIR:-/tmp}/shrados.$$
33+LIB_FILE="$TMP_DIR/library.tsv"
34+MAP_FILE="$TMP_DIR/view.tsv"
35+
36+# cleanup on exit
37+cleanup() { rm -rf "$TMP_DIR"; }
38+
39+# print err message, exit on failure
40+die() { printf '%s\n' "$*" >&2; exit 1; }
41+
42+# check if command exists
43+cmd_exists() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
44+
45+# setup subtle ANSI colors for interactive terminal output
46+setup_color()
47+{
48+ [ -t 1 ] || return 0
49+ [ -z "${NO_COLOR:-}" ] || return 0
50+
51+ C_RESET=$(printf '\033[0m')
52+ C_DIM=$(printf '\033[2m')
53+ C_PROMPT=$(printf '\033[33m')
54+ C_DIR=$(printf '\033[36m')
55+ C_VID=$(printf '\033[32m')
56+}
57+
58+# make sure arg isnt negative
59+validate_idx()
60+{
61+ case "$1" in
62+ ''|*[!0-9]*) return 1 ;;
63+ *) return 0 ;;
64+ esac
65+}
66+
67+# make sure arg is 16Byte hex
68+validate_id()
69+{
70+ case "$1" in
71+ ????????????????)
72+ case "$1" in *[!0-9a-fA-F]*) return 1 ;; *) return 0 ;; esac
73+ ;;
74+ *) return 1 ;;
75+ esac
76+}
77+
78+# check if path is video based on extension
79+is_video_path()
80+{
81+ p=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
82+ case "$p" in
83+ *.mkv|*.mp4|*.webm|*.mov|*.m4v|*.avi|*.mpg|*.mpeg|*.ts|*.m2ts|*.flv|*.wmv|*.3gp) return 0 ;;
84+ *) return 1 ;;
85+ esac
86+}
87+
88+# do a HTTP GET request, save body to file, return HTTP code
89+http_get()
90+{
91+ if [ -n "$AUTH" ]; then
92+ "$PARADOS_CURL" -sS -u "$AUTH" -o "$2" -w '%{http_code}' "${PARADOS_URL}$1"
93+ else
94+ "$PARADOS_CURL" -sS -o "$2" -w '%{http_code}' "${PARADOS_URL}$1"
95+ fi
96+}
97+
98+# parse /library JSON response, print "id<TAB>path" lines
99+parse_library_json()
100+{
101+ awk '
102+ {
103+ s = $0
104+ while (match(s, /"id":"([^"]+)","path":"([^"]*)"/)) {
105+ chunk = substr(s, RSTART, RLENGTH)
106+ id = chunk; sub(/^"id":"/, "", id); sub(/","path":"[^"]*"$/, "", id)
107+ path = chunk; sub(/^"id":"[^"]+","path":"/, "", path); sub(/"$/, "", path)
108+ gsub(/\\"/, "\"", path); gsub(/\\t/, "\t", path); gsub(/\\r/, "", path); gsub(/\\n/, "", path)
109+ printf "%s\t%s\n", id, path
110+ s = substr(s, RSTART + RLENGTH)
111+ }
112+ }
113+ ' "$1"
114+}
115+
116+# fetch /library, parse, save to LIB_FILE
117+refresh_library()
118+{
119+ tmp="$TMP_DIR/library.json"
120+ code=$(http_get "/library" "$tmp") || { printf '%s\n' "request failed: /library" >&2; return 1; }
121+ case "$code" in
122+ 200)
123+ parse_library_json "$tmp" | while IFS="$TAB" read -r id path; do
124+ validate_id "$id" || continue
125+ is_video_path "$path" || continue
126+ printf '%s\t%s\n' "$id" "$path"
127+ done | sort -f -t "$TAB" -k2,2 > "$LIB_FILE" || { printf '%s\n' "failed parsing /library response" >&2; return 1; }
128+ ;;
129+ 401) printf '%s\n' "unauthorized: use login" >&2; return 1 ;;
130+ *) printf '%s\n' "request failed: /library (HTTP $code)" >&2; return 1 ;;
131+ esac
132+ return 0
133+}
134+
135+# build view of current dir, save to MAP_FILE with lines:
136+build_view()
137+{
138+ dirs_raw="$TMP_DIR/dirs.raw"; dirs_sorted="$TMP_DIR/dirs.sorted"
139+ files_raw="$TMP_DIR/files.raw"; files_sorted="$TMP_DIR/files.sorted"
140+ : > "$dirs_raw"; : > "$files_raw"; : > "$MAP_FILE"
141+
142+ while IFS="$TAB" read -r id path; do
143+ [ -n "$id" ] || continue
144+ if [ -n "$CUR_DIR" ]; then
145+ prefix="$CUR_DIR/"
146+ case "$path" in "$prefix"*) rel=${path#"$prefix"} ;; *) continue ;; esac
147+ else
148+ rel=$path
149+ fi
150+ case "$rel" in
151+ */*) printf '%s\n' "${rel%%/*}" >> "$dirs_raw" ;;
152+ *) printf '%s\t%s\t%s\n' "$id" "$rel" "$path" >> "$files_raw" ;;
153+ esac
154+ done < "$LIB_FILE"
155+
156+ sort -fu "$dirs_raw" > "$dirs_sorted"
157+ sort -f -t "$TAB" -k2,2 "$files_raw" > "$files_sorted"
158+
159+ n=1
160+ while IFS= read -r dir; do
161+ [ -n "$dir" ] || continue
162+ if [ -n "$CUR_DIR" ]; then full="$CUR_DIR/$dir"; else full="$dir"; fi
163+ printf '%s\tD\t-\t%s\t%s\n' "$n" "$dir" "$full" >> "$MAP_FILE"
164+ n=$((n + 1))
165+ done < "$dirs_sorted"
166+
167+ while IFS="$TAB" read -r id name full; do
168+ [ -n "$id" ] || continue
169+ printf '%s\tF\t%s\t%s\t%s\n' "$n" "$id" "$name" "$full" >> "$MAP_FILE"
170+ n=$((n + 1))
171+ done < "$files_sorted"
172+}
173+
174+# given ls index, print corresponding line from MAP_FILE
175+lookup_entry()
176+{
177+ awk -F "$TAB" -v i="$1" '$1 == i { print; found=1; exit } END { if (!found) exit 1 }' "$MAP_FILE"
178+}
179+
180+# split MAP_FILE line into vars
181+split_entry()
182+{
183+ IFS="$TAB" read -r ENTRY_N ENTRY_TYP ENTRY_ID ENTRY_NAME ENTRY_FULL <<EOF2
184+$1
185+EOF2
186+}
187+
188+# print command prompt
189+print_prompt()
190+{
191+ if [ -n "$CUR_DIR" ]; then
192+ printf '%sparados>%s %s[/%s]%s ' "$C_PROMPT" "$C_RESET" "$C_DIM" "$CUR_DIR" "$C_RESET"
193+ else
194+ printf '%sparados>%s ' "$C_PROMPT" "$C_RESET"
195+ fi
196+}
197+
198+# output all commands
199+cmd_help()
200+{
201+ cat <<'EOF2'
202+Commands:
203+ help show this help table
204+ clear clear screen
205+ ping call "/ping"
206+ login prompt for username, password
207+ logout clear login (and cache if its enabled)
208+ rescan do server rescan + refresh local view
209+ ls list dirs/videos in current folder
210+ cd n enter directory by ls index
211+ cd .. go up one directory
212+ cd / go to root
213+ watch n play video at ls index using VIDEO_PLAYER
214+ pwd show current folder
215+ quit | exit leave shrados
216+EOF2
217+}
218+
219+# clear screen
220+cmd_clear() { command -v clear >/dev/null 2>&1 && clear || printf '\033[2J\033[H'; }
221+
222+# call "/ping", print response or error
223+cmd_ping()
224+{
225+ tmp="$TMP_DIR/ping.txt"
226+ code=$(http_get "/ping" "$tmp") || { printf '%s\n' "request failed: /ping" >&2; return 1; }
227+ case "$code" in
228+ 200) cat "$tmp"; printf '\n' ;;
229+ 401) printf '%s\n' "unauthorized: use login" >&2; return 1 ;;
230+ *) printf '%s\n' "request failed: /ping (HTTP $code)" >&2; return 1 ;;
231+ esac
232+}
233+
234+# list current dir based on MAP_FILE
235+cmd_ls()
236+{
237+ build_view
238+ [ -n "$CUR_DIR" ] && printf 'cwd: /%s\n' "$CUR_DIR" || printf 'cwd: /\n'
239+ [ -s "$MAP_FILE" ] || { printf '%s\n' "(empty)"; return 0; }
240+ while IFS="$TAB" read -r n typ id name full; do
241+ case "$typ" in
242+ D) printf '%3s %s[dir]%s %s/\n' "$n" "$C_DIR" "$C_RESET" "$name" ;;
243+ F) printf '%3s %s[vid]%s %s\n' "$n" "$C_VID" "$C_RESET" "$name" ;;
244+ esac
245+ done < "$MAP_FILE"
246+}
247+
248+# print current dir
249+cmd_pwd() { [ -n "$CUR_DIR" ] && printf '/%s\n' "$CUR_DIR" || printf '/\n'; }
250+
251+# change directory based on ls index, .., or /
252+cmd_cd()
253+{
254+ [ $# -eq 1 ] || { printf '%s\n' "usage: cd <n|..|/>" >&2; return 1; }
255+ case "$1" in
256+ /) CUR_DIR=""; return 0 ;;
257+ ..)
258+ [ -z "$CUR_DIR" ] && return 0
259+ case "$CUR_DIR" in */*) CUR_DIR=${CUR_DIR%/*} ;; *) CUR_DIR="" ;; esac
260+ return 0
261+ ;;
262+ esac
263+ validate_idx "$1" || { printf '%s\n' "cd expects an ls index, '..', or '/'" >&2; return 1; }
264+ build_view
265+ line=$(lookup_entry "$1") || { printf '%s\n' "invalid index: $1" >&2; return 1; }
266+ split_entry "$line"
267+ [ "$ENTRY_TYP" = "D" ] || { printf '%s\n' "index is not a directory: $1" >&2; return 1; }
268+ CUR_DIR=$ENTRY_FULL
269+}
270+
271+# given ls index, get video id, call "/stream/id", pipe to VIDEO_PLAYER, print errors
272+cmd_watch()
273+{
274+ [ $# -eq 1 ] || { printf '%s\n' "usage: watch <n>" >&2; return 1; }
275+ validate_idx "$1" || { printf '%s\n' "watch expects an ls index" >&2; return 1; }
276+ build_view
277+ line=$(lookup_entry "$1") || { printf '%s\n' "invalid index: $1" >&2; return 1; }
278+ split_entry "$line"
279+ [ "$ENTRY_TYP" = "F" ] || { printf '%s\n' "index is not a video: $1" >&2; return 1; }
280+ validate_id "$ENTRY_ID" || { printf '%s\n' "invalid id in index: $1" >&2; return 1; }
281+ cmd_exists "$VIDEO_PLAYER"
282+ printf 'watching: %s\n' "$ENTRY_NAME"
283+ # shellcheck disable=SC2086
284+ set -- "$VIDEO_PLAYER" $VIDEO_PLAYER_ARGS -
285+ if [ -n "$AUTH" ]; then
286+ "$PARADOS_CURL" -sS -u "$AUTH" "${PARADOS_URL}/stream/$ENTRY_ID" | "$@"
287+ else
288+ "$PARADOS_CURL" -sS "${PARADOS_URL}/stream/$ENTRY_ID" | "$@"
289+ fi
290+}
291+
292+# call "/rescan", print response or error
293+cmd_rescan()
294+{
295+ tmp="$TMP_DIR/rescan.txt"
296+ code=$(http_get "/rescan" "$tmp") || { printf '%s\n' "request failed: /rescan" >&2; return 1; }
297+ case "$code" in
298+ 200) printf '%s\n' "rescan: ok"; refresh_library ;;
299+ 401) printf '%s\n' "unauthorized: use login" >&2; return 1 ;;
300+ 403) printf '%s\n' "rescan forbidden: auth may be disabled server-side" >&2; return 1 ;;
301+ *) printf '%s\n' "request failed: /rescan (HTTP $code)" >&2; return 1 ;;
302+ esac
303+}
304+
305+# prompt for password without echo, return input
306+prompt_password()
307+{
308+ printf 'pass: ' >&2
309+ if command -v stty >/dev/null 2>&1; then
310+ stty_state=$(stty -g 2>/dev/null || true)
311+ stty -echo 2>/dev/null || true
312+ IFS= read -r pass || return 1
313+ [ -n "$stty_state" ] && stty "$stty_state" 2>/dev/null || stty echo 2>/dev/null || true
314+ printf '\n' >&2
315+ else
316+ IFS= read -r pass || return 1
317+ fi
318+ printf '%s' "$pass"
319+}
320+
321+# save authentication cache if enabled and AUTH set, with permissions 600
322+save_auth_cache()
323+{
324+ [ "$CACHE_LOGIN" = "1" ] || return 0
325+ [ -n "$AUTH" ] || return 0
326+ mkdir -p "$CACHE_DIR" || return 1
327+ (umask 077 && printf '%s' "$AUTH" > "$AUTH_FILE") || return 1
328+}
329+
330+# load authentication cache if enabled and cache file exists, set AUTH
331+load_auth_cache()
332+{
333+ [ "$CACHE_LOGIN" = "1" ] || return 0
334+ [ -f "$AUTH_FILE" ] || return 0
335+ AUTH=$(tr -d '\r\n' < "$AUTH_FILE")
336+}
337+
338+# delete authentication cache file if enabled
339+clear_auth_cache()
340+{
341+ [ "$CACHE_LOGIN" = "1" ] || return 0
342+ rm -f "$AUTH_FILE"
343+}
344+
345+# prompt for username and password, set AUTH
346+cmd_login()
347+{
348+ printf 'user: '
349+ IFS= read -r user || return 1
350+ [ -n "$user" ] || { printf '%s\n' "empty username" >&2; return 1; }
351+ pass=$(prompt_password) || return 1
352+ AUTH="$user:$pass"
353+ if ! refresh_library; then AUTH=""; return 1; fi
354+ save_auth_cache || printf '%s\n' "warning: failed to write auth cache" >&2
355+ printf '%s\n' "login: ok"
356+}
357+
358+# clear AUTH, delete auth cache, print result
359+cmd_logout() { AUTH=""; clear_auth_cache; printf '%s\n' "logout: ok"; }
360+
361+# dispatch command based on first arg, pass rest as args.
362+# return 99 to signal quit, 0 for success, 1 for error
363+dispatch()
364+{
365+ cmd=$1
366+ shift || true
367+ case "$cmd" in
368+ help) cmd_help ;;
369+ clear) cmd_clear ;;
370+ ping) cmd_ping ;;
371+ login) cmd_login ;;
372+ logout) cmd_logout ;;
373+ rescan) cmd_rescan ;;
374+ ls) cmd_ls ;;
375+ cd) cmd_cd "$@" ;;
376+ watch) cmd_watch "$@" ;;
377+ pwd) cmd_pwd ;;
378+ quit|exit) return 99 ;;
379+ '') return 0 ;;
380+ *) printf '%s\n' "unknown command: $cmd (use: help)" >&2; return 1 ;;
381+ esac
382+}
383+
384+main()
385+{
386+ trap cleanup EXIT INT TERM
387+ cmd_exists "$PARADOS_CURL"
388+ mkdir -p "$TMP_DIR" || die "failed creating temp dir: $TMP_DIR"
389+ : > "$LIB_FILE"; : > "$MAP_FILE"
390+
391+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then cmd_help; exit 0; fi
392+ if [ "${1:-}" = "--url" ]; then
393+ [ $# -ge 2 ] || die "missing value for --url"
394+ PARADOS_URL=$2
395+ shift 2
396+ fi
397+ [ $# -eq 0 ] || die "usage: shrados.sh [--url URL]"
398+
399+ load_auth_cache
400+ if ! refresh_library; then printf '%s\n' "not logged in. use: login" >&2; fi
401+
402+ printf '%s\n' "shrados: minimal video repl"
403+ printf 'server: %s\n' "$PARADOS_URL"
404+ printf 'player: %s %s\n' "$VIDEO_PLAYER" "$VIDEO_PLAYER_ARGS"
405+ cmd_help
406+
407+ setup_color
408+
409+ while :; do
410+ print_prompt
411+ if ! IFS= read -r line; then printf '\n'; break; fi
412+ [ -n "$line" ] || continue
413+ set -- $line
414+ cmd=${1:-}
415+ shift || true
416+ dispatch "$cmd" "$@"
417+ rc=$?
418+ [ "$rc" -eq 99 ] && break
419+ done
420+}
421+
422+main "$@"