Skip to content

Generate Timelapse for 2023-10-02 - 2023-10-10 #18

Generate Timelapse for 2023-10-02 - 2023-10-10

Generate Timelapse for 2023-10-02 - 2023-10-10 #18

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 '1920x1080'.
type: choice
options:
- 2048x1536
- 1920x1440
- 1920x1080
- 1280x960
- 1280x720
- 1024x768
- 800x600
- 640x480
required: false
video_fps:
description: "Provide a custom framerate (frames per second) for the video. Default is '15'."
type: choice
options:
- 1
- 3
- 5
- 7
- 9
- 10
- 13
- 15
- 18
- 21
- 25
- 30
- 60
default: '15'
required: false
video_codec:
description: "Provide a custom codec for the video. Default is 'libx264'."
default: libx264
type: choice
options:
- libx264
- libx265
- libvpx
- libvpx-vp9
- libaom-av1
- libxvid
- libsvtav1
required: false
video_pixfmt:
type: choice
description: "Provide custom pixfmt flag for the video. Default is 'yuv420p'."
default: yuv420p
options:
- yuv420p
- yuv422p
- yuv444p
- yuv420p10le
- yuv422p10le
required: false
video_output:
type: choice
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
options:
- assets/timelapse
jobs:
generate_video:
name: "Collect Metadata and Files"
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
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}:${{ secrets.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 our inputs
# ------------------------------------------------------------------
MAX_TIMEFRAME=60
DEFAULT_TIMEFRAME=7
VIDEO_TIMEFRAME=${DEFAULT_TIMEFRAME}
if [[ "$IS_SCHEDULED" == "true" ]]; thena
[[ "$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"
[[ "$start_date" == "$end_date" ]] || date_range="$start_date - $end_date";
# 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-tree --name-only -r HEAD:"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 "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 }}
description: ${{ steps.collect.outputs.description }}
run: |
OUTPUT_DIR="assets/timelapse"
OUTPUT_PATH="${OUTPUT_DIR-}/${VIDEO_FILENAME:-"timelapse_${date_range// /_}.mp4"}"
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
echo "⏱️ Generating timelapse video... 🎬"
ffmpeg \
-r "${VIDEO_FPS:-15}" \
-pattern_type glob \
-i "./assets/*/*.jpg" \
-s "${VIDEO_SIZE:-"1920x1080"}" \
-vcodec ${VIDEO_CODEC:-"libx264"} \
-pix_fmt ${VIDEO_PIXFMT:-"yuv420p"} \
-vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" \
-an \
-crf 17 \
-preset "${VIDEO_PRESET:-"slow"}" \
-tune film \
-movflags +faststart \
-metadata title="${VIDEO_TITLE:-}" \
-metadata description="${VIDEO_DESCRIPTION:-}" \
-metadata year="$(date +%Y)" \
-metadata date="$(date +%Y-%m-%d)" \
-metadata comment="Generated by github.com/nberlette/f1" \
"${OUTPUT_PATH:-}"
- 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+="| **Preset** | %-56s |"$'\n'
COMMIT_BODY+="| **Pix Format** | %-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_PRESET:-}" \
"${VIDEO_PIXFMT:-}"
)"
# 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}"
- name: Upload Artifact
uses: actions/upload-artifact@v3
env:
OUTPUT_PATH: ${{ env.OUTPUT_PATH }}
VIDEO_FILENAME: ${{ env.VIDEO_FILENAME }}
with:
path: ${{ env.OUTPUT_PATH }}
name: ${{ env.VIDEO_FILENAME }}