Improve gallery video UX and add upload-to-publish media workflow.
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>
This commit is contained in:
3
app/scripts/convert-videos.sh
Executable file
3
app/scripts/convert-videos.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deprecated alias — use sync-media.sh (upload/ → app/images/).
|
||||
exec "$(dirname "$0")/sync-media.sh" "$@"
|
||||
193
app/scripts/sync-media.sh
Executable file
193
app/scripts/sync-media.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user