Generate Timelapse for 2023-08-15 - 2023-09-30 #35
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Timelapse | |
run-name: "Generate Timelapse for ${{ format('{0} - {1}', inputs.start_date, inputs.end_date) }}" | |
on: | |
# schedule: | |
# - cron: "45 6 * * *" | |
# - cron: "30 6 * * SUN" | |
# workflow_call: | |
workflow_dispatch: | |
inputs: | |
create_pr: | |
type: boolean | |
description: "Create a Pull Request for the video?" | |
default: true | |
start_date: | |
type: string | |
description: "Start date (no earlier than '2023-06-03'):" | |
required: true | |
end_date: | |
type: string | |
description: "End date, in 'YYYY-MM-DD' format:" | |
required: true | |
video_title: | |
type: string | |
description: "Video title (supported placeholders are '{date_start}', '{date_end}', '{date_range}'):" | |
default: "Timelapse for {date_range}" | |
required: false | |
video_description: | |
type: string | |
description: "Video Description:" | |
default: "Las Vegas Formula 1 Track construction from {date_range}" | |
required: false | |
video_filename: | |
type: string | |
description: "Output Filename:" | |
default: "timelapse_{date_start}_{date_end}.mp4" | |
required: false | |
video_size: | |
description: "Resolution (width x height):" | |
type: string | |
default: "1024x768" | |
required: false | |
video_fps: | |
description: "Framerate (fps):" | |
type: number | |
default: 5 | |
required: false | |
video_codec: | |
description: "Codec (-vcodec flag):" | |
default: libx264 | |
type: string | |
required: false | |
video_format: | |
type: string | |
description: "Format flags (-vf flag):" | |
default: "scale=-2:1080,format=yuv420p" | |
required: false | |
jobs: | |
generate_video: | |
name: "Generate Timelapse Video" | |
runs-on: ubuntu-latest | |
outputs: | |
files: ${{ steps.collect.outputs.files }} | |
dates: ${{ steps.collect.outputs.dates }} | |
all_dates: ${{ steps.collect.outputs.all_dates }} | |
timeframe: ${{ steps.collect.outputs.timeframe }} | |
start_date: ${{ steps.collect.outputs.start_date }} | |
end_date: ${{ steps.collect.outputs.end_date }} | |
date_range: ${{ steps.collect.outputs.date_range }} | |
title: ${{ steps.collect.outputs.title }} | |
description: ${{ steps.collect.outputs.description }} | |
filename: ${{ steps.collect.outputs.filename }} | |
env: | |
CACHE_KEY: ${{ inputs.start_date }}-${{ inputs.end_date }} | |
IS_SCHEDULED: ${{ ((github.event_name == 'schedule' && 'true') || 'false') }} | |
IS_WORKFLOW_DISPATCH: ${{ ((github.event_name == 'workflow_dispatch' && 'true') || 'false') }} | |
IS_SCHEDULED_DAILY: ${{ ((github.event_name == 'schedule' && github.event.schedule == '45 6 * * *' && 'true') || 'false') }} | |
IS_SCHEDULED_WEEKLY: ${{ ((github.event_name == 'schedule' && github.event.schedule == '30 6 * * SUN' && 'true') || 'false') }} | |
IS_WORKFLOW_CALL: ${{ ((github.event_name == 'workflow_call' && 'true') || 'false') }} | |
VIDEO_END_DATE: ${{ (inputs.end_date || '') }} | |
VIDEO_START_DATE: ${{ (inputs.start_date || '') }} | |
VIDEO_TITLE: ${{ (inputs.video_title || 'Timelapse for {date_range}') }} | |
VIDEO_DESCRIPTION: ${{ (inputs.video_description || 'Timelapse video for {date_range}') }} | |
VIDEO_FILENAME: ${{ (inputs.video_filename || format('timelapse_{0}_-_{1}.mp4', inputs.start_date, inputs.end_date)) }} | |
GITHUB_REPOSITORY: ${{ github.repository }} | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
GITHUB_ACTOR: ${{ github.actor }} | |
steps: | |
- name: "Checkout Repo (sparse)" | |
id: collect | |
run: | | |
# using custom checkout logic rather than actions/checkout | |
REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
git config --global user.name "github-actions[bot]" | |
git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
git config --global fetch.parallel 32 | |
git clone --filter=blob:none --no-checkout --depth 1 --sparse $REPO . | |
git sparse-checkout init --cone | |
git sparse-checkout add . .github/workflows src src/kv src/helpers | |
all_dates=($(git ls-tree --name-only -d -r HEAD:assets .)) | |
TOTAL_DATES="${#all_dates[@]}" | |
MAX_TIMEFRAME=60 | |
DEFAULT_TIMEFRAME=7 | |
VIDEO_TIMEFRAME=${DEFAULT_TIMEFRAME} | |
if [[ "$IS_SCHEDULED" == "true" ]]; then | |
[[ "$IS_SCHEDULED_DAILY" == "true" ]] && VIDEO_TIMEFRAME=1 | |
[[ "$IS_SCHEDULED_WEEKLY" == "true" ]] && VIDEO_TIMEFRAME=7 | |
fi | |
if ((VIDEO_TIMEFRAME > TOTAL_DATES)); then | |
VIDEO_TIMEFRAME="$TOTAL_DATES" | |
elif ((VIDEO_TIMEFRAME > MAX_TIMEFRAME)); then | |
VIDEO_TIMEFRAME="$MAX_TIMEFRAME" | |
fi | |
start_date="${VIDEO_START_DATE:-}" | |
end_date="${VIDEO_END_DATE:-}" | |
if [ -z "${VIDEO_START_DATE:-}" ] && [ -z "${VIDEO_END_DATE:-}" ]; then | |
start_date="${all_dates[@]:$(($((TOTAL_DATES - 1)) - VIDEO_TIMEFRAME)):1}" | |
end_date="${all_dates[@]:$((TOTAL_DATES - 1)):1}" | |
else | |
start_date="${VIDEO_START_DATE-}" | |
end_date="${VIDEO_END_DATE-}" | |
# clamp end_date to the latest date in the array | |
if [[ "$end_date" > "${all_dates[@]: -1}" ]]; then | |
end_date="${all_dates[@]: -1}" | |
if [[ "$start_date" > "$end_date" || "$start_date" == "$end_date" ]]; then | |
start_date="$(date -d "$end_date -${timeframe} days" +%Y-%m-%d)" | |
fi | |
fi | |
# clamp start_date to the earliest date in the array | |
if [[ "$start_date" < "${all_dates[@]:0:1}" ]]; then | |
start_date="${all_dates[@]:0:1}" | |
if [[ "$end_date" < "$start_date" || "$end_date" == "$start_date" ]]; then | |
end_date="$(date -d "$start_date +${timeframe} days" +%Y-%m-%d)" | |
fi | |
fi | |
fi | |
date_range="$start_date" | |
if [[ "$start_date" != "$end_date" ]]; then | |
date_range="$start_date - $end_date" | |
fi | |
# determine the 'timeframe' as the time between dates, in days. | |
timeframe=$(($(($(date -d "$end_date" +%s) - $(date -d "$start_date" +%s))) / 86400)) | |
((timeframe <= 0)) && timeframe=1 | |
VIDEO_TIMEFRAME=$timeframe | |
# collect the dates and files, add them to the sparse checkout | |
dates=() | |
files=() | |
for cur in "${all_dates[@]}"; do | |
if [[ "$cur" > "$start_date" || "$cur" == "$start_date" ]]; then | |
if [[ "$cur" < "$end_date" || "$cur" == "$end_date" ]]; then | |
dates+=("$cur") | |
git sparse-checkout add "assets/$cur" | |
files+=($(git ls-files assets/$cur)) | |
fi | |
fi | |
done | |
git checkout | |
echo "all_dates=$(printf '%s\n' "${all_dates[@]}" | jq -R . | jq -c -s '@json')" >>$GITHUB_OUTPUT | |
echo "dates=$(printf '%s\n' "${dates[@]}" | jq -R . | jq -c -s '@json')" >>$GITHUB_OUTPUT | |
echo "files=$(printf '%s\n' "${files[@]}" | jq -R . | jq -c -s '@json')" >>$GITHUB_OUTPUT | |
VIDEO_TITLE=${VIDEO_TITLE//"{date_start}"/$start_date} | |
VIDEO_TITLE=${VIDEO_TITLE//"{date_end}"/$end_date} | |
VIDEO_TITLE=${VIDEO_TITLE//"{date_range}"/$date_range} | |
VIDEO_DESCRIPTION=${VIDEO_DESCRIPTION//"{date_start}"/$start_date} | |
VIDEO_DESCRIPTION=${VIDEO_DESCRIPTION//"{date_end}"/$end_date} | |
VIDEO_DESCRIPTION=${VIDEO_DESCRIPTION//"{date_range}"/$date_range} | |
VIDEO_FILENAME=${VIDEO_FILENAME//"{date_start}"/$start_date} | |
VIDEO_FILENAME=${VIDEO_FILENAME//"{date_end}"/$end_date} | |
VIDEO_FILENAME=${VIDEO_FILENAME//"{date_range}"/"$(echo "$date_range" | tr -d ' ')"} | |
echo "VIDEO_TITLE=${VIDEO_TITLE}" >>$GITHUB_ENV | |
echo "VIDEO_DESCRIPTION=${VIDEO_DESCRIPTION}" >>$GITHUB_ENV | |
echo "VIDEO_FILENAME=${VIDEO_FILENAME}" >>$GITHUB_ENV | |
echo "VIDEO_TIMEFRAME=${VIDEO_TIMEFRAME}" >>$GITHUB_ENV | |
echo "VIDEO_START_DATE=${VIDEO_START_DATE}" >>$GITHUB_ENV | |
echo "VIDEO_END_DATE=${VIDEO_END_DATE}" >>$GITHUB_ENV | |
echo "timeframe=$timeframe" >>$GITHUB_OUTPUT | |
echo "start_date=$start_date" >>$GITHUB_OUTPUT | |
echo "end_date=$end_date" >>$GITHUB_OUTPUT | |
echo "date_range=$date_range" >>$GITHUB_OUTPUT | |
echo "title=${VIDEO_TITLE}" >>$GITHUB_OUTPUT | |
echo "description=${VIDEO_DESCRIPTION}" >>$GITHUB_OUTPUT | |
echo "filename=${VIDEO_FILENAME}" >>$GITHUB_OUTPUT | |
echo "is_scheduled=${IS_SCHEDULED}" >>$GITHUB_OUTPUT | |
echo "is_workflow_dispatch=${IS_WORKFLOW_DISPATCH}" >>$GITHUB_OUTPUT | |
echo "is_workflow_call=${IS_WORKFLOW_CALL}" >>$GITHUB_OUTPUT | |
echo "is_scheduled_daily=${IS_SCHEDULED_DAILY}" >>$GITHUB_OUTPUT | |
echo "is_scheduled_weekly=${IS_SCHEDULED_WEEKLY}" >>$GITHUB_OUTPUT | |
- id: cache | |
name: "Cache Assets" | |
uses: actions/cache@v3 | |
with: | |
key: ${{ runner.os }}-assets-${{ hashFiles('assets/**/*.jpg') }} | |
path: assets | |
restore-keys: | | |
${{ runner.os }}-assets-${{ env.CACHE_KEY }} | |
${{ runner.os }}-assets-${{ github.sha }} | |
${{ runner.os }}-assets- | |
- name: install ffmpeg | |
run: | | |
if ! command -v ffmpeg &>/dev/null; then | |
if command -v brew &>/dev/null; then | |
brew install --force ffmpeg | |
else | |
NONINTERACTIVE=1 \ | |
sudo apt-get update && \ | |
sudo apt-get install -y ffmpeg --no-install-recommends && \ | |
sudo apt-get clean && \ | |
sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* | |
fi | |
fi | |
- id: generate | |
name: Generate Timelapse | |
env: | |
all_dates: ${{ steps.collect.outputs.all_dates }} | |
start_date: ${{ steps.collect.outputs.start_date }} | |
end_date: ${{ steps.collect.outputs.end_date }} | |
date_range: ${{ steps.collect.outputs.date_range }} | |
timeframe: ${{ steps.collect.outputs.timeframe }} | |
title: ${{ env.VIDEO_TITLE || steps.collect.outputs.title }} | |
description: ${{ env.VIDEO_DESCRIPTION || steps.collect.outputs.description }} | |
path: ${{ steps.collect.outputs.path }} | |
VIDEO_FILENAME: ${{ env.VIDEO_FILENAME || steps.collect.outputs.filename }} | |
VIDEO_FPS: ${{ env.VIDEO_FPS || inputs.video_fps }} | |
VIDEO_CODEC: ${{ env.VIDEO_CODEC || inputs.video_codec }} | |
VIDEO_SIZE: ${{ env.VIDEO_SIZE || inputs.video_size }} | |
VIDEO_FORMAT: ${{ env.VIDEO_FORMAT || inputs.video_format }} | |
VIDEO_DATES: ${{ env.VIDEO_DATES || steps.collect.outputs.dates }} | |
VIDEO_FILES: ${{ env.VIDEO_FILES || steps.collect.outputs.files }} | |
RUNNER_TEMP: ${{ runner.temp }} | |
run: | | |
TIMELAPSE_DIR="$(mktemp -d -t timelapse-XXXXXXXXXX)" | |
VIDEO_DATES=($(jq -r 'fromjson | .[]' <<<"$VIDEO_DATES")) | |
VIDEO_FILES=($(jq -r 'fromjson | .[]' <<<"$VIDEO_FILES")) | |
for date in "${VIDEO_DATES[@]}"; do | |
cp -r "./assets/$date" "$TIMELAPSE_DIR/" | |
done | |
OUTPUT_DIR="assets/timelapse" | |
VIDEO_FILENAME="${VIDEO_FILENAME:-"timelapse_${date_range// /_}.mp4"}" | |
OUTPUT_PATH="${OUTPUT_DIR-}/${VIDEO_FILENAME-}" | |
mkdir -p "${OUTPUT_DIR-}" | |
if [ -f "${OUTPUT_PATH-}" ]; then | |
# If the file already exists, append a timestamp to the filename | |
OUTPUT_PATH="${OUTPUT_DIR-}/$(date +%s)_${VIDEO_FILENAME:-}" | |
fi | |
echo "OUTPUT_PATH=${OUTPUT_PATH:-}" >>$GITHUB_ENV | |
echo "path=${OUTPUT_PATH:-}" >>$GITHUB_OUTPUT | |
echo "title=${VIDEO_TITLE:-}" >>$GITHUB_OUTPUT | |
echo "description=${VIDEO_DESCRIPTION:-}" >>$GITHUB_OUTPUT | |
echo "filename=${VIDEO_FILENAME:-}" >>$GITHUB_OUTPUT | |
MAX_FPS=30 | |
VIDEO_FPS=${VIDEO_FPS:-5} | |
VIDEO_R=$((VIDEO_FPS * 2)) | |
if ((VIDEO_FPS > MAX_FPS)); then | |
VIDEO_FPS=$MAX_FPS | |
VIDEO_R=$MAX_FPS | |
elif ((VIDEO_FPS < 1)); then | |
VIDEO_FPS=1 | |
VIDEO_R=1 | |
fi | |
DEBUG_LOG="debug_log_$(date +%Y-%m-%d)_${RANDOM}.log" | |
echo "debug_log=${DEBUG_LOG-}" >>$GITHUB_OUTPUT | |
DEBUG_LOG="${RUNNER_TEMP}/${DEBUG_LOG}" | |
touch "$DEBUG_LOG" | |
cat >> "$DEBUG_LOG" <<__EOF_1 | |
📸 Image Count: | |
${#VIDEO_FILES[@]} from ${#VIDEO_DATES[@]} days | |
📅 Image Dates: | |
${VIDEO_START_DATE:-} - ${VIDEO_END_DATE:-} | |
📝 Video Title: | |
${VIDEO_TITLE:-} | |
ℹ️ Description: | |
${VIDEO_DESCRIPTION:-} | |
📎 Output File: | |
${VIDEO_FILENAME:-} | |
🌳 Branch Name: | |
${BRANCH_NAME:-} | |
🏁 Flags Used: | |
-framerate ${VIDEO_FPS} | |
-pattern_type glob | |
-i "${TIMELAPSE_DIR-}/*/*.jpg" | |
-s "${VIDEO_SIZE:-"1024x768"}" | |
-vcodec ${VIDEO_CODEC:-"libx264"} | |
-vf "${VIDEO_FORMAT:-"scale=-2:1080,format=yuv420p"}" | |
-r $((VIDEO_FPS * 2)) | |
-an | |
-tune film | |
-movflags +faststart | |
-metadata title="${title:-}" | |
-metadata description="${description:-}" | |
-metadata year="$(date +%Y)" | |
-metadata date="$(date +%Y-%m-%d)" | |
-metadata comment="Generated by github.com/${GITHUB_REPOSITORY:-nberlette/f1}" | |
"${OUTPUT_PATH:-}" 2>&1 | |
------------------------------------------------------------------ | |
FFMPEG OUTPUT | |
------------------------------------------------------------------ | |
__EOF_1 | |
ffmpeg \ | |
-framerate $VIDEO_FPS \ | |
-pattern_type glob \ | |
-i "${TIMELAPSE_DIR-}/*/*.jpg" \ | |
-s "${VIDEO_SIZE:-"1024x768"}" \ | |
-vcodec ${VIDEO_CODEC:-"libx264"} \ | |
-vf "${VIDEO_FORMAT:-"scale=-2:1080,format=yuv420p"}" \ | |
-r $VIDEO_R \ | |
-an \ | |
-tune film \ | |
-movflags +faststart \ | |
-metadata title="${title:-}" \ | |
-metadata description="${description:-}" \ | |
-metadata year="$(date +%Y)" \ | |
-metadata date="$(date +%Y-%m-%d)" \ | |
-metadata comment="Generated by github.com/${GITHUB_REPOSITORY:-nberlette/f1}" \ | |
"${OUTPUT_PATH:-}" 2>&1 | tee -a "$DEBUG_LOG" | |
- if: success() | |
name: Upload Artifact | |
uses: actions/upload-artifact@v3 | |
continue-on-error: true | |
env: | |
OUTPUT_PATH: ${{ env.OUTPUT_PATH }} | |
VIDEO_FILENAME: ${{ env.VIDEO_FILENAME }} | |
with: | |
path: ${{ env.OUTPUT_PATH }} | |
name: ${{ env.VIDEO_FILENAME }} | |
- if: always() && inputs.create_pr == 'true' | |
id: create_pr | |
name: "Commit + Create Pull Request" | |
run: | | |
OUTPUT_PATH=${{ steps.generate.outputs.path }} | |
VIDEO_TITLE=${{ steps.generate.outputs.title }} | |
START_DATE=${{ steps.collect.outputs.start_date }} | |
END_DATE=${{ steps.collect.outputs.end_date }} | |
TIMEFRAME=${{ steps.collect.outputs.timeframe }} | |
BRANCH_NAME="timelapse/${START_DATE}${END_DATE:+_${END_DATE}}" | |
COMMIT_BODY="" | |
COMMIT_BODY+=$(printf "| Label | %-56s |\n" "Value") | |
COMMIT_BODY+=$(printf "| :-------------- | %-56s |\n" "$(printf '%-56s' ":" | tr ' ' '-')") | |
COMMIT_BODY+=$(printf "| **Title** | %-56s |\n" "$VIDEO_TITLE") | |
COMMIT_BODY+=$(printf "| **Start Date** | %-56s |\n" "$START_DATE") | |
COMMIT_BODY+=$(printf "| **End Date** | %-56s |\n" "$END_DATE") | |
COMMIT_BODY+=$(printf "| **Timeframe** | %-56s |\n" "$TIMEFRAME") | |
COMMIT_BODY+=$(printf "| **Video Size** | %-56s |\n" "$VIDEO_SIZE") | |
COMMIT_BODY+=$(printf "| **Video FPS** | %-56s |\n" "$VIDEO_FPS") | |
COMMIT_BODY+=$(printf "| **Video Codec** | %-56s |\n" "$VIDEO_CODEC") | |
COMMIT_BODY+=$(printf "| **Video Flags** | %-56s |\n" "$VIDEO_FORMAT") | |
COMMIT_MSG="$( | |
printf \ | |
"feat: 🎬 new timelapse for %s\n\nVideo Details:\n\n$%s" \ | |
"${VIDEO_START_DATE}${VIDEO_END_DATE:+" - ${VIDEO_END_DATE}" \ | |
"$COMMIT_BODY" | |
)" | |
git checkout -b "$BRANCH_NAME" | |
git add --sparse "$OUTPUT_PATH" | |
git commit -m "$COMMIT_MSG" | |
git push --set-upstream origin "$BRANCH_NAME" | |
VIDEO_URL="https://github.com/${GITHUB_REPOSITORY}/raw/${BRANCH_NAME}/${OUTPUT_PATH}" | |
PR_BODY=$(printf "## 📝 Video Details\n\n%s\n\n" "$COMMIT_BODY") | |
PR_BODY+=$(printf "## 📺 Download the full video\n\n- [x] [%s](%s)\n\n---\n\n" "$VIDEO_TITLE" "$VIDEO_URL") | |
PR_BODY+="👋 Project maintainers, please review and merge this PR when ready!" | |
PR_BODY+=$'\n- 🤖 The F1 Bot\n\n' | |
PR_BODY+="> **Note**: if this automated PR is incorrect, please open an issue." | |
PR_TITLE=$(printf '🎬 New Timelapse for %s (%d days)' "$START_DATE${END_DATE:+ - $END_DATE}" "$TIMEFRAME") | |
PR_PROJECT="Timelapse" | |
PR_LABELS=(assets automated timelapse) | |
ALL_LABELS=($(gh label list --json name --jq '.[].name')) | |
for label in "${PR_LABELS[@]}"; do | |
if [[ ! " ${ALL_LABELS[@]} " =~ " ${label} " ]]; then | |
# create a hex color code for the label (deterministic based on the label name) | |
label_color=$(echo -n "$label" | md5sum | cut -c1-6) | |
gh label create "$label" -c "#${label_color}" | |
fi | |
done | |
gh pr create \ | |
-B "main" \ | |
-t "$PR_TITLE" \ | |
-b "$PR_BODY" \ | |
-l "${PR_LABELS}$(printf ',%s' "${PR_LABELS[@]:1}" \ | |
-r "$GITHUB_ACTOR" \ | |
-a "$GITHUB_ACTOR" | |
- if: always() && env.DEBUG_LOG | |
name: Upload Debug Log | |
uses: actions/upload-artifact@v3 | |
continue-on-error: true | |
env: | |
DEBUG_LOG: ${{ steps.generate.outputs.debug_log }} | |
with: | |
path: ${{ runner.temp }}/${{ env.DEBUG_LOG }} | |
name: ${{ env.DEBUG_LOG }} |