]> Git — Sourcephile - julm/camera.git/blob - index.sh
reencode: support a specific file
[julm/camera.git] / index.sh
1 #!/usr/bin/env bash
2 # Copyright: Julien Moutinho <julm+camera@sourcephile.fr>
3 # License: AGPL-3.0-or-later
4 # Usage:
5 # $ mv julm/2023/src/02/IMG_20230228_111645{,.fav}.jpg
6 # $ echo >julm/2023/src/02/IMG_20230228_111645.fav.txt "Some HTML <b>comment</b> about the picture"
7 # $ ./index.sh julm/2023/src/02
8 # $ sensible-browser julm/2023/fav/02/index.html
9 #
10 # NginxConfig:
11 # locations."/julm/perso/camera/" = {
12 # alias = "${root}/julm/perso/camera/";
13 # basicAuthFile = "/run/credentials/nginx.service/autogeree.net.www.julm.perso.camera.htpasswd";
14 # };
15 # # Disable basicAuthFile for by-uuid
16 # locations."~ ^/julm/perso/camera/([0-9]+/[0-9][0-9]/by-uuid/[0-9a-f-]+/.+)$" = {
17 # alias = "${root}/julm/perso/camera/$1";
18 # };
19
20 # shellcheck disable=SC2086
21
22 cd "${0%/*}"
23 set -eu
24 shopt -s nullglob
25
26 ffmpeg_ () { local -; set -x; nice -19 time ffmpeg </dev/null -hide_banner -loglevel warning -stats -y "$@"; }
27 magick_ () { local -; set -x; nice -19 magick "$@"; }
28 mv_ () { local -; mv -i >&2 "$@"; }
29
30 for dir in "$@"; do
31 IFS=/ read -r user year x month x <<<"$dir"
32 test -n "$year" || exec "$0" "$user"/2*/src/*/
33 test -n "$month" || exec "$0" "$user"/"$year"/src/*/
34 pushd "$user/$year" >/dev/null
35 test -d src || { popd; continue; }
36 mkdir -p fav
37 # cp, not ln, because in push.sh --copy-links cannot be used for by-uuid/
38 cp -f --remove-destination -t fav ../../index.css
39 test -d src/"$month" || { popd; continue; }
40 unset creationDoMOld
41 unset creationDateOld
42 mkdir -p tmp/fav/"$month"
43 {
44 genDate=$(date +%s)
45 cat <<EOF
46 <!doctype html>
47 <!-- Generator: https://git.sourcephile.fr/julm/camera.git -->
48 <!-- GenerationDate: $(date -R -d@"$genDate") -->
49 <html>
50
51 <head lang="fr">
52 <meta charset="utf-8">
53 <title>camera/$user/$year/$month</title>
54 <link rel="stylesheet" href="../index.css?$genDate">
55 </head>
56
57 <body>
58
59 <nav class="path">
60 <a href='../../..'>camera</a>/ <a href='../..'>$user</a>/ <a href='..'>$year</a>/
61 EOF
62 for m in {01..12}; do
63 if [ "$m" -eq 01 ]; then printf '{ '; fi
64 if [ "$m" -eq "$month" ]; then
65 printf %s "$month"
66 else
67 printf %s "<a href='../$m'>$m</a>"
68 fi
69 if [ "$m" -eq 12 ]; then printf ' }'; else printf ', '; fi
70 done
71 cat <<EOF
72 </nav>
73
74 <div class="camera">
75 <ul>
76 EOF
77 echo >&2 "$dir: processing favorites in chronological order"
78 mkdir -p fav/"$month"
79 # Time is encoded in filenames like so: {IMG,VID}_YYYYMMDD_hhmmss.*
80 # hence sorting after the underscore (1st field, 5th char).
81 # To support subdirectories, sorting is done on the prefix %f
82 # which is replaced by %p thereafter.
83 (cd src; find -L "$month" -name '*.fav.*' -not -name "*.txt" -printf '%f %p\n') | sort -t/ -k1.5 | cut -f2 -d ' ' |
84 while read -r src; do
85 echo >&2 "$dir: processing $year/src/$src"
86 base=${src%.*}
87 name=${base#*/}
88 id=${name%%.*}
89 creationDate=$(printf %s "${src##*/???_}" | sed -e 's/\(....\)\(..\)\(..\)_\(..\)\(..\)\(..\).*/\1-\2-\3 \4:\5:\6/')
90 creationDoM=$(date +'%A %_d %B' -d "$creationDate")
91 if test "$creationDoM" != "${creationDoMOld-}"; then
92 dom=$(date +'%d' -d "$creationDate")
93 echo "<li id='$dom' class='day'><div>"
94 echo "<a class='day-current' href='#$dom'>$creationDoM</a>"
95 echo "<div>"
96 test ! "${creationDateOld:+set}" ||
97 echo "<a class='day-previous' href='#$(date +'%d' -d "$creationDateOld")'>^</a>"
98 echo "</div>"
99 echo "</div></li>"
100 creationDoMOld=$creationDoM
101 creationDateOld=$creationDate
102 fi
103 mkdir -p fav/"${base%/*}"
104 mkdir -p {tmp,wip}/fav/"${base%/*}"
105 test -e fav/"$base".uuid ||
106 uuidgen --random >fav/"$base".uuid
107 uuid=$(cat fav/"$base".uuid)
108 mkdir -p fav/"$month"/by-uuid/"$uuid"
109 wow=
110 test "${src##*.wow.}" = "${src}" || wow=wow
111 need_orient () {
112 # Slow, but needed to make wide videos span two columns of the CSS grid
113 IFS=' ' read -r width height rotation <<<"$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height:stream_side_data=rotation -of csv=p=0:s='\ ' src/"$src")"
114 case "$rotation" in
115 (90|-90) w=$width; width=$height; height=$w;;
116 esac
117 if [ "$width" -gt "$height" ]
118 then scale="-2:360"; orient="landscape"
119 else scale="360:-2"; orient="portrait"
120 fi
121 }
122 unset dst
123 case "$src" in
124 *.jpg|*.JPG)
125 echo "<li id='$id' class='$wow'><div class='item'>"
126 echo "<picture>"
127 printf "<source type='image/avif' srcset='"
128 for w in {1200,600,300}; do
129 test fav/"$base"."$w"x.avif -nt src/"$src" || {
130 magick_ src/"$src" \
131 -sampling-factor 4:2:0 -colorspace sRGB \
132 -auto-orient -thumbnail "$w"x -unsharp 0x.5 \
133 wip/fav/"$base"."$w"x.avif
134 mv_ -f {wip/,}fav/"$base"."$w"x.avif
135 }
136 printf %s "$name.${w}x.avif ${w}w, "
137 done
138 echo "' />"
139 w=1200
140 test fav/"$base"."$w"x.jpg -nt src/"$src" || {
141 magick_ -define jpeg:size=$((w * 2))x src/"$src" \
142 -sampling-factor 4:2:0 -interlace JPEG -colorspace sRGB \
143 -auto-orient -thumbnail "$w"x -unsharp 0x.5 \
144 wip/fav/"$base"."$w"x.jpg
145 mv_ -f {wip/,}fav/"$base"."$w"x.jpg
146 }
147 echo "<source type='image/jpeg' srcset='$name.${w}x.jpg ${w}w' />"
148 echo "<img src='$name.${w}x.jpg' alt='$creationDate' />"
149 echo "</picture>"
150 dst="$name".1200x.jpg
151 ln -fs -t fav/"$month"/by-uuid/"$uuid" ../../"$dst"
152 ;;
153 *.mp4|*.MOV)
154 need_orient
155 echo "<li id='$id' class='$wow vid-orient-$orient'><div class='item'>"
156 test fav/"$base".avif -nt src/"$src" || {
157 ffmpeg_ -i src/"$src" \
158 -map 0:v \
159 -filter:v format=yuv420p10le,scale="$scale" \
160 -frames:v 1 -crf 33 \
161 wip/fav/"$base".avif
162 mv_ -f {wip/,}fav/"$base".avif
163 }
164 need_opus () {
165 test tmp/fav/"$base".opus -nt src/"$src" || {
166 ffmpeg_ -i src/"$src" \
167 -map 0:a -c:a libopus -b:a 64k -application voip \
168 wip/fav/"$base".opus
169 mv_ -f {wip,tmp}/fav/"$base".opus
170 }
171 }
172 test fav/"$base".360p.av1.webm -nt src/"$src" || {
173 need_opus
174 ffmpeg_ -i src/"$src" -i tmp/fav/"$base".opus \
175 -map 0:v \
176 -filter:v format=yuv420p,scale="$scale" \
177 -c:v libsvtav1 -preset 10 -crf 33 \
178 -svtav1-params input-depth=8:keyint=10s:tune=0:enable-overlays=1:scd=1:scm=0:fast-decode=1 \
179 -map 1:a -c:a copy \
180 wip/fav/"$base".360p.av1.webm
181 mv_ -f {wip/,}fav/"$base".360p.av1.webm
182 }
183 dst="$name".360p.av1.webm
184 ln -fs -t fav/"$month"/by-uuid/"$uuid" ../../"$dst"
185 #test fav/"$base".360p.vp9.webm -nt src/"$src" || {
186 # need_opus
187 # # See https://developers.google.com/media/vp9/settings/vod#recommended_settings
188 # # See https://gist.github.com/mrintrepide/3033c35ee9557e66cff7806f48dbd339
189 # set -- \
190 # -filter:v format=yuv420p10le,scale="$scale" \
191 # -c:v libvpx-vp9 -quality good -crf 20 \
192 # -minrate 138k -b:v 276k -maxrate 400k \
193 # -cpu-used 4 -static-thresh 0 -tile-columns 0 -tile-rows 0 -frame-parallel 0 \
194 # -row-mt 1 -aq-mode 0 -auto-alt-ref 6 -lag-in-frames 25 -enable-tpl 1
195 # test -e tmp/fav/"$base".360p.vp9.webm-0.log || {
196 # # FIXME: how to disable h264 debug log?
197 # ffmpeg 2>/dev/null -i src/"$src" \
198 # -map 0:v "$@" -pass 1 -passlogfile wip/fav/"$base".360p.vp9.webm \
199 # -f null \
200 # /dev/null
201 # mv_ -t tmp/fav/"${base%/*}" wip/fav/"$base".360p.vp9.webm-*.log
202 # }
203 # ffmpeg -i src/"$src" -i tmp/fav/"$base".opus \
204 # -map 0:v "$@" -pass 2 -passlogfile tmp/fav/"$base".360p.vp9.webm \
205 # -map 1:a -c:a copy \
206 # wip/fav/"$base".360p.vp9.webm
207 # mkdir -p fav/"$month"/by-uuid/"$uuid"
208 # mv_ wip/fav/"$base".360p.vp9.webm fav/"$base".360p.vp9.webm
209 # rm -fv tmp/fav/"$base".360p.vp9.webm-*.log
210 #}
211 #test fav/"$base".mp4 -nt src/"$src" || {
212 # ffmpeg -i src/"$src" \
213 # -filter:v format=yuv420p \
214 # -c:v libx264 -preset medium -crf 22 \
215 # -tune zerolatency \
216 # -maxrate 1M -bufsize 2M \
217 # -movflags use_metadata_tags +faststart \
218 # -c:a libopus -base:a 64k -application voip \
219 # -filter:v "scale=iw/2:ih/2" \
220 # wip/fav/"$base".mp4
221 # mv_ wip/fav/"$base".mp4 fav/"$base".mp4
222 #}
223 # class='orient$orient'
224 echo "<video controls preload=none poster='$name.avif'>"
225 # See https://jakearchibald.com/2022/html-codecs-parameter-for-av1/
226 # and ffmpeg -i fav/$month/foo.av1.webm -c:v copy -bsf:v trace_headers -f null /dev/null |&
227 # grep -e seq_profile -e seq_level_idx -e seq_tier -e high_bitdepth -e twelve_bit
228 P=0; LL=01; T=M; DD=08
229 echo "<source type='video/webm; codecs=av01.$P.$LL$T.$DD' src='$name.360p.av1.webm' />"
230 echo "<p><a href='$name.360p.av1.webm'>$name.360p.av1.webm</a></p>"
231 echo "</video>"
232 ;;
233 *.av1.webm)
234 need_orient
235 echo "<li id='$id' class='$wow vid-orient-$orient'><div class='item'>"
236 test fav/"$base".avif -nt src/"$src" || {
237 ffmpeg_ -i src/"$src" \
238 -map 0:v \
239 -filter:v format=yuv420p10le,scale="$scale" \
240 -frames:v 1 -crf 33 \
241 wip/fav/"$base".avif
242 mv_ -f {wip/,}fav/"$base".avif
243 }
244 test fav/"$base".webm -nt src/"$src" || {
245 ln -f {src,fav}/"$base".webm
246 }
247 dst="$name".webm
248 ln -fs -t fav/"$month"/by-uuid/"$uuid" ../../"$dst"
249 echo "<video controls preload=none poster='$name.avif'>"
250 # See https://jakearchibald.com/2022/html-codecs-parameter-for-av1/
251 # and ffmpeg -i fav/$month/foo.av1.webm -c:v copy -bsf:v trace_headers -f null /dev/null |&
252 # grep -e seq_profile -e seq_level_idx -e seq_tier -e high_bitdepth -e twelve_bit
253 P=0; LL=01; T=M; DD=08
254 echo "<source type='video/webm; codecs=av01.$P.$LL$T.$DD' src='$name.webm' />"
255 echo "<p><a href='$name.webm'>$name.webm</a></p>"
256 echo "</video>"
257 ;;
258 *)
259 echo >&2 "ERROR: unsupported format: $src"
260 exit 1
261 ;;
262 esac
263 touch -a src/"$base".txt
264 printf %s "<span class='comment'>"
265 sed -e '$q;s/$/<br \/>\n/' src/"$base".txt
266 echo "</span>"
267 echo "</div>" # class='item'
268 echo "<span class='infos'>"
269 echo " <span class='creationDate'>$creationDate</span>"
270 echo "</span>"
271 echo "<span class='links'>"
272 echo " <a class='by-uuid' href='https://autogeree.net/julm/perso/camera/$year/$month/by-uuid/$uuid/${dst##*/}' title='Lien public vers cette capture' target='_blank'>@</a>"
273 echo " <a class='anchor' href='#$id' title='Ancre vers cette capture'>#</a>"
274 echo "</span>"
275 echo "<span class='downloads'>"
276 echo " <a class='download' href='${dst##*/}' title='Enregistrer cette capture'>Enregistrer</a>"
277 echo "</span>"
278 echo "</li>"
279 echo
280 done
281 cat <<EOF
282 </ul>
283 </div>
284
285 </body>
286 </html>
287 EOF
288 } >tmp/fav/"$month"/index.html
289 mv_ -f {tmp/,}fav/"$month"/index.html
290
291 echo >&2 "$dir: removing any deleted favorites"
292 (cd fav; find -L "$month" -mindepth 1 -not -path '*/by-uuid/*' -type f -not -name index.html) |
293 while read -r fav; do
294 base=$fav
295 base=${base%.300x.avif}
296 base=${base%.600x.avif}
297 base=${base%.1200x.avif}
298 base=${base%.1200x.jpg}
299 base=${base%.avif}
300 base=${base%.360p.av1.webm}
301 base=${base%.webm}
302 base=${base%.uuid}
303 name=${base#*/}
304 unset hasSrc
305 for src in src/"$base".???; do hasSrc=set; done
306 test "${hasSrc:+set}" || {
307 echo >&2 "$dir: removing $year/fav/$fav"
308 rm -f fav/"$fav"
309 rm -f fav/"$month"/by-uuid/*/"$name".*
310 }
311 done
312 rmdir -p 2>/dev/null fav/"$month"/by-uuid/*/ || true
313 rmdir -p 2>/dev/null wip/fav/* || true
314 popd >/dev/null
315 done