From: Paul Hänsch Date: Sun, 14 Jan 2024 21:21:33 +0000 (+0100) Subject: Merge commit 'b931bbd0c30907b9cc956d3707b26b449bf41f76' X-Git-Url: http://git.plutz.net/?p=serve0;a=commitdiff_plain;h=HEAD;hp=b931bbd0c30907b9cc956d3707b26b449bf41f76 Merge commit 'b931bbd0c30907b9cc956d3707b26b449bf41f76' --- diff --git a/.gitignore b/.gitignore index 5c9950a..a01ee28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -cgilite -serverkey -users.db +.*.swp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..de179ab --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: _subtrees + +_subtrees: _cgilite + +cgilite: + git subtree add --squash -P $@ https://git.plutz.net/git/$@ master + +_cgilite: cgilite + git subtree pull --squash -P $< https://git.plutz.net/git/$< master + diff --git a/advsearch.sh b/advsearch.sh new file mode 100755 index 0000000..24d31c9 --- /dev/null +++ b/advsearch.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +f='' +order="$(POST order |grep -m1 -xE 'Name|Date|Length|Group' || printf Name)" + +for n in 1 2 3 4 5 6 7 8 9 10; do + [ "$(POST pol_$n)" = neg ] \ + && f="$f~" + cat="$(POST cat_$n)" + for m in $(seq 1 $(POST_COUNT tag_$n)); do + tag="$(POST tag_$n $m)" + [ ! "${tag##${cat}:*}" ] || [ ! "${tag##-${cat}:*}" ] || [ "$cat" = '*' -a "${tag##*:*}" ] \ + && f="${f}${tag}|" + [ "$cat" = \$ ] && f="${f}\$:${tag}|" + done + f="${f%[|^]}^" +done +f="$(printf '%s' "$f" |sed -E 's;[~|^]+$;;; s;\|\^;^;g;')" +#f="${f%^}" + +REDIRECT "$(URL "${ITEM}")?o=${order}&f=${f}" diff --git a/cgilite/.gitignore b/cgilite/.gitignore new file mode 100644 index 0000000..5c9950a --- /dev/null +++ b/cgilite/.gitignore @@ -0,0 +1,3 @@ +cgilite +serverkey +users.db diff --git a/cgilite.sh b/cgilite/cgilite.sh similarity index 100% rename from cgilite.sh rename to cgilite/cgilite.sh diff --git a/common.css b/cgilite/common.css similarity index 100% rename from common.css rename to cgilite/common.css diff --git a/file.sh b/cgilite/file.sh similarity index 100% rename from file.sh rename to cgilite/file.sh diff --git a/html-sh.sed b/cgilite/html-sh.sed similarity index 100% rename from html-sh.sed rename to cgilite/html-sh.sed diff --git a/logging.sh b/cgilite/logging.sh similarity index 100% rename from logging.sh rename to cgilite/logging.sh diff --git a/markdown.awk b/cgilite/markdown.awk similarity index 100% rename from markdown.awk rename to cgilite/markdown.awk diff --git a/session.sh b/cgilite/session.sh similarity index 100% rename from session.sh rename to cgilite/session.sh diff --git a/storage.sh b/cgilite/storage.sh similarity index 100% rename from storage.sh rename to cgilite/storage.sh diff --git a/users.sh b/cgilite/users.sh similarity index 100% rename from users.sh rename to cgilite/users.sh diff --git a/db_meta.sh b/db_meta.sh new file mode 100755 index 0000000..572ebbd --- /dev/null +++ b/db_meta.sh @@ -0,0 +1,208 @@ +#!/bin/sh + +[ "$include_dbmeta" ] && return 0 +include_dbmeta="$0" + +. "$_EXEC/cgilite/storage.sh" + +# == FILE FORMAT == +# LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP + +# == GLOBALS == +UNSET_META='unset \ + META_LENGTH META_WIDTH META_HEIGHT META_TAGS META_COMMENT META_NAME META_GROUP +' + +LOCAL_META='local \ + META_LENGTH META_WIDTH META_HEIGHT META_TAGS META_COMMENT META_NAME META_GROUP +' + +eval "$UNSET_META" + +read_meta() { + local name="$1" meta_db="$_DATA/.index/meta" + + [ "${name%%/*}" != "$name" ] \ + && meta_db="$_DATA/${name%%/*}/.index/meta" + name="$(STRING "${name##*/}")" + + # Global exports + META_LENGTH='' META_WIDTH='' META_HEIGHT='' META_TAGS='' + META_COMMENT='' META_NAME='' META_GROUP='' META_GROUPORDER='' + + if [ $# -eq 0 ]; then + read -r META_LENGTH META_WIDTH META_HEIGHT META_TAGS META_COMMENT \ + META_NAME META_GROUP META_GROUPORDER + elif [ "$name" -a -f "$meta_db" -a -r "$meta_db" ]; then + read -r META_LENGTH META_WIDTH META_HEIGHT META_TAGS META_COMMENT \ + META_NAME META_GROUP META_GROUPORDER <<-EOF + $(grep -F " ${name}${CR}" "$meta_db" |dbmeta_autogroup) + EOF + fi + if [ "$META_NAME" ]; then + META_NAME="$(UNSTRING "${META_NAME%${CR}}")" + META_COMMENT="$(UNSTRING "${META_COMMENT#comment=}")" + META_TAGS="$(UNSTRING "${META_TAGS#tags=}")" + META_GROUP="${META_GROUP#\\}" + else + eval "$UNSET_META" + return 1 + fi +} + +update_meta(){ + local name="${1:=${META_NAME}}" tags comment length width height group + local LENGTH WIDTH HEIGH TAGS COMMENT NAME GROUP + local arg cnt meta_db="$_DATA/.index/meta" + + [ "${name%%/*}" != "$name" ] \ + && meta_db="$_DATA/${name%%/*}/.index/meta" + name="$(STRING "${name##*/}")" + + for arg in "$@"; do case $arg in + comment=*) comment="${arg#*=}";; + tags=*) tags="${arg#*=}";; + lenght=*) lenght="${arg#*=}";; + width=*) width="${arg#*=}";; + height=*) height="${arg#*=}";; + group=*) group="${arg#*=}";; + esac; done + + if LOCK "$meta_db"; then + read -r LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP <<-EOF + $(grep -F " ${name}${CR}" "$meta_db") + EOF + if [ ! "$NAME" ]; then + RELEASE "$meta_db" + return 1 + fi + printf '%i %i %i tags=%s comment=%s %s\r %s\n' \ + "${length:-${length-${LENGTH}}${length+0}}" \ + "${width:-${width-${WIDTH}}${width+0}}" \ + "${height:-${height-${HEIGHT}}${height+0}}" \ + "$(STRING "${tags-$(UNSTRING "${TAGS#tags=}")}")" \ + "$(STRING "${comment-$(UNSTRING "${COMMENT#comment=}")}")" \ + "${NAME%${CR}}" "${group:-${GROUP:-\\}}" \ + >"${meta_db}.$$" + + grep -vF " ${name}${CR}" "$meta_db" >>"${meta_db}.$$" + + mv -- "${meta_db}.$$" "${meta_db}" + RELEASE "$meta_db" + else + return 1 + fi +} + +new_meta(){ + local name="$1" meta_db="$_DATA/.index/meta" + local LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP + + [ "${name%%/*}" != "$name" ] \ + && meta_db="$_DATA/${name%%/*}/.index/meta" + name="$(STRING "${name##*/}")" + + if LOCK "$meta_db"; then + while read -r LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP; do + if [ "$name" = "${NAME%${CR}}" ]; then + RELEASE "$vid_db" + return 1 + fi + done <"$meta_db" + printf '0 0 0 tags=\\ comment=\\ %s\r \\\n' \ + "${name}" >>"$meta_db" + RELEASE "$meta_db" + else + return 1 + fi +} + +delete_meta() { + local name="$1" meta_db="$_DATA/.index/meta" + local LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP + + [ "${name%%/*}" != "$name" ] \ + && meta_db="$_DATA/${name%%/*}/.index/meta" + name="$(STRING "${name##*/}")" + + if LOCK "$meta_db"; then + while read -r LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP; do + [ "$name" = "${NAME%${CR}}" ] \ + || printf '%i %i %i tags=%s comment=%s %s\r %s\n' \ + "$length" "$width" "$height" "${TAGS#tags=}" \ + "${COMMENT#comment=}" "${NAME%${CR}}" "${GROUP:-\\}" + done <"$meta_db" >"${meta_db}.$$" + + mv -- "${meta_db}.$$" "$meta_db" + RELEASE "$meta_db" + else + return 1 + fi +} + +list_meta(){ + local meta pfx + local LENGTH WIDTH HEIGHT TAGS COMMENT NAME GROUP + + if [ "$#" -eq 0 ]; then + find "$_DATA" -path '*/.index/meta' + else + printf %s\\n "$@" + fi \ + | while read meta; do + pfx="${meta#$_DATA}" + pfx="${pfx%/.index/meta}" + pfx="$(STRING "${pfx#/}")" + [ "$pfx" = '\' ] && pfx='' || pfx="${pfx}/" + + { printf '%s\n' "$pfx" + dbmeta_autogroup "$meta" + } | sed -E ' + 1{ h; d; } + G; + s;^([^\t]+ [^\t]+ [^\t]+ [^\t]+ [^\t]+ )([^\n]+)\n(.*)$;\1\3\2; + ' + done +} + +dbmeta_autogroup(){ + sed -E ' + # strip empty group field + s;\r \\$;\r;; + h; # save original dataset + + # strip common suffixes of web video sites + s;-([0-9a-zA-Z_-]{11}|ph[0-9a-f]{13}|xh[0-9a-zA-Z]{5}|[0-9]{6,})\r;-\r;; + + # perform auto grouping if group id is missing or empty + /\r$/bAUTOGROUP; + + # only perform ordering if manual group id is present + /\r .+$/bAUTOORDER; + + b; # pass invalid records without processing + + :AUTOORDER + # strip all fields but the name + s;^([^\t]+ [^\t]+ [^\t]+ [^\t]+ [^\t]+ )([^\r]+)\r (.+)$;\2; + + # reduce to numerals + s;[^0-9]+;;g; + + # append ordering field to dataset + H; g; s;\n;\t;; + b; + + :AUTOGROUP + # strip all fields but the name + s;^[^\t]+ [^\t]+ [^\t]+ [^\t]+ [^\t]+ ;; + + # replace all numeric parts and append numerals to an ordering field + # the group id will be made up of only the non-numeric character frame + # the ordering field will hold all numbers from the name + :X s;^([^0-9]*)([0-9]+)([^\r]*)\r\t?([0-9]*)$;\1\r\3\r \4\2;; tX; + + # append group id and ordering field to dataset + H; g; s;\n;\t;; + ' "$@" +} diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..79b9a87 --- /dev/null +++ b/index.cgi @@ -0,0 +1,96 @@ +#!/bin/sh + +exec 2>/dev/null +file_pattern='^.*\.(mov|ts|mpg|mpeg|mp4|m4v|avi|mkv|flv|sfv|wmv|ogm|ogv|webm|iso|rmvb)$' + +. "${_EXEC:-${0%/*}}/cgilite/cgilite.sh" + +FILTER="$(GET f)" +SEARCH="$(GET s)" +ORDER="$(GET o |grep -m1 -axE 'Date|Name|Length|Group' || printf Name)" +LISTSIZE="$(COOKIE pagesize |grep -m1 -axE '[1-9][0-9]*' || printf 60)" +ITEM="${PATH_INFO%/}" +ACTION="$(GET a)" + +case $ACTION in + setprefs) + SET_COOKIE +$((86400 * 90)) pagesize="$(POST pagesize |grep -m1 -axE '[1-9][0-9]*' || printf 60)" + SET_COOKIE +$((86400 * 90)) mode="$(POST mode |grep -m1 -axE 'browse|index' || printf browse)" + SET_COOKIE +$((86400 * 90)) fakemp4="$(POST fakemp4 |grep -m1 -axE 'yes' || printf no)" + SET_COOKIE +$((86400 * 90)) downscale="$(POST downscale |grep -m1 -axE 'yes' || printf no)" + [ "$(POST index)" = "update" ] && touch -cd @0 "${_DATA}/.index/meta.time" + REDIRECT "$(POST ref)" + ;; + bookmark) + bm="$_DATA/.index/bookmarks" + . "$_EXEC/cgilite/storage.sh" + + s="$(POST search |STRING)"; f="$(POST filter |STRING)" + if LOCK "$bm"; then + grep -avF " search=$s filter=$f${CR}" "$bm" >"$bm.tmp" + [ ! "$(POST delete)" ] \ + && printf '%s search=%s filter=%s\r\n' \ + "$(POST name |STRING)" "$s" "$f" >>"$bm.tmp" + mv "$bm.tmp" "$bm" + RELEASE "$bm" + fi + REDIRECT "$(POST ref)" + ;; + multitag) + . "$_EXEC/multitag.sh" + REDIRECT "$(POST ref)" + ;; + thumbnail) if [ -f "$_DATA/$PATH_INFO" ]; then + . "$_EXEC/cgilite/file.sh" + index="$_DATA/${PATH_INFO%/*}/.index" + thumb="$index/${PATH_INFO##*/}"; thumb="${thumb%.*}.jpg" + if [ -d "$index" -a ! -f "$thumb" ] && { printf %s "$PATH_INFO" |grep -qE -e "${file_pattern}" ;}; then + . "$_EXEC/thumbnail.sh" + gen_thumb "$_DATA/$PATH_INFO" "$thumb" + fi + FILE "$thumb" + return 0 + fi;; + download) if [ -f "$_DATA/$PATH_INFO" ]; then + . "$_EXEC/cgilite/file.sh" + fakemp4="$(COOKIE fakemp4)" + downscale="$(COOKIE downscale)" + downfile="$_DATA/${PATH_INFO%/*}/.transcode/${PATH_INFO%.*}.480p.webm" + if [ "$downscale" = yes -a -f "$downfile" ]; then + FILE "$downfile" "$([ "$fakemp4" = yes ] && printf 'video/mp4')" + else + FILE "$_DATA/$PATH_INFO" "$([ "$fakemp4" = yes ] && printf 'video/mp4')" + fi + return 0 + fi;; + delete) if [ -f "$_DATA/$PATH_INFO" ]; then + : + fi;; + advsearch) if [ -d "$_DATA/$PATH_INFO" ]; then + . "$_EXEC/advsearch.sh" + return 0 + fi;; + spawnindex) if [ -d "$_DATA/$PATH_INFO" ]; then + if [ "$(POST recursive)" = yes ]; then + find "$_DATA/$PATH_INFO" -depth -type d \! -name .index \ + -exec mkdir -p '{}'/.index \; + else + mkdir -p "$_DATA/$PATH_INFO/.index" + fi + REDIRECT "$(POST ref)" + fi;; +esac + +if [ -f "$_EXEC/$PATH_INFO" ]; then + . "$_EXEC/cgilite/file.sh" + FILE "$_EXEC/$PATH_INFO" + return 0 +elif [ -f "$_DATA/$PATH_INFO" ]; then + . "$_EXEC/view.sh" + return 0 +elif [ -d "$_DATA/$PATH_INFO" ]; then + . "$_EXEC/list.sh" + return 0 +else + printf 'Status: 404 Not Found\r\nContent-Length 0:\r\n\r\n' +fi diff --git a/index2post.sh b/index2post.sh new file mode 100755 index 0000000..5fdf9ca --- /dev/null +++ b/index2post.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +REQUEST_METHOD=manual +. "${_EXEC:-${0%/*}}/cgilite/cgilite.sh" +. "${_EXEC}/cgilite/storage.sh" + +find "${_DATA}" -path '*/.index/meta' -exec cat '{}' + \ +| while read l w h t c n; do + printf '?op=add&select=%s' "/$(URL "$(UNSTRING "${n%$CR}")"*)" + printf "&tag=%s" $(printf %s "${t#tags=}" |tr , \ ) + printf \\n +done diff --git a/indexmeta.sh b/indexmeta.sh new file mode 100755 index 0000000..87fbd12 --- /dev/null +++ b/indexmeta.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +[ -n "$include_indexmeta" ] && return 0 +include_indexmeta="$0" + +. "$_EXEC/cgilite/storage.sh" +file_pattern='^.*\.(mov|ts|mpg|mpeg|mp4|m4v|avi|mkv|flv|sfv|wmv|ogm|ogv|webm|iso|rmvb)$' + +meta_name() { + local fn="${1##*/}" + STRING "${fn%.*}" +} + +meta_line() { + local video probe l w h + video="$1" + [ "${video%.part}" = "$video" -a -s "$video" ] || return 0 + + probe="$(printf \\n; ffprobe -show_entries format=duration:stream=width,height "$video" 2>&-)" + l="${probe#*duration=}" l="${l%%${BR}*}" l="${l%.*}" + w="${probe#*width=}" w="${w%%${BR}*}" + h="${probe#*height=}" h="${h%%${BR}*}" + + printf '%i %i %i tags= comment= %s\r\n' \ + "${l:-0}" "${w:-0}" "${h:-0}" "$(meta_name "$video")" +} + +meta_file(){ + local file meta name + file="$1" + meta="${file%/*}/.index/meta" + name="$(meta_name "$file")" + + if [ -d "${meta%/meta}" ] && LOCK "$meta"; then + grep -avF " ${name}${CR}" "$meta" >"$meta.tmp" + meta_line "$file" \ + | tee -a "$meta.tmp" + mv "$meta.tmp" "$meta" + RELEASE "$meta" + fi +} + +meta_purge(){ + local file meta name + file="$1" + meta="${file%/*}/.index/meta" + name="$(meta_name "$file")" + + if [ -d "${meta%/meta}" ] && LOCK "$meta"; then + grep -avF " ${name}${CR}" "$meta" >"${meta}.tmp" + grep -aF " ${name}${CR}" "$meta" >>"${meta}.trash" + mv "${meta}.tmp" "$meta" + RELEASE "$meta" + fi +} + +meta_info(){ + local file meta + file="$1"; meta="${file%/*}/.index/meta" + + if [ -d "${meta%/meta}" ]; then + grep -aF " $(meta_name "$file")${CR}" "$meta" \ + | grep -m1 -axE '[0-9]+ [0-9]+ [0-9]+ tags=[^ ]* comment=[^ ]* .+' \ + || meta_file "$file" + else + printf '0\t0\t0\ttags=\tcomment=\t%s\r\n' "$(meta_name "$file")" + fi +} + +meta_dir(){ + local dir meta v + dir="${1}" + meta="${dir}/.index/meta" + metat="${dir}/.index/meta.time" + + [ -f "$metat" ] || touch -d @0 "$metat" + + if [ -d "$dir/.index" -a \! -f "$meta" ] && LOCK "$meta"; then + touch "$meta" # preliminary touch to prevent concurrent generators + find -L "$dir" -type f -mindepth 1 -maxdepth 1 \ + | grep -aE "$file_pattern" \ + | while read -r v; do + meta_line "$v" + done >"$meta" + touch "$metat" + + RELEASE "$meta" + elif [ -d "$dir/.index" -a "$dir" -nt "$metat" ] && LOCK "$meta"; then + touch "$meta" + find -L "$dir" -type f -newer "$metat" \ + -mindepth 1 -maxdepth 1 \ + | grep -aE "$file_pattern" \ + | while read -r v; do + grep -qF " $(meta_name "$v")${CR}" "$meta" \ + || meta_line "$v" + done >>"$meta" + touch "$metat" + + RELEASE "$meta" + fi +} diff --git a/list.sh b/list.sh new file mode 100755 index 0000000..94a9264 --- /dev/null +++ b/list.sh @@ -0,0 +1,229 @@ +#!/bin/sh + +. "$_EXEC/indexmeta.sh" +. "$_EXEC/widgets.sh" +. "$_EXEC/db_meta.sh" + +list_item() { + local meta file link name + + if [ "${META_NAME%/}" != "${META_NAME}" ]; then + printf '[a .list .dir href="%s?%s" . %s]' \ + "$(URL "${PATH_INFO%/}/${META_NAME}")" "${w_refuri#*\?}" \ + "$(HTML "${META_NAME%/}")" + return 0 + fi + + file="$_DATA/${PATH_INFO%/}/$(list_fullname "${META_NAME}")" + if [ -f "$file" ]; then + link="$(URL "${PATH_INFO%/}/${file#${_DATA}/${PATH_INFO}}")" + name="$(HTML "${PATH_INFO%/}/${file#${_DATA}/${PATH_INFO}}")" + printf '[div .list .file + [a href="%s" [img src="%s?a=thumbnail"]][label . %s] + [span .time %i:%02imin] [span .dim %ix%i] %s + [checkbox "select" "%s" id="select_%s"][label for="select_%s" +] + ]' \ + "$link" "$link" "${name##/}" \ + "$((META_LENGTH / 60))" "$((META_LENGTH % 60))" \ + "$META_WIDTH" "$META_HEIGHT" \ + "$(printf %s\\n "${META_TAGS}" \ + | sed -r 's;^;,;; s;,+;,;g; s;,$;;; + :X s;,-?([^,]+)(,|$); [span .tag\n \1]\2;; tX;' + )" "$name" "$link" "$link" + else + debug "Canning record for nonexist file: $META_NAME" + meta_purge "$_DATA/$ITEM/$META_NAME" + fi +} + +[ "$FILTER" ] && list_fex="$( + fex='p' + STRING "$FILTER^" \ + | sed -E 's;\^;\n;g; s;[]\/\(\)\\\^\$\?\.\+\*\;\[\{\}];\\&;g' \ + | while read -r f; do + [ "${f##*[A-Z]*}" ] && tl="y;ABCDEFGHIJKLMNOPQRSTUVWXYZ;abcdefghijklmnopqrstuvwxyz;;" + case $f in + ''|~) continue;; + ~\\\$:*) fex="h; ${tl} /${f#~\\\$:}/d; g;${fex}";; + \\\$:*) fex="h; ${tl} /${f#\\\$:}/{g;${fex}}";; + ~*) fex="/(\ttags=([^\t]*,)?)(${f#\~})((,[^\t]*)?\t)/d; ${fex}";; + *) fex="/(\ttags=([^\t]*,)?)(${f})((,[^\t]*)?\t)/{${fex}}";; + esac + printf '%s\n' "${fex}" + done \ + | tail -n1 +)" + +list_fullname(){ + local short="$1" file + file="$(printf %s\\n "$_DATA/$ITEM/$short".*)" + file="${file%%${BR}*}" + [ -e "$file" ] && printf %s\\n "${file#${_DATA}/${ITEM}/}" +} + +list_filter(){ + if [ "$FILTER" ]; then + debug "FEX:" "$list_fex" + sed -nE "$list_fex" + elif [ "${SEARCH#!}" != "${SEARCH}" ]; then + grep -aviEe "$(STRING "${SEARCH}" \ + | sed -E ':x s;((^|[^\\])(\\\\)*)\+;\1 ;g; tx; + s;((^|[^\\])(\\\\)*)\\\+;\1+;g; + s; ;\\+;g;')" + elif [ "${SEARCH}" ]; then + grep -aiEe "$(STRING "${SEARCH}" \ + | sed -E ':x s;((^|[^\\])(\\\\)*)\+;\1 ;g; tx; + s;((^|[^\\])(\\\\)*)\\\+;\1+;g; + s; ;\\+;g;')" + else + cat + fi +} + +list_order(){ + local fm fn fn al length ln h w t c name group o buffer l + + if [ $ORDER = Name ]; then + sort -k6 + elif [ $ORDER = Group ]; then + { sort -n -k8 -k6,6 |sort -s -k7,7 ; echo '0 0 0 tags= comment= _'; } \ + | while read -r length w h t c name group o; do + if [ "${ln%% *}" = "${group}" ]; then + al=$((al + length)) + buffer="${buffer}${BR}$length $w $h $t $c $name" + else + printf '%s\n' "$buffer" |while read -r l; do + [ "$l" ] && printf '%i %s\n' "$al" "$l" + done + al="$length" + buffer="$length $w $h $t $c $name" + fi + ln="$group" + done \ + | sort -s -n -k1,1 |sed -E 's;^[0-9]+\t;;;' + elif [ $ORDER = Length ]; then + sort -sn -k1 + elif [ $ORDER = Date ]; then + while read -r fm; do + fn="${fm%${CR} *}" + fn="$(list_fullname "${fn##* }")" + printf '%i %s\n' \ + "$(stat -c %Y "$fn")" "${fm}" + done \ + | sort -srn -k1 |sed -E 's;^[0-9]+\t;;;' + fi +} + +list_items() { + local mode meta cachename + mode="$(COOKIE mode |grep -m1 -axE 'index|browse' || printf index )" + + cachename="$(printf '%s\n' "$mode" "$FILTER" "$SEARCH" "$ORDER" |sha1sum)" + cachename="$_DATA/$ITEM/.index/${cachename% -}.cache" + meta="$_DATA/$ITEM/.index/meta" + meta_dir "$_DATA/$ITEM/" + + if [ "$mode" = browse ]; then + [ "$ITEM" ] && printf '0 0 0 \ \ ../\n' + (cd "$_DATA/$ITEM"; + find ./ -type d \! -name .index -mindepth 1 -maxdepth 1 \ + ) | sort |while read dir; do + printf '0 0 0 \\ \\ %s\n' "$(STRING "${dir#./}")" + done + + if [ "$cachename" -nt "$meta" ]; then + cat "$cachename" + else + list_meta "$meta" \ + | list_filter \ + | list_order \ + | { [ -d "${cachename%/*}" ] && tee "$cachename" || cat; } + fi + + elif [ "$mode" = index ]; then + if [ -f "$cachename" -a ! "$(find "$_DATA" -path '*/.index/meta' -newer "$cachename")" ]; then + cat "$cachename" + else + list_meta \ + | list_filter \ + | list_order \ + | { [ -d "${cachename%/*}" ] && tee "$cachename" || cat; } + fi + fi +} + +list_paginate() { + local page i c n end qry + page="$(GET p |grep -axE '[0-9]+' || printf 1)"; c=1 + end=$((page + LISTSIZE)) + eval "$LOCAL_META" # localize vars from db_meta + + printf '[div .itemlist ' + while :; do + if [ $c -lt $page ]; then + read -r discard || break + elif [ $c -ge $end ]; then + c=$((c + $(wc -l) )) + break + else + read_meta || break + list_item + fi + c=$((c + 1)) + done + printf ']' + + [ $(( c % LISTSIZE )) -gt 0 ] \ + && end=$((c / LISTSIZE + 1)) \ + || end=$((c / LISTSIZE)) + + printf '[div .pagination' + for n in $( seq 1 $end ); do + c=$(( (n - 1) * LISTSIZE + 1 )) + [ $c = $page ] \ + && printf '[a .page .current href="?p=%i&%s" %i]' "${c}" "${QUERY_STRING#p=*&}" "$n" \ + || printf '[a .page href="?p=%i&%s" %i]' "${c}" "${QUERY_STRING#p=*&}" "$n" + done + printf ']' +} + +printf 'Content-Type: text/html;charset=utf-8\r\n\r\n' + +{ printf ' +[!DOCTYPE HTML] +[html [head [title ' + w_bmname + printf ' by %s]' "$ORDER" + printf ' + [meta name="viewport" content="width=device-width"] + [link rel=stylesheet href="/cgilite/common.css" ] + [link rel=stylesheet href="/style.css" ] +] [body + [div #navigation + [a #t_bookmarks href="#bookmarks" ★]' + w_search + printf ' + [a #t_prefs href="#prefs" ⚙] + ]' + w_bookmarks + w_advsearch + w_prefs + printf ' + [form method=POST action="?a=multitag"' + list_items \ + | list_paginate + [ -d "$_DATA/$ITEM/.index" ] && { printf ' + [div #editing' + w_tagging + printf ' + ]'; } + printf ' + ]' + [ ! -d "$_DATA/$ITEM/.index" ] && { printf ' + [div #editing' + w_index + printf ' + ]'; } + printf ' +] ] +'; } | "$_EXEC/cgilite/html-sh.sed" diff --git a/multitag.sh b/multitag.sh new file mode 100755 index 0000000..c66ff14 --- /dev/null +++ b/multitag.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +. "$_EXEC/db_meta.sh" +. "$_EXEC/cgilite/session.sh" nocookie +. "$_EXEC/widgets.sh" + +# newtags='' # tags selected in ui +# tags='' # taglist from database +# detag='' # temporary remove list +# extags='' # exclusive tags, tags that preclude others within their categorie +# group='' # id for grouping videos (videos in a group get a common group id) +# op='' # operation: add | del | flip + +newtags='' +for tn in $(seq 1 $(POST_COUNT tag)); do + newtags="$(POST tag $tn)${BR}${newtags}" +done +newtags="$(POST newtag |tr -d '\r' |tr , '\n')${BR}${newtags}" + +if [ "$(POST makegroup)" = true ]; then + group="$(timeid)" +fi + +# strip leading and trailing line breaks +while [ "${newtags#${BR}}" != "${newtags}" ]; do newtags="${newtags#${BR}}"; done +while [ "${newtags%${BR}}" != "${newtags}" ]; do newtags="${newtags%${BR}}"; done + +op="$(POST op)" + +for select in $(seq 1 $(POST_COUNT select)); do + file="$(POST select $select)" + read_meta "${file%.*}" || continue + + tags="$(printf %s\\n "$META_TAGS" |tr , \\n)" + + if [ "$op" = add ]; then + extags="$(printf '%s' "${newtags}" |grep -e "^-" |cut -d: -f1 )" + [ "$extags" ] && tags="$(printf %s\\n "$tags" |grep -vwFe "$extags")" + if printf %s "${newtags}" |grep -e "^-" |grep -qEe "^-[^:]+$"; then + tags="$(printf %s\\n "$tags" |grep -vEe '^-[^:]+$')" + fi + tags="${tags}${BR}${newtags}" + + elif [ "$op" = del ]; then + detag="${newtags}${BR}"; while [ "$detag" ]; do + tags="$(printf '%s\n' "$tags" |grep -vxFe "${detag%%${BR}*}")" + detag="${detag#*${BR}}" + done + + elif [ "$op" = flip ]; then + fliptag="${newtags}${BR}"; while [ "$fliptag" ]; do + comp="$tags" + tags="$(printf '%s\n' "$tags" |grep -vxFe "${fliptag%%${BR}*}")" + [ "$comp" = "$tags" ] && tags="${tags}${BR}${fliptag%%${BR}*}" + fliptag="${fliptag#*${BR}}" + done + + fi + tags="$(printf '%s\n' "$tags" |sort -u |tr '\n' ,)" + tags="${tags#,}"; tags="${tags%,}" + + if [ "$group" ]; then + update_meta "$META_NAME" tags="${tags}" group="${group}" + else + update_meta "$META_NAME" tags="${tags}" + fi +done + +( if [ $(POST_COUNT select) -gt 0 ]; then + taglist >"$c_tags.$$" + mv -- "$c_tags.$$" "$c_tags" + fi & +) & diff --git a/stereoview.js b/stereoview.js new file mode 100644 index 0000000..0d46a66 --- /dev/null +++ b/stereoview.js @@ -0,0 +1,247 @@ +/* Copyright 2018, 2023 Paul Hänsch + + This file is part of Serve0 + + Serve0 is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Serve0 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Serve0 If not, see . +*/ + +var contLeft = document.createElement("div"); +var contRight = document.createElement("div"); +var lv = document.createElement("canvas"); +var rv = document.createElement("canvas"); +var debug = document.createElement("p"); + + contLeft.setAttribute("style", "position: fixed; top: 0; left: 0; width: 50%; height: 100%; overflow: hidden; z-index: 100; background-color: #000;"); +contRight.setAttribute("style", "position: fixed; top: 0; right: 0; width: 50%; height: 100%; overflow: hidden; z-index: 100; background-color: #000;"); + lv.setAttribute("style", "position: absolute; top: 50%; left: calc(100% - 32mm); max-width: unset; max-height: unset;"); + rv.setAttribute("style", "position: absolute; top: 50%; left: 32mm; max-width: unset; max-height: unset;"); + debug.setAttribute("style", "display: none; position: fixed; top: 0; left: 0; z-index: 101; background: #000;"); + + +function stereoview(layout, video) { + this.layout = layout; this.video = video; + + let w = video.videoWidth; h = video.videoHeight; + let render, controlTimeout = 0; + let pitch = 0, roll = 0, yaw = 0; + let scale, vsf = 1, fov = 90, dist = 32; + + document.body.appendChild(contLeft ).appendChild( lv ); + document.body.appendChild(contRight).appendChild( rv ); + document.body.appendChild( debug ); + + if ( layout == "180") { + lv.width = rv.width = w / 2; lv.height = rv.height = h; + scale = contLeft.offsetHeight / h * 2 * fov / 90; + } else { + lv.width = rv.width = w; lv.height = rv.height = h / 2; + scale = contLeft.offsetHeight / h * 4 * fov / 90; + } + + lc = lv.getContext("2d"); rc = rv.getContext("2d"); + + function draw() { + if ( layout == "180" ) { + // scale = contLeft.offsetHeight / h * 2 * fov / 90; + lc.drawImage(video, 0, 0); + rc.drawImage(video, -w / 2, 0); + lv.style.transform = rv.style.transform = + "translate(" + (yaw / 180 * -w / 2 - w / 4) + "px, " + + (pitch / 90 * -h / 2 * scale - h / 2) + "px)" + + "rotate(" + roll + "deg) " + "scale(" + (scale / vsf) + ", " + scale + ")"; + } else { + // scale = contLeft.offsetHeight / h * 4 * fov / 90; + lc.drawImage(video, yaw / 180 * -w / 2, 0); + lc.drawImage(video, yaw / 180 * -w / 2 + ((yaw>0) ? w : -w), 0); + rc.drawImage(video, yaw / 180 * -w / 2, -h/2); + rc.drawImage(video, yaw / 180 * -w / 2 + ((yaw>0) ? w : -w), -h/2); + lv.style.transform = rv.style.transform = + "translate(" + (- w / 2) + "px, " + (pitch / 90 * -h / 2 * (scale / 2) - h / 4) + "px) " + + "rotate(" + roll + "deg) " + "scale(" + (scale / vsf) + ", " + scale + ")"; + } + + // debug.textContent = "" + video.currentTime + " " + controlTimeout + " " + tx + " " + ty + " " + tz; + debug.textContent = "Pitch: " + pitch.toFixed(2) + " | Yaw: " + yaw.toFixed(2) + " | Roll: " + roll.toFixed(2) + + " | FOV: " + fov + " | Eye Distance: " + (2 * dist) + "mm"; + + requestAnimationFrame(draw); + + gpinput(); + }; + + function gpinput() { + let gp = navigator.getGamepads()[0]; + let date = new Date(); + + debug.innerHTML += "

GamePad: " + gp.axes[0].toFixed(3) + " | " + gp.axes[1].toFixed(3) + for (cnt = 0; cnt < 16; cnt++) { + gp.buttons[cnt].pressed ? debug.textContent += " | B" + cnt + " 1" : debug.textContent += " | B" + cnt + " 0"; + } + + if ( gp && Date.now() < controlTimeout ) { + true; + } else if (gp.buttons[0].pressed && gp.buttons[5].pressed) { + this.layout = layout = "180"; + document.body.appendChild(contLeft ).appendChild( lv ); + document.body.appendChild(contRight).appendChild( rv ); + lv.width = rv.width = w / 2; lv.height = rv.height = h; + scale = contLeft.offsetHeight / h * 2 * fov / 90; + video.play(); + controlTimeout = Date.now() + 600; + } else if (gp.buttons[0].pressed && gp.buttons[4].pressed) { + this.layout = layout = "360"; + document.body.appendChild(contLeft ).appendChild( lv ); + document.body.appendChild(contRight).appendChild( rv ); + lv.width = rv.width = w; lv.height = rv.height = h / 2; + scale = contLeft.offsetHeight / h * 4 * fov / 90; + video.play(); + controlTimeout = Date.now() + 600; + } else if (gp.buttons[3].pressed && gp.buttons[4].pressed) { + vsf = (vsf == 1) ? 2 : 1; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[0].pressed) { + ( contLeft.parentElement)? contLeft.parentElement.removeChild(contLeft ):{}; + (contRight.parentElement)?contRight.parentElement.removeChild(contRight):{}; + video.pause(); + controlTimeout = Date.now() + 300; + } else if (gp.buttons[1].pressed) { + (debug.style.display == "block") ? debug.style.display = "none" : debug.style.display = "block"; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[5].pressed) { + video.paused ? video.play() : video.pause(); + controlTimeout = Date.now() + 300; + } else if (gp.buttons[4].pressed) { + video.currentTime += 1 / 30; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[3].pressed && gp.axes[0] < -.3) { + dist -= .5; + lv.style.left = "calc(100% - " + dist + "mm)"; + rv.style.left = "" + dist + "mm"; + date.setTime(date.getTime() + 3 * 365 * 86400 * 1000) + document.cookie = "StereoDist=" + dist + "; expires=" + date.toUTCString() + "; path=/"; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[3].pressed && gp.axes[0] > .3) { + dist += .5; + lv.style.left = "calc(100% - " + dist + "mm)"; + rv.style.left = "" + dist + "mm"; + date.setTime(date.getTime() + 3 * 365 * 86400 * 1000) + document.cookie = "StereoDist=" + dist + "; expires=" + date.toUTCString() + "; path=/"; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[3].pressed && gp.axes[1] < -.3) { + fov -= 10; + date.setTime(date.getTime() + 3 * 365 * 86400 * 1000) + document.cookie = "StereoFOV=" + fov + "; expires=" + date.toUTCString() + "; path=/"; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[3].pressed && gp.axes[1] > .3) { + fov += 10; + date.setTime(date.getTime() + 3 * 365 * 86400 * 1000) + document.cookie = "StereoFOV=" + fov + "; expires=" + date.toUTCString() + "; path=/"; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[2].pressed && gp.axes[0] < -.3) { + yaw -= pitch ? ( Math.cos(pitch * Math.PI / 180) % 360 ) : 1; + if ( yaw > 180) yaw -= 360; + if ( yaw < -180) yaw += 360; + controlTimeout = Date.now() + 30; + } else if (gp.buttons[2].pressed && gp.axes[0] > .3) { + yaw += pitch ? ( Math.cos(pitch * Math.PI / 180) % 360 ) : 1; + if ( yaw > 180) yaw -= 360; + if ( yaw < -180) yaw += 360; + controlTimeout = Date.now() + 30; + } else if (gp.buttons[2].pressed && gp.axes[1] < -.3) { + video.volume += .05; + controlTimeout = Date.now() + 300; + } else if (gp.buttons[2].pressed && gp.axes[1] > .3) { + video.volume -= .05; + controlTimeout = Date.now() + 300; + } else if ( gp.axes[0] > .3) { + video.currentTime += 10; + controlTimeout = Date.now() + 200; + } else if ( gp.axes[0] < -.3) { + video.currentTime -= 10; + controlTimeout = Date.now() + 200; + } else if ( gp.axes[1] > .3) { + video.currentTime -= 60; + controlTimeout = Date.now() + 300; + } else if ( gp.axes[1] < -.3) { + video.currentTime += 60; + controlTimeout = Date.now() + 300; + } + } + + // mpuevent = new EventSource("http://localhost:314"); + // var x = [], y = [], z = [], cnt = -1, inertia = 6; + + // mpuevent.addEventListener("bearing", function(e) { + // bearing = e.data.split(" "); + // yaw = -parseFloat(bearing[0]); + // }, false); + // mpuevent.addEventListener("motion", function(e) { + // motion = e.data.split(" "); + + // cnt = (cnt + 1) % inertia; + // x[cnt] = parseFloat(motion[0]); + // y[cnt] = parseFloat(motion[1]); + // z[cnt] = parseFloat(motion[2]); + + // // tx = 0; x.forEach( function(n, i){ tx += n; } ); tx /= inertia; + // ty = 0; y.forEach( function(n, i){ ty += n; } ); ty /= inertia; + // tz = 0; z.forEach( function(n, i){ tz += n; } ); tz /= inertia; + + // pitch = Math.asin((tz / 9.81 > 1)?1:(tz/9.81)) / Math.PI * 180 + 22.5; + // roll = - Math.asin((ty / 9.81 > 1)?1:(ty/9.81)) / Math.PI * 180; + // // yaw = (yaw + ty) % 360; + // }, false ); + + window.addEventListener("devicemotion", (function() { + let x = [], y = [], z = [], cnt = -1, inertia = 6; + return event => { + cnt = (cnt + 1) % inertia; + + x[cnt] = event.accelerationIncludingGravity.x; + y[cnt] = event.accelerationIncludingGravity.y; + z[cnt] = event.accelerationIncludingGravity.z; + + tx = 0; x.forEach( function(n, i){ tx += n; } ); tx /= inertia; + ty = 0; y.forEach( function(n, i){ ty += n; } ); ty /= inertia; + tz = 0; z.forEach( function(n, i){ tz += n; } ); tz /= inertia; + + pitch = Math.asin((tz / 9.81 > 1)?1:(tz/9.81)) / Math.PI * 180; + roll = - Math.asin((ty / 9.81 > 1)?1:(ty/9.81)) / Math.PI * 180; + yaw = pitch ? ( yaw + - event.rotationRate.alpha / 1000 * event.interval * Math.cos(pitch * Math.PI / 180) + - event.rotationRate.gamma / 1000 * event.interval * Math.sin(pitch * Math.PI / 180) + ) % 360 : yaw; + if ( yaw > 180) yaw -= 360; + if ( yaw < -180) yaw += 360; + + // pitch = (pitch + event.rotationRate.beta / 1000 * event.interval) % 360; + // roll = (roll + event.rotationRate.gamma / 1000 * event.interval) % 360; + // yaw = (yaw + ty) % 360; + }; + })()); + + window.addEventListener("click", function(event) { + ( contLeft.parentElement)? contLeft.parentElement.removeChild(contLeft ):{}; + (contRight.parentElement)?contRight.parentElement.removeChild(contRight):{}; + // video.style.display = "block"; + video.pause(); + }, true); + + dist = dist ? dist : parseInt(document.getElementById("StereoDist").getAttribute("value")); + fov = fov ? fov : parseInt(document.getElementById("StereoFOV" ).getAttribute("value")); + + video.play(); + // video.style.display = "none"; + draw(); +}; diff --git a/style.css b/style.css new file mode 100644 index 0000000..79ddfe8 --- /dev/null +++ b/style.css @@ -0,0 +1,301 @@ +body { + color: #EEE; + background-color: #000; + padding-bottom: 2.5em; +} + +/* ====== TOP CONTROL BAR ====== */ + +#navigation { + text-align: center; + margin-bottom: 1em; padding: 0 2em; + background-color: #333; + box-shadow: .125em .125em .25em #000; +} +#navigation > a { + position: absolute; bottom: .25em; + padding: 0 .125em; + font-size: 1.5em; + text-decoration: none; +} +#navigation > a[href="#bookmarks"] { left: 0; } +#navigation > a[href="#prefs"] { right: 0; } + +#bookmarks, #advsearch, #prefs, #multitag { + -display: none; + height: 0; + overflow: hidden; +} + +#editing { + position: fixed; + bottom: 0; width: 100%; + padding: 0 .5em; + background-color: #333; +} + +:target a[href="#"] { + position: absolute; + top: 0; right: 0; + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + padding: 0 .25em; + z-index: 1; +} + +/* ====== MAIN LIST VIEW ====== */ + +.itemlist { text-align: center; } +.itemlist > * { text-align: left; } +.itemlist .list { + display: inline-block; + vertical-align: top; + width: 99%; + -padding: 0 .25em; + margin: 0 .5%; + margin-bottom: 1em; + overflow: hidden; +} + +.itemlist .list img { + -width: 1000%; height: 11em; + max-width: unset; + background-color: #111; + object-fit: cover; + transform: translate(-05%, 0); + margin-left: 50%; +} +.itemlist .list:hover img { + animation: thumbscroll 8s steps(10, end) infinite; +} +@keyframes thumbscroll { + from { transform: translate(-05%, 0);} + to { transform: translate(-105%, 0);} +} + +.itemlist .list label { + display: block; + font-weight: bolder; + word-break: break-word; +} +.itemlist .list .time, +.itemlist .list .dim { + position: absolute; top: 9.75em; + background-color: rgba(0,0,0,.5); + padding: .125em .25em; +} +.itemlist .list .time { right: 0; } +.itemlist .list .dim { left: 0; } +.itemlist .list input[type=checkbox] { display: none; } +.itemlist .list .tag, +.itemlist .list input[type=checkbox] + label { + display: inline-block; + background-color: #333; + margin-top: .125em; + margin-left: 0; + padding: 0 .25em; + border-radius: 1pt; +} +.itemlist .list input[type=checkbox]:checked + label { + background-color: #383; +} + + +/* ====== PAGINATION LIST ====== */ + +.pagination { + display: block; + font-size: 1.25em; + max-width: 98%; + margin: 0 auto; + padding: .25em 0; + background-color: #333; + border-radius: 2pt; +} +.pagination a { + display: inline-block; + padding: 0 .5em; + margin: 0 .5em; + border-radius: 2pt; +} +.pagination a.current { + background-color: #BBB; +} + +/* ====== BOOKMARK PANEL ====== */ + +#bookmarks:target, +#prefs:target { + display: block; position: fixed; + top: 50%; left: 50%; + transform: translate( -50%, -50% ); + width: 40em; max-width: 90%; + height: 30em; max-height: 90vh; + background-color: #333; + padding: 0 .5em; + z-index: 1; + box-shadow: .25em .25em .5em #000; + overflow-y: auto; +} + +#bookmarks label { + display: inline; + font-weight: bold; + font-size: 1.125em; + margin-left: 0; + margin-top: .75em; + word-break: break-word; +} +#bookmarks label:before, +#bookmarks a.conjunct:after { + content: '\0a'; + white-space: pre; +} +#bookmarks label:before { + line-height: 2.5em; + vertical-align: top; +} + + +/* ====== ADVSEARCH / FILTER PANEL ====== */ + +#advsearch:target { + display: block; position: fixed; + top: 0; width: 100%; + height: 30em; max-height: 90vh; + background-color: #333; + padding: 0 .5em; + z-index: 1; + box-shadow: .25em .25em .5em #000; + overflow-y: auto; +} + +-#advsearch { text-align: center; } +-#advsearch > * { text-align: left; } + +#advsearch .help { + width: 95%; + margin: 1em auto; padding: 0 .5em; + background-color: #444; + white-space: pre-line; +} + +#advsearch input.and + label { + display: inline-block; + vertical-align: top; + font-weight: bold; +} +#advsearch fieldset.select { + display: inline-block; + width: 99%; + margin: 0 .5%; margin-bottom: .75em; padding: 0 .375em; + box-shadow: .125em .125em .25em #000; +} + +#advsearch fieldset.select > label.head { + display: none; + width: 40%; + text-align: right; +} +#advsearch fieldset.select > input.cat { display: none; } +#advsearch fieldset.select > input.cat + label + .catselect { display: none; } +#advsearch fieldset.select > input.cat + label { + display: block; + width: 40%; + margin: 0; padding: 0 .5em; + text-align: right; +} +#advsearch fieldset.select > input.cat:checked + label { background-color: #444; } +#advsearch fieldset.select > input.cat:checked + label + .catselect { + display: block; position: absolute; + top: 1.5em; bottom: 0; right: 0; + width: 60%; + padding: 0 .25em; + background-color: #444; + overflow-y: auto; +} +#advsearch fieldset.select > input.cat + label + .catselect > * { + display: block; + white-space: pre; +} + +#advsearch input.and { display: none; } +#advsearch input.and + label { display: none; } +#advsearch input.and + label + fieldset { display: none; } +#advsearch input.and:checked + label + fieldset, +#advsearch input.and:first-of-type + label + fieldset { display: inline-block; } +#advsearch input.and:checked + label + fieldset + input + label, +#advsearch input.and:first-of-type + label + fieldset + input + label { display: inline-block; } +#advsearch input.and:checked + label + fieldset + input:checked + label { display: none; } +#advsearch input.and:first-of-type + label + fieldset + input:checked + label { display: none; } + + +/* ====== MULTITAG DIALOG ====== */ + +#multitag:target { + display: block; position: fixed; + bottom: 0; left: 0; width: 100%; + height: 30em; max-height: 90vh; + background-color: #333; + padding: 0 .5em; + z-index: 1; + box-shadow: .25em .25em .5em #000; + overflow-y: auto; +} + +-#multitag { text-align: center; } +-#multitag > * { text-align: left; } + +#multitag fieldset { + display: inline-block; + width: 99%; + margin: 0 .5%; margin-top: 1em; +} + +#multitag fieldset select { + width: 100%; height: 10em; +} +#multitag fieldset .tagselect { + height: 10em; + background-color: #444; + overflow-y: auto; +} +#multitag fieldset .tagselect > label { + display: block; + white-space: pre; +} +#multitag fieldset textarea[name=newtag] + label { + display: block; +} + + +/* ====== VIEW PAGE ====== */ + +body#view video { + display: block; + max-height: 80vh; + margin: 0 auto; +} + +body#view .tag { + display: inline-block; + background-color: #333; + margin-top: .125em; + margin-left: 0; + padding: 0 .25em; + border-radius: 1pt; +} + +body#view .itemlist { + margin-top: 2em; +} + + +/* ====== SCALE BLOCK ELEMENTS ====== */ + +@media(min-width: 20em) { .itemlist .list, #advsearch fieldset.select, #multitag fieldset { max-width: 49%; } } +@media(min-width: 40em) { .itemlist .list, #advsearch fieldset.select, #multitag fieldset { max-width: 32%; } } +@media(min-width: 60em) { .itemlist .list, #advsearch fieldset.select, #multitag fieldset { max-width: 24%; } } +@media(min-width: 80em) { .itemlist .list, #advsearch fieldset.select, #multitag fieldset { max-width: 19%; } } +@media(min-width: 100em) { .itemlist .list, #advsearch fieldset.select, #multitag fieldset { max-width: 19em; } } diff --git a/thumbnail.sh b/thumbnail.sh new file mode 100755 index 0000000..09f5ab0 --- /dev/null +++ b/thumbnail.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +[ -n "$include_thumbnails" ] && return 0 +include_thumbnails="$0" + +gen_thumb(){ + local file="$1" thumb="$2" bgcolor="${3:-#000000}" + local tmp="${TMPDIR:-/tmp}/serve0tmp_$$/" lenght n + + if [ "${file%.part}" = "${file}" -a ! -s "$thumb" -a -s "$file" ] && mkdir "$tmp"; then + length="$( ffprobe -show_entries format=duration "$file" 2>&- )" + length="${length#*duration=}" length="${length%%${BR}*}" length="${length%.*}" + + # ffmpeg -nostdin -y -i "$file" -vf fps=11/$length,scale=320:-2 -frames 10 "$tmp/thumb_%02d.jpg" 2>&- + + for n in 1 2 3 4 5 6 7 8 9 10; do + ffmpeg -nostdin -y -ss "$((n * length / 11))" -i "$file" -frames 1 "$tmp/thumb_$((n - 1)).jpg" 2>&- + done + + montage "$tmp"/thumb_[0-9].jpg \ + -background "$bgcolor" \ + -tile 10x1 -geometry 320x180+0+0 \ + -interlace line -quality 85 "$thumb" + rm -r -- "${tmp}" + fi +} diff --git a/view.sh b/view.sh new file mode 100755 index 0000000..7c73592 --- /dev/null +++ b/view.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +. "$_EXEC/db_meta.sh" +. "$_EXEC/widgets.sh" + +read_meta "${ITEM%.*}" + +printf 'Content-Type: text/html;charset=utf-8\r\n\r\n' + +{ cat <<-EOF + [!DOCTYPE HTML] + [html [head + [title . $(HTML "${ITEM##*/}")] + [meta name="viewport" content="width=device-width"] + [link rel=stylesheet href="/cgilite/common.css" ] + [link rel=stylesheet href="/style.css" ] + ] [body #view + [script type="text/javascript" src="/stereoview.js"\n] + [div #navigation + [a #t_bookmarks href="#bookmarks" ★] + $(w_search) + [a #t_prefs href="#prefs" ⚙] + ] + EOF + w_bookmarks + w_advsearch + w_prefs + cat <<-EOF + [input type=hidden id=StereoFOV name=StereoFOV value="$(COOKIE StereoFOV |grep -xE '[0-9]+' || printf 90)"] + [input type=hidden id=StereoDist name=StereoDist value="$(COOKIE StereoDist |grep -xE '[0-9]+' || printf 32)"] + [video #mainvideo controls="controls" preload="auto" [source src="?a=download" type="video/mp4"]] + [a "?a=download" Download] + [label Stereoscopic View:] + [a "javascript:stereoview('sbs180', document.getElementById("mainvideo"));" SBS 180°] + [a "javascript:stereoview( 'tb360', document.getElementById("mainvideo"));" Top/Bottom 360°] + [a "javascript:stereoview( 'cu360', document.getElementById("mainvideo"));" Cubic 360°] + [h1 . $(HTML "${ITEM##*/}" |sed -E 's;[^0-9a-zA-Z&#];&[wbr];g')] + [span .time $((META_LENGTH / 60)):$(printf %02i $((META_LENGTH % 60)))min] [span .dim ${META_WIDTH}x${META_HEIGHT}] + EOF + printf %s\\n "$META_TAGS" |tr , \\n |while read tag; do + [ "$tag" ] && printf ' [span .tag . %s]\n' "$(HTML "${tag#-}")" + done + + if [ "${META_GROUP}" ]; then + printf '[div .itemlist' + list_meta "$_DATA/${ITEM%/*}/.index/meta" \ + | grep -F "${CR} ${META_GROUP}" \ + | sort -n -k8 -k6,6 \ + | while read_meta; do for file in "$_DATA/${META_NAME}".*; do + [ "/${file#${_DATA}/}" = "$ITEM" ] && continue + name="$(HTML "/${file#${_DATA}/}")" + + printf '[div .list .file + [a href="%s" [img src="%s?a=thumbnail"]][label . %s] + [span .time %i:%02imin] [span .dim %ix%i] %s + ]' \ + "$name" "$name" "${name##/}" \ + "$((META_LENGTH / 60))" "$((META_LENGTH % 60))" \ + "$META_WIDTH" "$META_HEIGHT" \ + "$(printf %s\\n "${META_TAGS}" \ + | sed -r 's;^;,;; s;,+;,;g; s;,$;;; + :X s;,-?([^,]+)(,|$); [span .tag\n \1]\2;; tX;' + )" + done; done + printf ']' + fi + + printf ' + [div #editing + [form method=POST action="/?a=multitag" + [hidden "select" "%s"]' "$(HTML "${ITEM}")" + [ -d "$_DATA/${ITEM%/*}/.index/" ] && w_tagging + printf ' + ] + ] +] ] +'; } | "$_EXEC/cgilite/html-sh.sed" diff --git a/widgets.sh b/widgets.sh new file mode 100755 index 0000000..35058cb --- /dev/null +++ b/widgets.sh @@ -0,0 +1,273 @@ +#!/bin/sh + +[ -n "$include_widgets" ] && return 0 +include_widgets="$0" + +. "$_EXEC/cgilite/storage.sh" +. "$_EXEC/db_meta.sh" + +w_refuri="$(URL "$PATH_INFO")?$(HTML "$QUERY_STRING")" + +w_str_s="$(STRING "$SEARCH")" +w_str_f="$(STRING "$FILTER")" + +taglist(){ + list_meta |sed -E ' + s;^.*\ttags=([^\t]*)\t.*$;\1;; + s;,;\n;g; + ' \ + | sort |uniq -c |sort -rn |sed -E 's;^ *[0-9]+ ;;;' \ + | UNSTRING | HTML \ + | sed -E 's; \;;\n;g; s;\n+;\n;g;' +} +tagorder(){ + printf '*\n%s\n$\n' "$w_tagcategories" \ + | while read -r category; do + printf '%s\n' "$w_tags" \ + | { [ "$category" = '*' ] && grep -avF ':' || grep -awF "${category}"; } \ + | { sed -u 10q; sort; } + done +} + +c_tags="$_DATA/.index/tags.cache"; +if [ ! -s "$c_tags" ]; then + taglist >"$c_tags.$$" + mv "$c_tags.$$" "$c_tags" +fi +w_tags="$(cat "$c_tags")" +w_tagcategories="$( + printf %s "$w_tags" \ + | sed -rn '/:/s;^-?([^:]+):.*$;\1;p' \ + |sort -u +)" +w_tags="$(tagorder)" + +[ "$ORDER" = Name ] && w_coname=checked +[ "$ORDER" = Date ] && w_codate=checked +[ "$ORDER" = Length ] && w_colength=checked +[ "$ORDER" = Group ] && w_cogroup=checked + +w_bmname= +w_bmname(){ + [ "$w_bmname" ] || w_bmname="$( + bm="$_DATA/.index/bookmarks" + name="$(grep -m1 -aF " search=${w_str_s} filter=${w_str_f}${CR}" "$bm" 2>&-)" + + if [ "$name" ]; then + printf '%s' "$name" |cut -f1 |UNSTRING |HTML + else + printf '%s\t%s' "$SEARCH" "$FILTER" \ + | sed -r '/^\t$/{ s;\t;All;; q;} + /.*\t$/{ s;\t$;;; q;} + /^\t.*/{ s;^\t;;; + :x; s;(^|[~^|])([^|^~:]+):;\1;; tx; + s;\^; and ;g; s;\|;,;g; s;~;not ;g; q;}' \ + | HTML + fi + )" + printf '%s' "$w_bmname" +} + +w_bookmarks(){ + local bm="$_DATA/.index/bookmarks" name='' search='' filter='' + [ ! -d "${bm%/*}" ] && return 0 + [ ! -f "$bm" ] && touch "$bm" + + grep -qaF " search=$w_str_s filter=${w_str_f}${CR}" "$bm" && name=Update || name=Add + + cat <<-EOF + [form #bookmarks action=?a=bookmark method=POST + [a href="#" x] + [hidden "ref" "${w_refuri}"] + [hidden "search" "$(HTML "$SEARCH")"][hidden "filter" "$(HTML "$FILTER")"] + [label Name for current page:] + [input name="name" value="$(w_bmname)" placeholder="Name" ] + [button type="submit" . ${name}] + EOF + [ "$name" ] && printf ' [submit "delete" "delete" Delete]' + + sort "$bm" |while read -r name search filter; do + search="${search#search=}" filter="${filter#filter=}" filter="${filter%${CR}}" + [ "$search" = "${w_str_s}" -a "$filter" = "${w_str_f}" ] && continue + + name="$(UNSTRING "$name")"; + search="$(UNSTRING "${search}" |URL)"; + filter="$(UNSTRING "${filter}" |URL)"; + printf ' + [label .link . %s][a .conjunct href="?o=%s&s=%s&f=%s" . +] + [a .link target=blank href="/?o=Name&s=%s&f=%s" by Name] + [a .link target=blank href="/?o=Date&s=%s&f=%s" by Date] + [a .link target=blank href="/?o=Length&s=%s&f=%s" by Length] + [a .link target=blank href="/?o=Group&s=%s&f=%s" by Group] + ' "$(HTML "$name" |sed 's;,\;;&[wbr];g;')" \ + "$ORDER" "${SEARCH:+${SEARCH}} $search" "${FILTER:+${FILTER}^}$filter" \ + "$search" "$filter" "$search" "$filter" \ + "$search" "$filter" "$search" "$filter" + done + printf ']' +} + +w_search(){ + printf ' + [form #search method=GET action=./? + [select name=o size=1 + [option disabled=disabled Order By] + [option value=Name %s Name] + [option value=Date %s Date] + [option value=Length %s Length] + [option value=Group %s Group] + ][input type="search" name=s placeholder=Search value="%s"][button .search type=submit Search] + [a #t_avsearch href="#advsearch" Advanced] + ]' "$w_coname" "$w_codate" "$w_colength" "$w_cogroup" \ + "$(HTML "$SEARCH")" +} + +w_prefs(){ + local tm tf td + + tm=''; [ "$(COOKIE mode)" = browse ] && tm=' ' + tf=''; [ "$(COOKIE fakemp4)" = yes ] && tf=checked + td=''; [ "$(COOKIE downscale)" = yes ] && td=checked + + printf ' + [form #prefs method="POST" action="?a=setprefs" + [a href="#" x] + [hidden "ref" "%s"] + [label for=prefs_ps Pagesize] + [input #prefs_ps type=number name=pagesize value="%i"][br] + [radio "mode" "browse" %s #prefs_modebrowse] [label for=prefs_modebrowse Browse Folders][br] + [radio "mode" "index" %s #prefs_modeindex ] [label for=prefs_modeindex View Full Index][br] + [checkbox "fakemp4" "yes" %s #prefs_fmp4] [label for=prefs_fmp4 Fake .MP4 file type][br] + [checkbox "downscale" "yes" %s #prefs_downscale] [label for=prefs_downscale Prefer downscale to 480p][br] + [submit "index" "update" Force Index Update][br] + [submit "store" "store" Set Cookie] + ]' "${w_refuri}" "${LISTSIZE}" "${tm:+checked=checked}" "${tm:-checked=checked}" "$tf" "$td" +} + +w_index(){ + cat <<-EOF + [form #index method="POST" action="?a=spawnindex" + [hidden "ref" "${w_refuri}"] + [label Set up for Index view: ] + [checkbox "recursive" "yes" #spawn_recursive] [label for=spawn_recursive Include subdirectories] + [submit "spawn" "spawn" Set up] + ] + EOF + return 0 +} + +w_advsearch(){ + local n lbid tag category filter f t d + filter="$(HTML "${FILTER}^")" + + printf '[form #advsearch action=./?a=advsearch method=POST + [a href="#" X] + [p .help Refine the search further by setting additional search tags using the [strong "+and"] button.]' + + for n in 1 2 3 4 5 6 7 8 9 10; do + f="${filter%%^*}"; filter="${filter#*^}" + t=''; [ "$f" -a ! "${f%%~*}" ] && t=" " + lbid="cat_${n}_(none)" + + printf \ + '[input .and type=checkbox name=and id="and_%i" %s][label for="and_%i" +and + ][fieldset .select + [radio "pol_%i" "pos" .pol %s #pol_pos_%i"][label for=pol_pos_%i Any] + [radio "pol_%i" "neg" .pol %s #pol_neg_%i"][label for=pol_neg_%i None] + [label .head Category:]' \ + "$n" "${f:+checked=checked}" "$n" \ + "$n" "${t:-checked=checked}" "$n" "$n" \ + "$n" "${t:+checked=checked}" "$n" "$n" + + f="|${f#\~}|" + printf '%s\n$:\n' "$w_tags" \ + | while read tctag; do + [ "$tctag" ] || continue + category="${tctag#-}"; category="${category%%:*}" + tag="${tctag#-}"; tag="${tag#*:}" + [ "${tag}" = "${tctag#-}" ] \ + && category="*" + + if [ "$category" != "$oldcat" ]; then + [ "$oldcat" ] && printf '\n]' + lbid="cat_${n}_${category}" + + t='' + [ "$category" = '*' -a "${f%%|${category}:*}" ] && t=checked + [ "$category" != '*' -a ! "${f%%|${category}:*}" ] && t=checked + [ "$category" != '*' -a ! "${f%%|-${category}:*}" ] && t=checked + + printf '[radio "cat_%i" "%s" .cat %s id="%s"][label for="%s" %s] + [div .catselect\n' \ + $n "$category" "$t" "$lbid" "$lbid" "$category" + fi + + if [ "$category" = '$' ]; then + tag="${f##*\$:}" tag="${tag%%\|*}" + printf '[input name="tag_%i" value="%s"]' "$n" "$(HTML "$tag")" + else + t=''; [ ! "${f%%*|${tctag}|*}" ] && t=checked + printf '[label [checkbox "tag_%s" "%s" %s] %s]' "$n" "$tctag" "$t" "$tag" + fi + + oldcat="$category" + done + [ "$w_tags" ] && printf '\n]' + printf ']' + done + + printf \ + '[fieldset .submit [select name=order + [option disabled=disabled Order By] + [option value=Name %s Name] + [option value=Date %s Date] + [option value=Length %s Length] + [option value=Group %s Group] + ][button type=submit Apply Filter]] + ]' "$w_coname" "$w_codate" "$w_colength" "$w_cogroup" +} + +w_delete(){ + printf '[a href="#multitag" Add Tags / Remove Tags] + [div #multitag [input type="hidden" name="ref" value="%s"] + [a href="#" X] + [fieldset [legend New:] + [submit "op" "filedelete" Delete Files] + ]]' "$w_refuri" +} + +w_tagging(){ + local tctag oldcat="" category tag + + cat <<-EOF + [a href="#multitag" Add Tags / Remove Tags] + [div #multitag [input type="hidden" name="ref" value="${w_refuri}"] + [a href="#" X] + EOF + + printf '%s\n' "$w_tags" \ + | while read tctag; do + [ "$tctag" ] || continue + category="${tctag#-}"; category="${category%%:*}" + tag="${tctag#-}"; tag="${tag#*:}" + + [ "${tag}" = "${tctag#-}" ] \ + && category="Tags" + + if [ "$category" != "$oldcat" ]; then + [ "$oldcat" ] && printf ']]' + printf '[fieldset [legend %s:][div .tagselect\n' "$category" + fi + printf '[label [checkbox "tag" "%s"] %s]\n' "$tctag" "$tag" + + oldcat="$category" + done + [ "$w_tags" ] && printf ']]' + + cat <<-EOF + [fieldset [legend New:][textarea name=newtag] + [label [checkbox "makegroup" "true"] Join selected into group] + [submit "op" "del" Remove][submit "op" "flip" Flip][submit "op" "add" Add] + ]] + EOF +}