whiskers.git / web / tools / check-preview
check-preview56 lines · 3.4 KB · raw
1#!/usr/bin/env bash
2# Checks the link preview's files the way a scraper would care about them, without Discord:
3# a still PNG of the declared size, and an MP4 that is H.264 in yuv420p, even-sided, the
4# declared size, seekable from the start (faststart), silent, short and small.
5#
6#     mise run web:check-preview           (or web/tools/check-preview [dir] with the tools on PATH)    (default worker/public/preview)
7#
8# The declared size is read from site/src/config.rs, the one place the page's tags read it from.
9set -euo pipefail
10cd "$(dirname "$0")/.."
11dir="${1:-worker/public/preview}"
12config=site/src/config.rs
13want_w="$(grep -oP 'PREVIEW_WIDTH: u32 = \K[0-9]+' "$config")"
14want_h="$(grep -oP 'PREVIEW_HEIGHT: u32 = \K[0-9]+' "$config")"
15# Discord makes embeds only for files under about 8 MB (MediaCMS discussion 1183); the budget
16# here is half that, so a longer loop has room.
17max_video=4000000
18max_image=1000000
19fail=0
20bad() { echo "check-preview: FAIL: $*" >&2; fail=1; }
21ok() { echo "check-preview: ok: $*"; }
22probe() { ffprobe -v error -select_streams "$1" -show_entries "stream=$2" -of default=nw=1:nk=1 "$3"; }
23
24png="$dir/card.png"; mp4="$dir/loop.mp4"
25[ -f "$png" ] || bad "$png is missing"
26[ -f "$mp4" ] || bad "$mp4 is missing"
27[ "$fail" = 0 ] || exit 1
28
29# The files are the ones the sources draw: a card left over from other sources is a failure.
30if [ -f "$dir/.sources" ] && [ "$(cat "$dir/.sources")" = "$(tools/preview-hash)" ]; then
31  ok "drawn from the current sources"
32else
33  bad "not drawn from the current sources (run web/tools/preview)"
34fi
35
36[ "$(probe v:0 codec_name "$png")" = png ] || bad "card.png is not a PNG"
37size="$(probe v:0 width,height "$png" | paste -sd x)"
38[ "$size" = "${want_w}x${want_h}" ] && ok "card.png is $size" || bad "card.png is $size, the page declares ${want_w}x${want_h}"
39n="$(stat -c %s "$png")"; [ "$n" -le "$max_image" ] && ok "card.png is $n bytes" || bad "card.png is $n bytes (over $max_image)"
40
41[ "$(probe v:0 codec_name "$mp4")" = h264 ] && ok "video codec h264" || bad "video codec is not h264"
42[ "$(probe v:0 pix_fmt "$mp4")" = yuv420p ] && ok "pixel format yuv420p" || bad "pixel format is not yuv420p"
43size="$(probe v:0 width,height "$mp4" | paste -sd x)"
44w="${size%x*}"; h="${size#*x}"
45[ $((w % 2)) = 0 ] && [ $((h % 2)) = 0 ] && ok "even sides ($size)" || bad "odd side in $size"
46[ "$size" = "${want_w}x${want_h}" ] && ok "video is $size, as the page declares" || bad "video is $size, the page declares ${want_w}x${want_h}"
47[ "$(ffprobe -v error -show_entries stream=codec_type -of csv=p=0 "$mp4" | grep -c audio || true)" = 0 ] && ok "no audio stream" || bad "has an audio stream"
48n="$(stat -c %s "$mp4")"; [ "$n" -le "$max_video" ] && ok "video is $n bytes (limit $max_video)" || bad "video is $n bytes (over $max_video)"
49d="$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$mp4")"
50awk -v d="$d" 'BEGIN { exit !(d >= 5 && d <= 30) }' && ok "duration ${d}s" || bad "duration ${d}s is outside 5..30"
51# faststart: the index (moov) must come before the media data (mdat), or a player must download
52# the whole file before it can start.
53moov="$(grep -abo moov "$mp4" | head -1 | cut -d: -f1)"; mdat="$(grep -abo mdat "$mp4" | head -1 | cut -d: -f1)"
54[ -n "$moov" ] && [ -n "$mdat" ] && [ "$moov" -lt "$mdat" ] && ok "faststart (moov at $moov, before mdat at $mdat)" || bad "moov is not before mdat"
55[ "$(head -c 12 "$mp4" | tail -c 8 | head -c 4)" = ftyp ] && ok "starts with an ftyp box" || bad "does not start with ftyp"
56exit "$fail"