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"