Skip to content

Generate Timelapse for 2023-10-09 - 2023-10-10 #25

Generate Timelapse for 2023-10-09 - 2023-10-10

Generate Timelapse for 2023-10-09 - 2023-10-10 #25

Workflow file for this run

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:
start_date:
type: string
description: |
Provide a custom start date for the generated timelapse video. The
format should be 'YYYY-MM-DD'. The earliest date available is '2023-06-03'.
required: true
end_date:
type: string
description: |
Provide a custom end date for the generated timelapse video. The
format should be 'YYYY-MM-DD'. The latest date available is today's date.
required: true
video_title:
type: string
description: |
Provide a custom title for the video. Supports several placeholders:
'{date_start}', '{date_end}', '{date_range}', '{timeframe}'
default: "Timelapse for {date_range}"
required: false
video_description:
type: string
description: |
Provide a custom description for the video. The same
placeholder rules apply as with titles.
default: "Timelapse video for {date_range}"
required: false
video_filename:
type: string
description: |
Provide a custom filename for the video. The extension must be '.mp4'.
Supports same placeholders as the title and description inputs.
default: "timelapse_{date_range}.mp4"
required: false
video_size:
description: |
Provide a custom size for the video. Default is '1024x768'.
type: string
default: "1024x768"
required: false
video_fps:
description: "Provide a custom framerate (frames per second) for the video. Default is 5."
type: number
default: '5'
required: false
video_codec:
description: "Provide a custom codec for the video. Default is 'libx264'."
default: libx264
type: string
required: false
video_format:
type: string
description: "Provide custom format flag (-vf) for the video."
default: "scale=-2:1080,format=yuv420p"
required: false
video_output:
type: string
description: "Provide a custom output path for the video. This will be the directory the 'video_filename' is output to. Default is 'assets/timelapse'."
default: assets/timelapse
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 }}
is_scheduled: ${{ steps.collect.outputs.is_scheduled }}
is_workflow_dispatch: ${{ steps.collect.outputs.is_workflow_dispatch }}
is_workflow_call: ${{ steps.collect.outputs.is_workflow_call }}
is_scheduled_daily: ${{ steps.collect.outputs.is_scheduled_daily }}
is_scheduled_weekly: ${{ steps.collect.outputs.is_scheduled_weekly }}
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)) }}
steps:
- name: "Checkout Repo (sparse)"
id: collect
env:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
# sparse checkout the repository
# because our specific requirements aren't met by the default action,
# we opt for 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 src src/kv src/helpers
all_dates=($(git ls-tree --name-only -d -r HEAD:assets .))
TOTAL_DATES="${#all_dates[@]}"
# normalize inputs
# ------------------------------------------------------------------
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:-}"
[ -z "${VIDEO_START_DATE:-}" ] && start_date="${all_dates[@]:$((TOTAL_DATES - DEFAULT_TIMEFRAME)):1}"
[ -z "${VIDEO_END_DATE:-}" ] && end_date="${all_dates[@]:$((TOTAL_DATES - 1)):1}"
if [ -n "${VIDEO_START_DATE:-}" ] && [ -n "${VIDEO_END_DATE:-}" ]; then
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
# ------------------------------------------------------------------
# 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 main
# ------------------------------------------------------------------
# output the results as JSON to GitHub Actions Outputs
# ------------------------------------------------------------------
all_dates_str="$(printf '%s\n' "${all_dates[@]}" | jq -R . | jq -c -s '@json')"
dates_str="$(printf '%s\n' "${dates[@]}" | jq -R . | jq -c -s '@json')"
files_str="$(printf '%s\n' "${files[@]}" | jq -R . | jq -c -s '@json')"
echo "all_dates=${all_dates_str-}" >>$GITHUB_OUTPUT
echo "dates=${dates_str-}" >>$GITHUB_OUTPUT
echo "files=${files_str-}" >>$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:
start_date: ${{ steps.collect.outputs.start_date }}
end_date: ${{ steps.collect.outputs.end_date }}
timeframe: ${{ steps.collect.outputs.timeframe }}
date_range: ${{ steps.collect.outputs.date_range }}
title: ${{ steps.collect.outputs.title }}
path: ${{ steps.collect.outputs.path }}
description: ${{ steps.collect.outputs.description }}
filename: ${{ steps.collect.outputs.filename }}
files: ${{ steps.collect.outputs.files }}
dates: ${{ steps.collect.outputs.dates }}
all_dates: ${{ steps.collect.outputs.all_dates }}
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: ${{ steps.collect.outputs.dates }}
VIDEO_FILES: ${{ steps.collect.outputs.files }}
run: |
TIMELAPSE_DIR="$(mktemp -d -t timelapse-XXXXXXXXXX)"
VIDEO_DATES=($(jq 'fromjson | .[]' <<<"$VIDEO_DATES"))
VIDEO_FILES=($(jq 'fromjson | .[]' <<<"$VIDEO_FILES"))
for date in "${VIDEO_DATES[@]}"; do
mkdir -p "$TIMELAPSE_DIR/$date"
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
VIDEO_FPS=${VIDEO_FPS:-5}
echo "⏱️ Generating timelapse video... 🎬"
echo "📸 Image Count: ${#files[@]} from ${#dates[@]} days"
echo "📅 Image Dates: ${VIDEO_START_DATE:-} - ${VIDEO_END_DATE:-}"
echo "📝 Video Title: ${VIDEO_TITLE:-}"
echo "ℹ️ Description: ${VIDEO_DESCRIPTION:-}"
echo "📎 Output File: ${VIDEO_FILENAME:-}"
ffmpeg \
-framerate ${VIDEO_FPS} \
-pattern_type glob \
-i "${TIMELAPSE_DIR-}/${VIDEO_DATES[@]}/*.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/nberlette/f1" \
"${OUTPUT_PATH:-}"
- 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 }}
- name: Commit and Push
env:
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 }}
run: |
# create a new branch for our timelapse
BRANCH_NAME="timelapse/${VIDEO_START_DATE}-$RANDOM"
COMMIT_BODY=$'Video Details:\n\n'
COMMIT_BODY+="| Label | %-56s |"$'\n'
COMMIT_BODY+="| :-------------- | $(printf '%-56s' ":" | tr ' ' '-') |"$'\n'
COMMIT_BODY+="| **Title** | %-56s |"$'\n'
COMMIT_BODY+="| **Description** | %-56s |"$'\n'
COMMIT_BODY+="| **Filename** | %-56s |"$'\n'
COMMIT_BODY+="| **Start Date** | %-56s |"$'\n'
COMMIT_BODY+="| **End Date** | %-56s |"$'\n'
COMMIT_BODY+="| **Timeframe** | %-56s |"$'\n'
COMMIT_BODY+="| **Video Size** | %-56s |"$'\n'
COMMIT_BODY+="| **Video FPS** | %-56s |"$'\n'
COMMIT_BODY+="| **Video Codec** | %-56s |"$'\n'
COMMIT_BODY+="| **Video Flags** | %-56s |"$'\n'
COMMIT_BODY+="| **Preset** | %-56s |"$'\n'
COMMIT_BODY="$(
printf "${COMMIT_BODY}\n\n" \
"Value" \
"${VIDEO_TITLE:-}" \
"${VIDEO_DESCRIPTION:-}" \
"${VIDEO_FILENAME:-}" \
"${VIDEO_START_DATE:-}" \
"${VIDEO_END_DATE:-}" \
"${VIDEO_TIMEFRAME:-}" \
"${VIDEO_SIZE:-}" \
"${VIDEO_FPS:-}" \
"${VIDEO_CODEC:-}" \
"${VIDEO_FORMAT:-}" \
"${VIDEO_PRESET:-}"
)"
# create a new branch, commit, and push
git checkout -b "${BRANCH_NAME}"
git add "${OUTPUT_PATH}"
git commit -m "feat: 🎬 new timelapse for ${VIDEO_START_DATE-}"$'\n\n'"${COMMIT_BODY-}"
git push
# create the pull request body
VIDEO_URL="https://github.com/nberlette/f1/raw/${BRANCH_NAME}/${OUTPUT_PATH}"
PR_BODY=$'## 📝 Video Details\n\n%s\n'
PR_BODY+=$'## 📺 Preview the video below!\n\n[![%s](%s)](%s)\n\n'
PR_BODY+=$'---\n\nHey @nberlette 👋 please review and merge this PR when ready!\n\n'
PR_BODY+=$'- 🤖 The F1 Bot\n\n'
PR_BODY+="> **Note**: if this **automated** PR is incorrect, please open an issue!"$'\n\n'
PR_BODY="$(printf "${PR_BODY}" "${COMMIT_BODY}" "${VIDEO_TITLE}" "${VIDEO_URL}" "${VIDEO_URL}")"
PR_TITLE=""
printf -v PR_TITLE '🎬 New Timelapse for %s - %s (%d days)' "${VIDEO_START_DATE}" "${VIDEO_END_DATE}" "${VIDEO_TIMEFRAME}"
# open the pull request using the github cli
gh pr create --title "${PR_TITLE}" -b "${PR_BODY}" -l timelapse,assets,automation \
-r nberlette -a nberlette -B main -H "${BRANCH_NAME}"