#!/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