Stage raw files in upload/, publish with make sync-media/publish, and polish the lightbox: autoplay, remembered volume, Escape to close, and image/video icons without poster or caption clutter. Co-authored-by: Cursor <cursoragent@cursor.com>
194 lines
4.1 KiB
Bash
Executable File
194 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copy photos and convert videos from upload/ into app/images/ (gallery source).
|
|
# Videos are always re-encoded for browser playback (H.264 + AAC, yuv420p, faststart).
|
|
#
|
|
# Usage (from app/):
|
|
# ./scripts/sync-media.sh
|
|
# UPLOAD_DIR=/upload IMAGES_DIR=/app/images ./scripts/sync-media.sh
|
|
#
|
|
# Requires: ffmpeg (for videos)
|
|
|
|
set -euo pipefail
|
|
|
|
upload_dir="${UPLOAD_DIR:-../upload}"
|
|
images_dir="${IMAGES_DIR:-images}"
|
|
dry_run=0
|
|
force=0
|
|
max_width=1920
|
|
crf=23
|
|
preset=medium
|
|
|
|
usage() {
|
|
sed -n '2,8p' "$0"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --upload-dir DIR Raw media source (default: ../upload)"
|
|
echo " --images-dir DIR Gallery output (default: images)"
|
|
echo " --dry-run Print actions only"
|
|
echo " --force Re-copy / re-encode even if output looks up to date"
|
|
echo " --max-width N Scale down wide video (default: 1920)"
|
|
echo " --crf N H.264 quality (default: 23)"
|
|
echo " -h, --help Show this help"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--upload-dir) upload_dir=$2; shift 2 ;;
|
|
--images-dir) images_dir=$2; shift 2 ;;
|
|
--dry-run) dry_run=1; shift ;;
|
|
--force) force=1; shift ;;
|
|
--max-width) max_width=$2; shift 2 ;;
|
|
--crf) crf=$2; shift 2 ;;
|
|
-h | --help) usage; exit 0 ;;
|
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if ! command -v ffmpeg >/dev/null 2>&1; then
|
|
echo "ffmpeg is required." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "$upload_dir" ]]; then
|
|
echo "Upload directory not found: $upload_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$images_dir"
|
|
|
|
is_video_ext() {
|
|
case "${1,,}" in
|
|
.mp4 | .mov | .webm) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
is_jpeg_ext() {
|
|
case "${1,,}" in
|
|
.jpg | .jpeg) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
skip_name() {
|
|
local base=${1##*/}
|
|
[[ "$base" == .* ]] && return 0
|
|
[[ "$base" == *.web.mp4 ]] && return 0
|
|
[[ "$base" == *.converting.mp4 ]] && return 0
|
|
[[ "$base" == *.part ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
skip_path() {
|
|
case "$1" in
|
|
*/thumbs/* | */hero/*) return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
needs_convert() {
|
|
local src=$1 dst=$2
|
|
if [[ -f "$dst" ]] && [[ "$force" -eq 0 ]] && [[ "$dst" -nt "$src" ]]; then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
copy_one() {
|
|
local src=$1 dst=$2 rel
|
|
rel=${src#"$upload_dir"/}
|
|
rel=${rel#/}
|
|
echo "→ copy $rel"
|
|
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
echo " would write: ${dst#"$images_dir"/}"
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$dst")"
|
|
cp -p "$src" "$dst"
|
|
echo " wrote: ${dst#"$images_dir"/}"
|
|
}
|
|
|
|
convert_one() {
|
|
local src=$1 dst=$2
|
|
local dir name tmp rel
|
|
dir=$(dirname "$dst")
|
|
name=$(basename "$dst" .mp4)
|
|
tmp="$dir/.${name}.converting.mp4"
|
|
rel=${src#"$upload_dir"/}
|
|
rel=${rel#/}
|
|
|
|
echo "→ convert $rel"
|
|
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
echo " would write: ${dst#"$images_dir"/}"
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$dir"
|
|
rm -f "$tmp"
|
|
trap 'rm -f "$tmp"' RETURN
|
|
|
|
ffmpeg -nostdin -hide_banner -loglevel error -y \
|
|
-i "$src" \
|
|
-vf "scale=min(${max_width}\\,iw):-2" \
|
|
-c:v libx264 -profile:v high -level:v 4.0 -pix_fmt yuv420p \
|
|
-crf "$crf" -preset "$preset" \
|
|
-c:a aac -b:a 128k -ac 2 \
|
|
-movflags +faststart \
|
|
-f mp4 \
|
|
"$tmp"
|
|
|
|
mv -f "$tmp" "$dst"
|
|
trap - RETURN
|
|
echo " wrote: ${dst#"$images_dir"/}"
|
|
}
|
|
|
|
copied=0
|
|
converted=0
|
|
skipped=0
|
|
|
|
while IFS= read -r -d '' src; do
|
|
base=$(basename "$src")
|
|
ext=".${base##*.}"
|
|
|
|
if skip_name "$base" || skip_path "$src"; then
|
|
continue
|
|
fi
|
|
|
|
rel=${src#"$upload_dir"/}
|
|
rel=${rel#/}
|
|
dst="$images_dir/$rel"
|
|
|
|
if is_jpeg_ext "$ext"; then
|
|
if [[ -f "$dst" ]] && [[ "$force" -eq 0 ]] && [[ "$dst" -nt "$src" ]]; then
|
|
((skipped++)) || true
|
|
continue
|
|
fi
|
|
copy_one "$src" "$dst"
|
|
((copied++)) || true
|
|
continue
|
|
fi
|
|
|
|
if ! is_video_ext "$ext"; then
|
|
continue
|
|
fi
|
|
|
|
dst="$images_dir/${rel%.*}.mp4"
|
|
|
|
if ! needs_convert "$src" "$dst"; then
|
|
((skipped++)) || true
|
|
continue
|
|
fi
|
|
|
|
convert_one "$src" "$dst"
|
|
((converted++)) || true
|
|
done < <(find "$upload_dir" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.mov' -o -iname '*.mp4' -o -iname '*.webm' \) -print0)
|
|
|
|
echo ""
|
|
echo "Done. Copied: $copied, converted: $converted, skipped: $skipped."
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
echo "(dry run — no files changed)"
|
|
fi
|