commit dc1adbd

uint  ·  2026-03-10 18:40:21 +0000 UTC
parent df02474
shrados: move to shrados/, add README
3 files changed,  +9, -434
+1, -2
1@@ -5,7 +5,6 @@ parados
2 *.o
3 *.swp
4 *.sh
5-!clients/shrados.sh
6-
7+!clients/shrados
8 # gorados
9 clients/gorados/gorados
+0, -432
  1@@ -1,432 +0,0 @@
  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-  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 "$@"
+8, -0
1@@ -0,0 +1,8 @@
2+### shrados  
3+is a _sh_ based client for parados.  
4+Dependancies are:
5+- curl
6+- awk
7+- sort
8+- tr
9+