commit 26018fe

uint  ·  2026-03-12 16:41:04 +0000 UTC
parent 1b7b7b2
add back shrados

i dont actually know how it disappeared
1 files changed,  +433, -0
+433, -0
  1@@ -0,0 +1,433 @@
  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+	LC_ALL=C 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+  url URL              set server URL
215+  pwd                  show current folder
216+  quit | exit          leave shrados
217+EOF2
218+}
219+
220+# clear screen
221+cmd_clear() { command -v clear >/dev/null 2>&1 && clear || printf '\033[2J\033[H'; }
222+
223+# call "/ping", print response or error
224+cmd_ping()
225+{
226+	tmp="$TMP_DIR/ping.txt"
227+	code=$(http_get "/ping" "$tmp") || { printf '%s\n' "request failed: /ping" >&2; return 1; }
228+	case "$code" in
229+		200) cat "$tmp"; printf '\n' ;;
230+		401) printf '%s\n' "unauthorized: use login" >&2; return 1 ;;
231+		*) printf '%s\n' "request failed: /ping (HTTP $code)" >&2; return 1 ;;
232+	esac
233+}
234+
235+# list current dir based on MAP_FILE
236+cmd_ls()
237+{
238+	build_view
239+	[ -n "$CUR_DIR" ] && printf 'cwd: /%s\n' "$CUR_DIR" || printf 'cwd: /\n'
240+	[ -s "$MAP_FILE" ] || { printf '%s\n' "(empty)"; return 0; }
241+	while IFS="$TAB" read -r n typ id name full; do
242+		case "$typ" in
243+			D) printf '%3s  %s[dir]%s %s/\n' "$n" "$C_DIR" "$C_RESET" "$name" ;;
244+			F) printf '%3s  %s[vid]%s %s\n' "$n" "$C_VID" "$C_RESET" "$name" ;;
245+		esac
246+	done < "$MAP_FILE"
247+}
248+
249+# print current dir
250+cmd_pwd() { [ -n "$CUR_DIR" ] && printf '/%s\n' "$CUR_DIR" || printf '/\n'; }
251+
252+# set server URL and reload library from new endpoint
253+cmd_url()
254+{
255+	[ $# -eq 1 ] || { printf '%s\n' "usage: url <http://host:port>" >&2; return 1; }
256+	PARADOS_URL=$1
257+	CUR_DIR=""
258+	if refresh_library; then
259+		printf 'server: %s\n' "$PARADOS_URL"
260+	else
261+		printf 'server set to: %s\n' "$PARADOS_URL"
262+		return 1
263+	fi
264+}
265+
266+# change directory based on ls index, .., or /
267+cmd_cd()
268+{
269+	[ $# -eq 1 ] || { printf '%s\n' "usage: cd <n|..|/>" >&2; return 1; }
270+	case "$1" in
271+		/) CUR_DIR=""; return 0 ;;
272+		..)
273+			[ -z "$CUR_DIR" ] && return 0
274+			case "$CUR_DIR" in */*) CUR_DIR=${CUR_DIR%/*} ;; *) CUR_DIR="" ;; esac
275+			return 0
276+			;;
277+	esac
278+	validate_idx "$1" || { printf '%s\n' "cd expects an ls index, '..', or '/'" >&2; return 1; }
279+	build_view
280+	line=$(lookup_entry "$1") || { printf '%s\n' "invalid index: $1" >&2; return 1; }
281+	split_entry "$line"
282+	[ "$ENTRY_TYP" = "D" ] || { printf '%s\n' "index is not a directory: $1" >&2; return 1; }
283+	CUR_DIR=$ENTRY_FULL
284+}
285+
286+# given ls index, get video id, call "/stream/id", pipe to VIDEO_PLAYER, print errors
287+cmd_watch()
288+{
289+	[ $# -eq 1 ] || { printf '%s\n' "usage: watch <n>" >&2; return 1; }
290+	validate_idx "$1" || { printf '%s\n' "watch expects an ls index" >&2; return 1; }
291+	build_view
292+	line=$(lookup_entry "$1") || { printf '%s\n' "invalid index: $1" >&2; return 1; }
293+	split_entry "$line"
294+	[ "$ENTRY_TYP" = "F" ] || { printf '%s\n' "index is not a video: $1" >&2; return 1; }
295+	validate_id "$ENTRY_ID" || { printf '%s\n' "invalid id in index: $1" >&2; return 1; }
296+	cmd_exists "$VIDEO_PLAYER"
297+	printf 'watching: %s\n' "$ENTRY_NAME"
298+	# shellcheck disable=SC2086
299+	set -- "$VIDEO_PLAYER" $VIDEO_PLAYER_ARGS -
300+	if [ -n "$AUTH" ]; then
301+		"$PARADOS_CURL" -sS -u "$AUTH" "${PARADOS_URL}/stream/$ENTRY_ID" | "$@"
302+	else
303+		"$PARADOS_CURL" -sS "${PARADOS_URL}/stream/$ENTRY_ID" | "$@"
304+	fi
305+}
306+
307+# call "/rescan", print response or error
308+cmd_rescan()
309+{
310+	tmp="$TMP_DIR/rescan.txt"
311+	code=$(http_get "/rescan" "$tmp") || { printf '%s\n' "request failed: /rescan" >&2; return 1; }
312+	case "$code" in
313+		200) printf '%s\n' "rescan: ok"; refresh_library ;;
314+		401) printf '%s\n' "unauthorized: use login" >&2; return 1 ;;
315+		403) printf '%s\n' "rescan forbidden: auth may be disabled server-side" >&2; return 1 ;;
316+		*) printf '%s\n' "request failed: /rescan (HTTP $code)" >&2; return 1 ;;
317+	esac
318+}
319+
320+# prompt for password without echo, return input
321+prompt_password()
322+{
323+	printf 'pass: ' >&2
324+	if command -v stty >/dev/null 2>&1; then
325+		stty_state=$(stty -g 2>/dev/null || true)
326+		stty -echo 2>/dev/null || true
327+		IFS= read -r pass || return 1
328+		[ -n "$stty_state" ] && stty "$stty_state" 2>/dev/null || stty echo 2>/dev/null || true
329+		printf '\n' >&2
330+	else
331+		IFS= read -r pass || return 1
332+	fi
333+	printf '%s' "$pass"
334+}
335+
336+# save authentication cache if enabled and AUTH set, with permissions 600
337+save_auth_cache()
338+{
339+	[ "$CACHE_LOGIN" = "1" ] || return 0
340+	[ -n "$AUTH" ] || return 0
341+	mkdir -p "$CACHE_DIR" || return 1
342+	(umask 077 && printf '%s' "$AUTH" > "$AUTH_FILE") || return 1
343+}
344+
345+# load authentication cache if enabled and cache file exists, set AUTH
346+load_auth_cache()
347+{
348+	[ "$CACHE_LOGIN" = "1" ] || return 0
349+	[ -f "$AUTH_FILE" ] || return 0
350+	AUTH=$(tr -d '\r\n' < "$AUTH_FILE")
351+}
352+
353+# delete authentication cache file if enabled
354+clear_auth_cache()
355+{
356+	[ "$CACHE_LOGIN" = "1" ] || return 0
357+	rm -f "$AUTH_FILE"
358+}
359+
360+# prompt for username and password, set AUTH
361+cmd_login()
362+{
363+	printf 'user: '
364+	IFS= read -r user || return 1
365+	[ -n "$user" ] || { printf '%s\n' "empty username" >&2; return 1; }
366+	pass=$(prompt_password) || return 1
367+	AUTH="$user:$pass"
368+	if ! refresh_library; then AUTH=""; return 1; fi
369+	save_auth_cache || printf '%s\n' "warning: failed to write auth cache" >&2
370+	printf '%s\n' "login: ok"
371+}
372+
373+# clear AUTH, delete auth cache, print result
374+cmd_logout() { AUTH=""; clear_auth_cache; printf '%s\n' "logout: ok"; }
375+
376+# dispatch command based on first arg, pass rest as args.
377+# return 99 to signal quit, 0 for success, 1 for error
378+dispatch()
379+{
380+	cmd=$1
381+	shift || true
382+	case "$cmd" in
383+		help) cmd_help ;;
384+		clear) cmd_clear ;;
385+		ping) cmd_ping ;;
386+		login) cmd_login ;;
387+		logout) cmd_logout ;;
388+		rescan) cmd_rescan ;;
389+		ls) cmd_ls ;;
390+		cd) cmd_cd "$@" ;;
391+		watch) cmd_watch "$@" ;;
392+		url) cmd_url "$@" ;;
393+		pwd) cmd_pwd ;;
394+		quit|exit) return 99 ;;
395+		'') return 0 ;;
396+		*) printf '%s\n' "unknown command: $cmd (use: help)" >&2; return 1 ;;
397+	esac
398+}
399+
400+main()
401+{
402+	trap cleanup EXIT INT TERM
403+	cmd_exists "$PARADOS_CURL"
404+	mkdir -p "$TMP_DIR" || die "failed creating temp dir: $TMP_DIR"
405+	: > "$LIB_FILE"; : > "$MAP_FILE"
406+
407+	if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then cmd_help; exit 0; fi
408+	[ $# -eq 0 ] || die "usage: shrados"
409+
410+	load_auth_cache
411+	if ! refresh_library; then printf '%s\n' "not logged in. use: login" >&2; fi
412+
413+	printf '%s\n' "shrados: minimal video repl"
414+	printf 'server: %s\n' "$PARADOS_URL"
415+	printf 'player: %s %s\n' "$VIDEO_PLAYER" "$VIDEO_PLAYER_ARGS"
416+	cmd_help
417+
418+	setup_color
419+
420+	while :; do
421+		print_prompt
422+		if ! IFS= read -r line; then printf '\n'; break; fi
423+		[ -n "$line" ] || continue
424+		set -- $line
425+		cmd=${1:-}
426+		shift || true
427+		dispatch "$cmd" "$@"
428+		rc=$?
429+		[ "$rc" -eq 99 ] && break
430+	done
431+}
432+
433+main "$@"
434+