Generate Timelapse for 2023-10-01 - 2023-10-09 #32
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: | |
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: | | |
# 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 | |
# 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: ${{ toJson(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 -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 | |
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-}/*/*.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:-}" | |
- 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: | |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
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}${VIDEO_END_DATE:+"_${VIDEO_END_DATE}"}" | |
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 --sparse "${OUTPUT_PATH}" | |
git commit -m "feat: 🎬 new timelapse for ${VIDEO_START_DATE-}"$'\n\n'"${COMMIT_BODY-}" | |
git push --set-upstream origin "${BRANCH_NAME}" | |
# create the pull request body | |
VIDEO_URL="https://github.com/${GITHUB_REPOSITORY}/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 @%s 👋 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}" "${GITHUB_ACTOR}" "${VIDEO_TITLE}" "${VIDEO_URL}" "${VIDEO_URL}")" | |
PR_TITLE="" | |
printf -v PR_TITLE '🎬 New Timelapse for %s (%d days)' "${VIDEO_START_DATE}${VIDEO_END_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,automated -r "${GITHUB_ACTOR}" -a "${GITHUB_ACTOR}" -B main -H "${BRANCH_NAME}" |