]> git.plutz.net Git - serve0/commitdiff
Merge commit '25d0e33ef0647e0019b18a934100a4f15a773a55' as 'cgilite'
authorPaul Hänsch <paul@plutz.net>
Mon, 6 Apr 2020 14:42:15 +0000 (16:42 +0200)
committerPaul Hänsch <paul@plutz.net>
Mon, 6 Apr 2020 14:42:15 +0000 (16:42 +0200)
19 files changed:
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
Makefile [new file with mode: 0644]
advsearch.sh [new file with mode: 0644]
cgilite/cgilite.sh [moved from cgilite.sh with 100% similarity]
cgilite/file.sh [moved from file.sh with 100% similarity]
cgilite/html-sh.sed [moved from html-sh.sed with 100% similarity]
cgilite/logging.sh [moved from logging.sh with 100% similarity]
cgilite/session.sh [moved from session.sh with 100% similarity]
cgilite/storage.sh [moved from storage.sh with 100% similarity]
index.cgi [new file with mode: 0755]
indexmeta.sh [new file with mode: 0644]
list.sh [new file with mode: 0644]
multitag.sh [new file with mode: 0644]
stereoview.js [new file with mode: 0644]
style.css [new file with mode: 0644]
thumbnail.sh [new file with mode: 0644]
view.sh [new file with mode: 0644]
widgets.sh [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..a01ee28
--- /dev/null
@@ -0,0 +1 @@
+.*.swp
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 100644 (file)
index 0000000..3a2f333
--- /dev/null
@@ -0,0 +1,19 @@
+#!/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; 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}|"
+  done
+  f="${f%[|^]}^"
+done
+f="${f%^}"
+
+REDIRECT "$(URL "${ITEM}")?o=${order}&f=${f}"
similarity index 100%
rename from cgilite.sh
rename to cgilite/cgilite.sh
similarity index 100%
rename from file.sh
rename to cgilite/file.sh
similarity index 100%
rename from html-sh.sed
rename to cgilite/html-sh.sed
similarity index 100%
rename from logging.sh
rename to cgilite/logging.sh
similarity index 100%
rename from session.sh
rename to cgilite/session.sh
similarity index 100%
rename from storage.sh
rename to cgilite/storage.sh
diff --git a/index.cgi b/index.cgi
new file mode 100755 (executable)
index 0000000..a33211d
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,106 @@
+#!/bin/sh
+
+for n in "$@"; do case ${n%%=*} in
+  data) _DATA="${n#data=}";;
+  exec) _exec="${n#exec=}";;
+  noerr) exec 2>&-;;
+esac; done
+
+[ -z "${_EXEC%/}" ] && _EXEC="$(realpath "${0%/*}")" || _EXEC="${_EXEC%/}"
+[ -z "${_DATA%/}" ] && _DATA=. || _DATA="${_DATA%/}"
+
+file_pattern='^.*\.(mov|ts|mpg|mpeg|mp4|m4v|avi|mkv|flv|sfv|wmv|ogm|ogv|webm|iso|rmvb)$'
+
+. "$_EXEC/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 50)"
+ITEM="$(PATH "${PATH_INFO#/}")"
+ACTION="$(GET a)"
+
+case $ACTION in
+  setprefs)
+    SET_COOKIE +$((86400 * 90))  pagesize="$(POST pagesize |grep -m1 -axE '[1-9][0-9]*' || printf 50)"
+    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)"
+  ;;
+esac
+
+if [ "$ITEM" = "/style.css" ]; then
+  . "$_EXEC/cgilite/file.sh"
+  [ -r "$_DATA/$ITEM" ] && FILE "$_DATA/$ITEM" \
+                        || FILE "$_EXEC/style.css"
+elif [ "$ITEM" = "/stereoview.js" ]; then
+  . "$_EXEC/cgilite/file.sh"
+  FILE "$_EXEC/stereoview.js"
+elif [ -f "$_DATA/$ITEM" ]; then
+  case $ACTION in
+    thumbnail)
+      . "$_EXEC/cgilite/file.sh"
+      . "$_EXEC/thumbnail.sh"
+      index="$_DATA/${ITEM%/*}/.index"
+      thumb="$index/${ITEM##*/}"; thumb="${thumb%.*}.jpg"
+      [ -d "$index" -a ! -f "$thumb" ] \
+      && { printf %s "$ITEM" |grep -qE -e "${file_pattern}" ;} \
+      && gen_thumb "$_DATA/$ITEM" "$thumb"
+      FILE "$thumb"
+    ;;
+    delete)
+    ;;
+    download)
+      . "$_EXEC/cgilite/file.sh"
+      fakemp4="$(COOKIE fakemp4)"
+      downscale="$(COOKIE downscale)"
+      downfile="$_DATA/${ITEM%/*}/.transcode/${ITEM%.*}.480p.webm"
+      if [ "$downscale" = yes -a -f "$downfile" ]; then
+        FILE "$downfile" "$([ "$fakemp4" = yes ] && printf 'video/mp4')"
+      else
+        FILE "$_DATA/$ITEM" "$([ "$fakemp4" = yes ] && printf 'video/mp4')"
+      fi
+    ;;
+    *) . "$_EXEC/view.sh"
+  esac
+elif [ -d "$_DATA/$ITEM" ]; then
+  case $ACTION in
+    advsearch)
+      . "$_EXEC/advsearch.sh"
+    ;;
+    spawnindex)
+      if [ "$(POST recursive)" = yes ]; then
+        find "$_DATA/$ITEM" -depth -type d \! -name .index \
+                            -exec mkdir -p '{}'/.index \;
+      else
+        mkdir -p "$_DATA/$ITEM/.index"
+      fi
+      REDIRECT "$(POST ref)"
+    ;;
+    *) . "$_EXEC/list.sh"
+    ;;
+  esac
+else
+  printf 'Status: 404 Not Found\r\nContent-Length 0:\r\n\r\n'
+fi
diff --git a/indexmeta.sh b/indexmeta.sh
new file mode 100644 (file)
index 0000000..2da085f
--- /dev/null
@@ -0,0 +1,107 @@
+#!/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
+  fn="$1"; fn="${fn##*/}"; fn="${fn%.*}"
+  STRING "$fn"; printf '\r'
+}
+
+meta_line() {
+  local video l w h
+  video="$1"
+
+  read l h w <<__EOF
+  $(printf '' \
+    | mplayer -input nodefault-bindings -nosound -vo null -identify -frames 0 "$video" 2>&- \
+    | sort | sed -rn '
+      s:ID_LENGTH=(.*)(\..*)$:\1:p;
+      s:ID_VIDEO_HEIGHT=(.*):\1:p;
+      s:ID_VIDEO_WIDTH=(.*):\1:p;' \
+    | tr '\n' ' '
+  )
+__EOF
+  printf '%i\t%i\t%i\ttags=\tcomment=\t%s\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}" "$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}" "$meta" >"${meta}.tmp"
+    grep -aF " ${name}" "$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")" "$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' "$(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 "$dir" -type f -mindepth 1 -maxdepth 1 \
+    | grep -aE "$file_pattern" \
+    | while read -r v; do
+      meta_line "$v"
+    done >"$meta"
+
+    RELEASE "$meta"
+  elif [ -d "$dir/.index" -a "$dir" -nt "$metat" ] && LOCK "$meta"; then
+    cp -p "$meta" "$meta.ref"; touch "$meta"
+    find "$dir" -type f -newer "$metat" \
+         -mindepth 1 -maxdepth 1 \
+    | grep -aE "$file_pattern" \
+    | while read -r v; do
+      grep -qF "       $(meta_name "$v")" "$meta" \
+      || meta_line "$v"
+    done >>"$meta"
+    sort -u "$meta" >"$meta.ref"
+    mv "$meta.ref" "$meta"
+    touch "$metat"
+
+    RELEASE "$meta"
+  fi
+}
diff --git a/list.sh b/list.sh
new file mode 100644 (file)
index 0000000..58eb062
--- /dev/null
+++ b/list.sh
@@ -0,0 +1,237 @@
+#!/bin/sh
+
+. "$_EXEC/indexmeta.sh"
+. "$_EXEC/widgets.sh"
+
+list_item() {
+  local meta type length width height tags comment name display link
+  meta="${1}"; type="${meta%%  *}"; meta="${meta#*     }"
+
+  if [ "$type" = dir ]; then
+    name="${meta%%     *}";
+    display="$(HTML "$name")"; link="$(URL "$ITEM/$name")"
+    printf '[a .list .dir href="%s" %s]' "${link}?${w_refuri#*\?}" "$name"
+    return 0
+  fi
+
+  length="${meta%%     *}"; meta="${meta#*     }"
+  width="${meta%%      *}"; meta="${meta#*     }"
+  height="${meta%%     *}"; meta="${meta#*     }"
+  tags="${meta%%       *}"; meta="${meta#*     }"
+  comment="${meta%%    *}"; meta="${meta#*     }"
+  name="${meta%%       *}"; meta="${meta#*     }"
+
+  if [ "$type" = metashort ]; then
+    name="$(list_fullname "$(UNSTRING "${name%${CR}}")")"
+  fi
+  if [ -f "$_DATA/$ITEM/$name" ]; then
+    link="$(URL "$ITEM/$name")"
+    name="$(HTML "$ITEM/$name")"
+    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##*&#47;}" \
+      "$((length / 60))" "$((length % 60))" \
+      "$width" "$height" \
+      "$(printf '%s\n' "${tags#tags=}" \
+         | sed -r "$UNSTRING"' s;^;,;; s;,+;,;g; s;,$;;;
+                   :X s;,-?([^,]+)(,|$); [span .tag\n \1]\2;; tX;'
+      )" "$name" "$link" "$link"
+  else
+    printf 'Canning record for nonexist file: %s\n' "$name" >&2
+    meta_purge "$_DATA/$ITEM/$name"
+  fi
+}
+
+
+[ "$FILTER" ] && list_fex="$(
+  fex='p'
+  STRING "$FILTER^" \
+  | sed -r 's;\^;\n;g; s;[]\/\(\)\\\^\$\?\.\+\*\;\[\{\}];\\&;g' \
+  | while read -r f; do
+    [ ! "${f#~}" ] && continue
+    [ "${f#~}" = "$f" ] \
+    && fex="/(\ttags=([^\t]*,)?)(${f})((,[^\t]*)?\t)/{${fex}}" \
+    || fex="/(\ttags=([^\t]*,)?)(${f#~})((,[^\t]*)?\t)/d; ${fex}"
+    printf '%s\n' "${fex}"
+  done \
+  | tail -n1
+)"
+
+list_fullname(){
+  sn="$1"
+  [ ! "${sn%%*/*}" ] && base="${sn%/*}" || base=.
+  file="$(printf '%s' "$_DATA/$ITEM/$sn".*)"
+  file="${file##*/}"
+  [ -e "$_DATA/$ITEM/$base/${file}" ] \
+  && printf '%s\n' "${base}/${file}"
+}
+
+list_filter(){
+  if [ "$FILTER" ]; then
+    sed -nr "$list_fex"
+  elif [ "${SEARCH#!}" != "${SEARCH}" ]; then
+    grep -aviEe "$(STRING "${SEARCH}" \
+                 | sed -r ':x s;((^|[^\\])(\\\\)*)\+;\1 ;g; tx;
+                            s;((^|[^\\])(\\\\)*)\\\+;\1+;g;
+                            s; ;\\+;g;')"
+  elif [ "${SEARCH}" ]; then
+    grep -aiEe "$(STRING "${SEARCH}" \
+                 | sed -r ':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 buffer l
+
+  if [ $ORDER = Name ]; then
+    sort -k6 |sed 's;^;metashort\t;;'
+  elif [ $ORDER = Group ]; then
+    sed -E '
+      :X
+      s;^([^\t]+\t[^\t]+\t[^\t]+\t[^\t]+\t[^\t]+\t[^ 0-9]*)(-[0-9a-zA-Z_-]{11}\r|-ph[0-9a-f]{13}\r|-[0-9]{8}\r|[0-9]+\+?)(.*)$;\1\r\3 \2;;
+      tX;' \
+    | { sort -n -k7 |sort -s -k6,6; echo '0 0 0 tags= comment= _'; } \
+    | while read -r length h w t c name; do
+      if [ "${ln%% *}" = "${name%% *}" ]; then
+        al=$((al + length))
+        buffer="${buffer}${BR}$length  $h      $w      $t      $c      $name"
+      else
+       printf '%s\n' "$buffer" |while read -r l; do
+          [ "$l" ] && printf '%s       %s\n' "$al" "$l"
+        done
+        al="$length"
+        buffer="$length        $h      $w      $t      $c      $name"
+      fi
+      ln="$name"
+    done \
+    | sort -s -n -k1 |sed -r 's;^[0-9]+\t;metashort\t;;' \
+    | sed -E ':X s;^([^\t]+\t[^\t]+\t[^\t]+\t[^\t]+\t[^\t]+\t[^\r]*)\r([^ ]*) ([^ ]+)( .*)?$;\1\3\2\4;; tX'
+  elif [ $ORDER = Length ]; then
+    sort -sn -k1 |sed 's;^;metashort\t;;'
+  elif [ $ORDER = Date ]; then
+    while read -r fm; do
+      sn="${fm##*      }"
+      fn="$(list_fullname "$(UNSTRING "${sn%${CR}}")")"
+      printf '%i       %s      %s\n' \
+             "$(stat -c %Y "$fn")" "${fm%      *}" "$fn"
+    done \
+    | sort -srn -k1 |sed -r 's;^[0-9]+\t;metalong\t;;'
+  fi
+}
+
+list_filemeta(){
+  local meta base cbase fm cachename
+  base="$1"
+  meta="$_DATA/$ITEM/$base/.index/meta"
+  meta_dir "$_DATA/$ITEM/$base"
+
+  cachename="$(printf '%s\n' "$mode" "$FILTER" "$SEARCH" "$ORDER" |sha1sum)"
+  cachename="$_DATA/$ITEM/.index/${cachename%  -}.cache"
+
+  if [ "$cachename" -nt "$meta" ] 2>&-; then
+    cat "$cachename"
+  else
+    cbase="$(STRING "$base")"
+    grep -axE '[0-9]+  [0-9]+  [0-9]+  tags=[^ ]*      comment=[^      ]*      .+' "$meta" \
+    | while read -r fm; do
+      printf '%s       %s/%s\n' "${fm% *}" "$cbase" "${fm##*   }"
+    done \
+    | list_filter \
+    | list_order \
+    | { [ -d "${meta%meta}" ] && tee "$cachename" || cat; }
+  fi
+}
+
+list_items() {
+  local mode meta
+  mode="$(COOKIE mode |grep -m1 -axE 'index|browse' || printf index )"
+  
+  if [ "$mode" = browse ]; then
+    [ "$ITEM" ] && printf 'dir\t..\n'
+    (cd "$_DATA/$ITEM";
+      find ./ -type d \! -name .index -mindepth 1 -maxdepth 1 \
+    ) | cut -d/ -f2- | sort |sed 's;^;dir\t;;'
+    list_filemeta .
+  elif [ "$mode" = index ]; then
+    (cd "$_DATA/$ITEM";
+      find ./ -path '*/.index/meta'
+    ) | while read -r meta; do
+      list_filemeta "${meta%/.index/meta}"
+    done
+  fi
+}
+
+list_paginate() {
+  local page i c n end qry
+  page="$(GET p |grep -axE '[0-9]+' || printf 1)"; c=1
+  end=$((page + LISTSIZE))
+  qry="${w_refuri#*\?}"; qry="${qry#&#112;&#61;*&#38;}"
+
+  printf '[div .itemlist '
+  while read -r i; do
+    c=$((c + 1))
+    [ $c -gt $page -a $c -le $end  ] && list_item "$i"
+  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="%s" %s]' "?p=${c}&${qry}" "$n" \
+    || printf '[a .page href="%s" %s]' "?p=${c}&${qry}" "$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="/style.css" ]
+] [body
+  [div #navigation
+    [a #t_bookmarks href="#bookmarks" &#x2605;]'
+    w_search
+    printf '
+    [a #t_avsearch href="#advsearch" Advanced]
+    [a #t_prefs href="#prefs" &#x2699;]
+  ]'
+  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 100644 (file)
index 0000000..353ead6
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/storage.sh"
+. "$_EXEC/indexmeta.sh"
+
+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}"
+
+while [ "${newtags#${BR}}" != "${newtags}" ]; do newtags="${newtags#${BR}}"; done
+while [ "${newtags%${BR}}" != "${newtags}" ]; do newtags="${newtags%${BR}}"; done
+
+[ "$(POST op)" = del ] && deltags="$newtags"
+
+for select in $(seq 1 $(POST_COUNT select)); do
+  file="$_DATA/$ITEM/$(POST select $select |PATH)"
+  meta="${file%/*}/.index/meta"
+
+  read -r length width height tags comment fn <<-EOF
+       $(meta_info "$file")
+       EOF
+  tags="$(UNSTRING "${tags#tags=}" |tr , '\n')"
+
+  if [ ! "$deltags" ]; 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="$(printf '%s\n' "${tags},${newtags}" \
+            | tr , '\n' |sort -u |tr '\n' , \
+            | STRING)"
+  else
+    detag="${deltags}${BR}"; while [ "$detag" ]; do
+      tags="$(printf '%s\n' "$tags" |grep -vxFe "${detag%%${BR}*}")"
+      detag="${detag#*${BR}}"
+    done
+    tags="$(printf '%s\n' "$tags" |sort -u |tr '\n' , |STRING)"
+  fi
+  tags="${tags#,}"; tags="${tags%,}"
+
+  if LOCK "$meta"; then
+    grep -avF "        $fn" "$meta" >"${meta}.tmp"
+
+    printf '%i %i      %i      tags=%s comment=%s      %s\n' \
+           "$length" "$width" "$height" "$tags" \
+           "${comment#comment=}" "$fn" \
+      >>"${meta}.tmp"
+    mv "${meta}.tmp" "$meta"
+
+    RELEASE "$meta"
+  fi
+done
diff --git a/stereoview.js b/stereoview.js
new file mode 100644 (file)
index 0000000..edba378
--- /dev/null
@@ -0,0 +1,147 @@
+/*  Copyright 2018 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 <http://www.gnu.org/licenses/>. 
+*/
+
+var render, video, controlTimeout = 0;
+var pitch = 0, roll = 0, yaw = 0;
+var w, h, hdeg, vdeg, scale, fov = 90;
+var lv = document.createElement("canvas");
+var rv = document.createElement("canvas");
+var debug = document.createElement("p");
+var gp;
+
+function draw() {
+  sw = fov * hdeg |0;
+  sh = h / 2 |0;
+  dh = h / 2 * scale |0;
+
+  if ( layout == "180" ) {
+    sx = (w / 2 - fov * hdeg) / 2 +   yaw * hdeg |0;
+    sy = (    h - fov * vdeg) / 2 + pitch * vdeg |0;
+    if   (sx + sw > w / 2) { sx = w / 2 - sw; } else if (sx < 0) { sx = 0; }
+    lc.drawImage(video,         sx, sy, sw, sh, 0, 0, lv.width, dh);
+    rc.drawImage(video, w / 2 + sx, sy, sw, sh, 0, 0, rv.width, dh);
+  } else {
+    sx = (w - fov * hdeg) / 2 + yaw   * hdeg |0;
+    sy = (h - fov * vdeg) / 4 + pitch * vdeg |0;
+    lc.drawImage(video, sx,       sy, sw, sh, 0, 0, lv.width, dh);
+    rc.drawImage(video, sx, h/2 + sy, sw, sh, 0, 0, rv.width, dh);
+    if (sx < 0) {
+      lc.drawImage(video, sx + w,       sy, sw, sh, 0, 0, lv.width, dh);
+      rc.drawImage(video, sx + w, h/2 + sy, sw, sh, 0, 0, rv.width, dh);
+    } else if ( sx + fov * hdeg > w) {
+      lc.drawImage(video, sx - w,       sy, sw, sh, 0, 0, lv.width, dh);
+      rc.drawImage(video, sx - w, h/2 + sy, sw, sh, 0, 0, rv.width, dh);
+    }
+  }
+
+  lv.style.transform = "rotate(" + roll + "deg)";
+  rv.style.transform = "rotate(" + roll + "deg)";
+
+  requestAnimationFrame(draw);
+
+  gp = navigator.getGamepads()[0];
+  if ( gp && Date.now() > controlTimeout ) {
+    if ( gp.axes[0] >  .3) { video.currentTime += 10; controlTimeout = Date.now() + 500; }
+    if ( gp.axes[0] < -.3) { video.currentTime -= 10; controlTimeout = Date.now() + 500; }
+    if ( gp.axes[1] < -.3) { video.currentTime += 60; controlTimeout = Date.now() + 500; }
+    if ( gp.axes[1] >  .3) { video.currentTime -= 60; controlTimeout = Date.now() + 500; }
+    if ( gp.buttons[0].pressed ) { video.currentTime += 1/30; video.pause(); }
+    if ( gp.buttons[1].pressed ) { video.play(); }
+    if ( gp.buttons[2].pressed ) { fov -= 10; controlTimeout = Date.now() + 500; }
+    if ( gp.buttons[3].pressed ) { fov += 10; controlTimeout = Date.now() + 500; }
+  }
+
+  // debug.textContent = "" + video.currentTime + " " + controlTimeout + " " + tx + " " + ty + " " + tz;
+};
+
+function stereoview(layout, video) {
+  this.layout = layout; this.video = video;
+  document.body.appendChild( lv );
+  document.body.appendChild( rv );
+  document.body.appendChild( debug );
+  
+  lv.setAttribute( "style", "position: fixed; top: 0; left:  0 ; width: 50%; height: 100%; z-index: 100;");
+  rv.setAttribute( "style", "position: fixed; top: 0; left: 50%; width: 50%; height: 100%; z-index: 100;");
+  debug.setAttribute( "style", "position: fixed; top: 0; left: 0; z-index: 101; background: #000;");
+
+  lv.setAttribute( "width", "" + lv.offsetWidth);
+  rv.setAttribute( "width", "" + rv.offsetWidth);
+  lv.setAttribute("height", "" + lv.offsetHeight);
+  rv.setAttribute("height", "" + rv.offsetHeight);
+  
+  lc = lv.getContext("2d");
+  rc = rv.getContext("2d");
+  
+  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() {
+    var x = [], y = [], z = [], cnt = -1, inertia = 6;
+    return function(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 + 22.5;
+      roll  = - Math.asin((ty / 9.81 > 1)?1:(ty/9.81)) / Math.PI * 180;
+      yaw   = (yaw + ty) % 360;
+    };
+  })());
+
+  window.addEventListener("click", function(event) {
+    (lv.parentElement)?lv.parentElement.removeChild(lv):{};
+    (rv.parentElement)?rv.parentElement.removeChild(rv):{};
+    video.style.display = "block";
+    video.pause();
+  }, true);
+
+  w = video.videoWidth; h = video.videoHeight;
+  hdeg = w / 360; vdeg = h / 180;
+  scale = lv.width / (fov * hdeg |0);
+  // scale = lv.width / (w / 4);
+  video.play();
+  video.style.display = "none";
+  draw();
+};
diff --git a/style.css b/style.css
new file mode 100644 (file)
index 0000000..2e461e1
--- /dev/null
+++ b/style.css
@@ -0,0 +1,424 @@
+* {
+  box-sizing: border-box;
+  margin: 0; padding: 0;
+  text-align: left;
+}
+button { padding: .125em .5em; }
+a { color: inherit; text-decoration: none;}
+
+input {
+  border: 1px solid;
+  padding: .25em .5em;
+  line-height: 1em;
+  vertical-align: bottom;
+}
+input[type=number] { padding-right: 0; }
+input[type=radio], input[type=checkbox] { vertical-align: baseline; }
+select {
+  border: 1px solid;
+  padding: .125em .5em;
+}
+select[multiple] { padding: 0; }
+select[multiple] option { padding: .25em .5em; }
+
+body {
+  color: white;
+  background-color: black;
+  min-height: 100%;
+}
+
+
+/* ###  Main Page Elements ### */
+
+#navigation{
+  position: relative;
+  text-align: center;
+  border-bottom: 1px solid;
+  background-color: #333;
+}
+
+#editing,
+#multitag {
+  position: fixed;
+  bottom: 0; width: 100%;
+  padding: .25em 0;
+  border-top: 1px solid;
+  background-color: #333;
+}
+
+#search { display: inline; }
+
+a[href="#prefs"],
+a[href="#bookmarks"] {
+  position: absolute;
+  top: 0;
+  margin: 0 .25em;
+  font-size: 1.5em;
+}
+a[href="#prefs"] { right: 0; }
+a[href="#bookmarks"] { left: 0; }
+
+a[href="#advsearch"] { margin-left: .5em; }
+a[href="#advsearch"]:before {
+  content: '\25b8';
+  margin: 0 .5em;
+}
+
+
+/* ###  Expandable Drawers  ### */
+
+#prefs, #bookmarks, #multitag, #advsearch {
+  display: block; position: absolute;
+  max-height: 0; width: 100%; max-width: 100%;
+  margin-top: -1px;
+  padding: 0 1em;
+  border: 1px none;
+  overflow: hidden;
+  background-color: #333;
+  transition: max-height .3s linear;
+  z-index: 1;
+}
+#bookmarks {
+  left: 0; width: 30%;
+  min-width: 300px;
+  margin-right: auto;
+}
+#prefs {
+  right: 0; width: 20%;
+  min-width: 200px;
+  margin-left: auto;
+}
+#advsearch {
+  text-align: center;
+}
+
+#advsearch:target, #multitag:target,
+#prefs:target, #bookmarks:target {
+  max-height: 25em; overflow-y: scroll;
+}
+#advsearch:target { border-style: none none solid none; }
+#prefs:target { border-style: none none solid solid; }
+#bookmarks:target { border-style: none solid solid none; }
+#multitag:target { border-style: solid none none none; }
+
+#advsearch a[href="#"],
+#multitag a[href="#"],
+#bookmarks a[href="#"],
+#prefs a[href="#"] {
+  display: block;
+  line-height: 2em;
+  font-weight: bold;
+  background-color: inherit;
+}
+-#prefs a[href="#"] { width: 2.5em; left: auto; }
+-#bookmarks a[href="#"] { width: 2.5em; right: auto; }
+#advsearch a[href="#"] { border-bottom: 1px solid; }
+
+
+/* ###  Preferences Drawer  ### */
+
+#prefs label[for=prefs_ps] {
+  font-weight: bold;
+}
+#prefs #prefs_ps {
+  max-width: 4em;
+  margin-bottom: 1em;
+}
+#prefs button {
+  margin: 1em 0;
+}
+#prefs input { vertical-align: top; }
+#prefs input[type=radio] + label,
+#prefs input[type=checkbox] + label {
+  display: inline-block;
+  margin-bottom: .5em;
+  max-width: 85%;
+}
+
+
+/* ###  Bookmarks Drawer  ### */
+
+#bookmarks input,
+#bookmarks button {
+  margin-bottom: 1.25em;
+}
+#bookmarks label {
+  display: block;
+  font-weight: bold;
+  word-break: break-word;
+  overflow: hidden;
+}
+#bookmarks a.link {
+  display: inline-block;
+  font-size: .75em;
+  text-decoration: underline;
+  margin: 0 1em 1.25em 0;
+}
+
+
+/* ###  Advance Search Drawer  ###*/
+
+#advsearch .help {
+  display: block;
+  margin: 1em .5em 1em .5em;
+  padding: .5em 1em;
+  background-color: #444;
+  line-height: 1.5em;
+}
+
+#advsearch input.and {display: none;}
+#advsearch input.and + label {display: none}
+#advsearch input.and + label + .select {
+  display: inline-block;
+  position: relative;
+  width: 100%; min-width: 0; max-width: 0;
+  min-height: 12em;
+  overflow: hidden;
+  vertical-align: top;
+  border: none;
+  transition: max-width .3s linear;
+}
+#advsearch .submit {
+  display: inline-block;
+  width: 100%;
+  vertical-align: top;
+}
+
+#advsearch input.and + label:nth-of-type(2),
+#advsearch input.and:checked + label + .select + input + label {
+  display: inline-block;
+  vertical-align: top;
+  margin: 0 .5% 1em .5%;
+  width: 4%; min-width: 4em;
+  padding: .5em 0;
+  text-align: center;
+  font-weight: bold;
+  border: 1px solid;
+}
+
+#advsearch input.and:checked + label + .select + input:checked + label,
+#advsearch input.and:checked + label:nth-of-type(2),
+#advsearch input.and:checked + label { display: none; }
+
+#advsearch input.and + label + .select:first-of-type,
+#advsearch input.and:checked + label + .select {
+  min-width: 200px; max-width: 100%;
+  margin: 0 .5% 1em .5%;
+  border: 1px solid;
+}
+
+@media (min-width: 460px){
+#advsearch .submit,
+#advsearch input.and + label + .select:first-of-type,
+#advsearch input.and:checked + label + .select {
+  width: 49%;
+} }
+@media (min-width: 660px){
+#advsearch .submit,
+#advsearch input.and + label + .select:first-of-type,
+#advsearch input.and:checked + label + .select {
+  width: 32%;
+} }
+@media (min-width: 860px){
+#advsearch .submit,
+#advsearch input.and + label + .select:first-of-type,
+#advsearch input.and:checked + label + .select {
+  width: 24%;
+} }
+@media (min-width: 1060px){
+#advsearch .submit,
+#advsearch input.and + label + .select:first-of-type,
+#advsearch input.and:checked + label + .select {
+  width: 19%;
+} }
+
+#advsearch .submit { min-height: 0; }
+#advsearch .submit * { width: 50%; }
+
+#advsearch .select input.pol {
+  margin: .5em .25em 0 .5em;
+}
+#advsearch .select input.pol + label {
+  font-weight: bold;
+}
+#advsearch .select label.head {
+  display: block;
+  font-weight: bold;
+  padding: .5em 0 0 .5em;
+  border-bottom: 1px solid;
+}
+
+#advsearch .select select {display: none;}
+#advsearch .select input.cat { display: none; }
+#advsearch .select input.cat + label {
+  display: block;
+  margin-right: 50%;
+  padding: .25em .5em;
+  font-size: 1.125em;
+  border-bottom: 1px solid;
+}
+#advsearch .select input.cat:checked + label {
+  background-color: #444;
+}
+#advsearch .select input.cat:checked + label + select {
+  display: block;
+  position: absolute;
+  top: 3.5em; bottom: 0;
+  right: 0; left: 50%;
+  width: 50%;
+}
+
+
+/* ###  Item Listing  ### */
+
+.list {
+  position: relative;
+  display: inline-block;
+  width: 100%;
+  padding: .375em;
+  vertical-align: top;
+  overflow: hidden;
+}
+
+.list.dir { padding: .5em 1em; }
+.list:before {
+  position: absolute;
+  top: .25em; left: .25em;
+  bottom: .25em; right: .25em;
+  content: '';
+  z-index: -2;
+}
+.list.dir:before { background-color: #CCF; color: black; }
+.list.file:before { background-color: #333; }
+.list.file:first-of-type { clear: left; }
+
+.list.file a img{
+  display: block;
+  width: 100%;
+  min-height: 4em;
+  border-bottom: 1px solid black;
+}
+.list.file a + label{
+  display: inline-block;
+  width: 100%; max-height: 2.5em;
+  margin-right: -10em;
+  padding: .25em .5em;
+  word-break: break-word;
+  background-color: #222;
+  overflow: hidden;
+}
+
+.list.file .time, .list.file .dim {
+  float: right; position: relative;
+  display: inline-block;
+  top: -1.5em; bottom: 1.5;
+  margin-right: .125em;
+  padding: .125em .25em;
+  background-color: rgba(0,0,0,.75);
+}
+
+.list.file .tag,
+.list.file input + label {
+  display: inline-block;
+  margin: .125em -.125em 0 0;
+  padding: .125em .5em;
+  color: black;
+  background-color: #DCC;
+}
+
+.list.file input[type="checkbox"] { display: none; }
+.list.file input[type="checkbox"] + label {
+  border: 1px solid;
+  background-color: #ECC;
+}
+.list.file input[type="checkbox"]:checked + label {
+  background-color: #8F8;
+}
+
+.itemlist,
+.pagination {
+  display: block;
+  text-align: center;
+  margin-top: 1em;
+}
+.pagination { margin-bottom: 3em;}
+.page {
+  display: inline-block;
+  margin: 0 .125em;
+  padding: .25em .5em;
+  color: #DAA;
+  background-color: #000;
+  border: 1px solid;
+}
+.page.current {
+  font-weight: bold;
+  color: black;
+  background-color: #FDD;
+  border-color: #DAA;
+}
+
+#index label:first-of-type { font-weight: bold; }
+#index input, #index button { margin-left: 1em;}
+
+
+/* ###  Video View  ### */
+
+body#view { padding-bottom: 6em; }
+
+#view h1 {
+  max-width: 100%;
+  margin: .5em .75em;
+  font-size: 1.25em;
+  font-weight: bold;
+  word-break: break-word;
+  text-decoration: none;
+}
+
+#view #mainvideo {
+  display: block;
+  width: 98%;
+  margin-left: auto; margin-right: auto;
+  max-height: 240px;
+}
+@media(min-height: 400px) { #view #mainvideo { max-height: 320px; } }
+@media(min-height: 480px) { #view #mainvideo { max-height: 460px; } }
+@media(min-height: 520px) { #view #mainvideo { max-height: 480px; } }
+@media(min-height: 640px) { #view #mainvideo { max-height: 600px; } }
+@media(min-height: 700px) { #view #mainvideo { max-height: 640px; } }
+@media(min-height: 760px) { #view #mainvideo { max-height: 720px; } }
+@media(min-height: 1000px){ #view #mainvideo { max-height: 960px; } }
+@media(min-height: 1200px){ #view #mainvideo { max-height: 1080px;} }
+
+#view a[href$=download],
+#view a[href^=javascript] {
+  margin-left: 1em;
+}
+
+#view .time, #view .dim,
+#view .tag {
+  margin-right: -.125em;
+  padding: .125em .25em;
+  color: black;
+}
+#view .time,
+#view .dim { background-color: #CCD; }
+#view .tag { background-color: #DCC; }
+
+
+
+/* ###  Multi Tagging Drawer  ### */
+
+#multitag fieldset{
+  display: inline-block;
+  width: 100%;
+  vertical-align: top;
+  padding: 0 .25em;
+  border: none;
+}
+
+@media (min-width: 520px) { .list, #multitag fieldset { width: 50%; min-width: 250px; } }
+@media (min-width: 760px) { .list, #multitag fieldset { width: 33%; } }
+@media (min-width: 1020px){ .list, #multitag fieldset { width: 25%; } }
+
+#multitag fieldset * { width: 100%; }
+#multitag fieldset button { width: 50%; }
diff --git a/thumbnail.sh b/thumbnail.sh
new file mode 100644 (file)
index 0000000..1430776
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+[ -n "$include_thumbnails" ] && return 0
+include_thumbnails="$0"
+
+gen_thumb(){
+  file="$1"; thumb="$2";
+
+  if [ ! -e "$thumb" ] && [ -f "$file" ]; then
+    l="$(
+      printf '' \
+      | mplayer -input nodefault-bindings -nosound -vo null -identify -frames 0 "$file" 2>&- \
+      | sed -rn 's:ID_LENGTH=(.*)(\..*)$:\1:p;' \
+    )"
+  
+    chunk="$((${l:-10} / 5))"
+    
+    tmp="$(mktemp -d)"
+    for cnt in 1 2 3 4; do
+      printf '' \
+      | mplayer -input nodefault-bindings -nosound -benchmark \
+                -noconfig all -really-quiet \
+                -frames 1 -ss "$((cnt * chunk))" \
+                -vf framestep=I \
+                -vo jpeg:quality=100:outdir="${tmp}" \
+                "$file" 2>&-
+      mv "${tmp}/00000001.jpg" "${tmp}/_${cnt}.jpg"
+    done
+    printf '' \
+    | mplayer -input nodefault-bindings -nosound -benchmark \
+              -noconfig all -really-quiet \
+              -vf scale=159:-2,tile=2:2:4:0:2 \
+              -vo jpeg:quality=96:outdir="${tmp}"\
+              "mf://$tmp/_*.jpg" 2>&-
+  
+    mv "${tmp}/00000001.jpg" "$thumb"
+    rm -r "${tmp}"
+  
+  elif [ ! -e "$thumb" ]; then
+    touch "$thumb"
+  fi
+}
diff --git a/view.sh b/view.sh
new file mode 100644 (file)
index 0000000..4ee5fb5
--- /dev/null
+++ b/view.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+. "$_EXEC/indexmeta.sh"
+. "$_EXEC/widgets.sh"
+
+read length width height tags comment short <<-EOF
+       $(meta_info "$_DATA/$ITEM")
+       EOF
+
+printf 'Content-Type: text/html;charset=utf-8\r\n\r\n'
+
+{ printf '
+[!DOCTYPE HTML]
+[html [head [title \n'
+  HTML "${ITEM##*/}"
+  printf ']
+  [meta name="viewport" content="width=device-width"]
+  [link rel=stylesheet href="/style.css" ]
+] [body #view
+  [script type="text/javascript" src="/stereoview.js"\n]
+  [div #navigation
+    [a #t_bookmarks href="#bookmarks" &#x2605;]'
+    w_search
+    printf '
+    [a #t_avsearch href="#advsearch" Advanced]
+    [a #t_prefs href="#prefs" &#x2699;]
+  ]'
+  w_bookmarks
+  w_advsearch
+  w_prefs
+  printf '
+  [video #mainvideo controls="controls" [source src="?a=download" type="video/mp4"]]
+  [a "?a=download" Download]
+  [a "javascript:stereoview(180, document.getElementById(&#34;mainvideo&#34;));" View 180° Stereoscopic]
+  [a "javascript:stereoview(360, document.getElementById(&#34;mainvideo&#34;));" View 360° Stereoscopic]
+  [h1\n %s]
+  [span .time %i:%02imin] [span .dim %ix%i] %s
+  ' "$(HTML "${ITEM##*/}" |sed -r "$w_ascii"' s;[^0-9a-zA-Z&#];&[wbr];g')" \
+    "$((length / 60))" "$((length % 60))" "$width" "$height" \
+    "$(printf '%s\n' "${tags#tags=}" |sed -r "$UNSTRING"'
+       s;^;,;; s;,+;,;g; s;,$;;; :X s;,-?([^,]+)(,|$); [span .tag\n \1]\2;; tX;'
+    )"
+  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 100644 (file)
index 0000000..7c945d6
--- /dev/null
@@ -0,0 +1,283 @@
+#!/bin/sh
+
+[ -n "$include_widgets" ] && return 0
+include_widgets="$0"
+
+. "$_EXEC/cgilite/storage.sh"
+
+w_refuri="$(URL "$PATH_INFO")?$(HTML "$QUERY_STRING")"
+
+w_str_s="$(STRING "$SEARCH")"
+w_str_f="$(STRING "$FILTER")"
+
+w_ascii='
+  s-&#48;-0-g; s-&#49;-1-g; s-&#50;-2-g; s-&#51;-3-g; s-&#52;-4-g; s-&#53;-5-g;
+  s-&#54;-6-g; s-&#55;-7-g; s-&#56;-8-g; s-&#57;-9-g;
+  s-&#65;-A-g; s-&#66;-B-g; s-&#67;-C-g; s-&#68;-D-g; s-&#69;-E-g; s-&#70;-F-g;
+  s-&#71;-G-g; s-&#72;-H-g; s-&#73;-I-g; s-&#74;-J-g; s-&#75;-K-g; s-&#76;-L-g;
+  s-&#77;-M-g; s-&#78;-N-g; s-&#79;-O-g; s-&#80;-P-g; s-&#81;-Q-g; s-&#82;-R-g;
+  s-&#83;-S-g; s-&#84;-T-g; s-&#85;-U-g; s-&#86;-V-g; s-&#87;-W-g; s-&#88;-X-g;
+  s-&#89;-Y-g; s-&#90;-Z-g;
+  s-&#97;-a-g; s-&#98;-b-g; s-&#99;-c-g; s-&#100;-d-g; s-&#101;-e-g; s-&#102;-f-g;
+  s-&#103;-g-g; s-&#104;-h-g; s-&#105;-i-g; s-&#106;-j-g; s-&#107;-k-g; s-&#108;-l-g;
+  s-&#109;-m-g; s-&#110;-n-g; s-&#111;-o-g; s-&#112;-p-g; s-&#113;-q-g; s-&#114;-r-g;
+  s-&#115;-s-g; s-&#116;-t-g; s-&#117;-u-g; s-&#118;-v-g; s-&#119;-w-g; s-&#120;-x-g;
+  s-&#121;-y-g; s-&#122;-z-g;
+  s-&#45;-\--g; s-&#47;-/-g; s-&#47;-/-g; s-&#58;-:-g; s-&#61;-=-g; s-&#64;-@-g;
+  s-&#95;-_-g; s-&#126;-~-g;              s-&#32;- -g; s-&#94;-^-g; s-&#124;-|-g;
+'
+
+w_tags="$_DATA/.index/tags.cache"; w_tagcategories="$_DATA/.index/tagcategories.cache"
+if [ ! -f "$w_tags" -o ! -f "$w_tagcategories" ] \
+   || [ "$(find "$_DATA/" -path '*/.index/meta' -newer "$w_tags")" ]
+  then
+  w_tags="$( { local cn=1
+    find "$_DATA/" -path '*/.index/meta' -print0 \
+    | xargs -r0 sed -r '
+      s;^.*\t.*\t.*\ttags=(.*)\tcomment=.*\t.*\r$;\1;;
+      s;,;\n;g;'"$UNSTRING" \
+    | { sort; printf '\n'; } \
+    | while read -r tag; do
+      [ "$tag" = "$otag" ] \
+      && cn=$((cn + 1)) \
+      || {
+        printf "%i     %s\n" "$cn" "$otag"
+        cn=1
+      }
+      otag="$tag"
+    done
+  } |sort -rn |cut -f2- |HTML |sed "$w_ascii s-&#10;-\n-g; s;\n\n;\n;g;" |tee "$w_tags" )"
+  w_tagcategories="$(printf %s "$w_tags" \
+                     | sed -rn '/:/s;^-?([^:]+):.*$;\1;p' |sort -u \
+                     | tee "$w_tagcategories" )"
+else
+  w_tags="$(cat "$w_tags")"
+  w_tagcategories="$(cat "$w_tagcategories")"
+fi
+
+
+[ "$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
+
+  printf '[form #bookmarks action=?a=bookmark method=POST
+            [a href="#" x]
+            [hidden "ref" "%s"]
+            [hidden "search" "%s"][hidden "filter" "%s"]
+            [label Name for current page:]
+            [input name="name" value="%s" placeholder="Name" ]
+            [button type="submit" %s]' \
+            "$w_refuri" \
+            "$(HTML "$SEARCH")" "$(HTML "$FILTER")" \
+            "$(w_bmname)" "${name}"
+  [ "$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 .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]
+            [br]' \
+            "$(HTML "$name" |sed 's;&#44\;;&[wbr];g;')" \
+            "$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 name=s placeholder=Search value="%s"]
+  ]
+  ' \
+  "$w_coname" "$w_codate" "$w_colength" "$w_cogroup" \
+  "$(HTML "$SEARCH")"
+}
+
+w_prefs(){
+  local tm tf td
+
+  tm=''; [ "$(COOKIE mode)" = index ] && 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="%s"][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}" "${tm:+checked}" "$tf" "$td"
+}
+
+w_index(){
+  printf '
+  [form #index method="POST" action="?a=spawnindex"
+    [hidden "ref" "%s"]
+    [label Set up for Index view: ]
+    [checkbox "recursive" "yes" #spawn_recursive] [label for=spawn_recursive Include subdirectories]
+    [submit "spawn" "spawn" Set up]
+  ]
+  ' "$w_refuri"
+  return 0
+}
+
+w_advsearch(){
+  local n lbid tag category filter f t d
+  filter="$(HTML "${FILTER}^" |sed "$w_ascii")"
+
+  printf '[form #advsearch action=?a=advsearch method=POST
+            [a href="#" Hide]
+            [p .help Select multiple tags from each category by holding down the [strong Ctrl] key on your keyboard.[br]
+            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}" $n \
+            $n "${t:-checked}" $n $n \
+            $n "${t:+checked}" $n $n
+
+    f="|${f#~}|"
+    printf '*\n%s\n' "$w_tagcategories" \
+    | while read -r category; do
+      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]
+              [select name=tag_%s size=10 multiple' \
+              $n "$category" "$t" "$lbid" "$lbid" "$category" $n
+
+      printf '%s\n' "$w_tags" \
+      | { [ "$category" = '*' ] && grep -avF ':' || grep -awF "${category}"; } \
+      | { for n in 1 2 3 4 5 6 7 8 9 0; do
+          read -r line && printf '%s\n' "$line" || break;
+          done;  # pass 10 lines through without modification
+          sort;  # and sort remaining lines
+      } | while read -r tag; do
+        [ "$tag" ] || continue
+        t=''; [ ! "${f%%*|${tag}|*}" ] && t=checked
+        d="${tag#-}"; d="${d#*:}"
+        printf '[option %s value="%s"\n%s]' "$t" "$tag" "$d"
+      done
+      printf '\n]'
+    done
+    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="#" Hide][br]
+          [fieldset [legend New:]
+          [submit "op" "filedelete" Delete Files]
+          ]]' "$w_refuri"
+}
+
+w_tagging(){
+  local tag category d
+  printf '[a href="#multitag" Add Tags / Remove Tags]
+          [div #multitag [input type="hidden" name="ref" value="%s"]' "$w_refuri"
+  printf '[a href="#" Hide][br]'
+
+  printf 'Tags\n%s\n' "$w_tagcategories" \
+  | while read -r category; do
+    [ "$category" ] || continue
+    printf '[fieldset [legend %s:][select name=tag size=4 multiple\n' "$category"
+    printf %s "$w_tags" \
+    | { [ "$category" = 'Tags' ] && grep -avF ':' || grep -awF "${category}"; } \
+    | { for n in 1 2 3 4 5 6 7 8 9 0; do
+        read -r line && printf '%s\n' "$line" || break
+        done;
+        sort;
+    } | while read -r tag; do
+      [ "$tag" ] || continue
+      d="${tag#-}"; d="${d#*:}"
+      printf '[option value="%s"\n%s]' "$tag" "$d"
+    done
+    printf ']]'
+  done
+
+  printf '[fieldset [legend New:][textarea name=newtag\n]
+          [submit "op" "del" Remove Tags][submit "op" "add" Add Tags]
+          ]]'
+}