Generate 3day Timelapse for 2023-10-06 #12
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 ${{ inputs.timeframe }}day Timelapse for ${{ inputs.start_date }}" | |
on: | |
schedule: | |
- cron: "45 6 * * *" | |
- cron: "30 6 * * SUN" | |
workflow_call: | |
workflow_dispatch: | |
inputs: | |
timeframe: | |
type: number | |
description: | | |
Time range (in days) to generate the timelapse video for. Value must | |
be between no less than 1. | |
required: true | |
default: "1" | |
start_date: | |
type: string | |
description: | | |
Provide a custom start date for the generated timelapse video. The | |
format should be 'YYYY-MM-DD'. If no start date is provided, it will | |
be calculated using the 'timeframe' subtracted from the latest date. | |
required: false | |
video_title: | |
type: string | |
description: | | |
Provide a custom title for the generated video. Supports several | |
different placeholders for dynamic values: | |
'{start_date}', '{end_date}', '{timeframe}', '{date_range}' | |
default: "Timelapse for {date_range}" | |
required: false | |
video_description: | |
type: string | |
description: | | |
Provide a custom description for the generated 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 generated video. The extension | |
should be '.mp4'. The same placeholder rules apply here as they do in | |
the title and description inputs. | |
default: "timelapse_{date_range}.mp4" | |
required: false | |
video_width: | |
type: number | |
description: | | |
Provide a custom width for the generated video. The height will be | |
calculated automatically based on the aspect ratio of the images. | |
The default value is 1920. | |
default: "1920" | |
required: false | |
video_fps: | |
type: number | |
description: "Provide a custom framerate for the generated video." | |
default: "15" | |
required: false | |
video_crf: | |
type: number | |
description: | | |
Provide a custom CRF value for the generated video. The default value | |
is 17. | |
default: "17" | |
required: false | |
video_preset: | |
type: string | |
description: | | |
Provide a custom preset for the generated video. The default value is | |
'slow'. | |
default: "slow" | |
required: false | |
video_movflags: | |
type: string | |
description: | | |
Provide custom movflags for the generated video. The default value is | |
'+faststart'. | |
default: "+faststart" | |
required: false | |
jobs: | |
cache: | |
name: "Cache Assets" | |
runs-on: ubuntu-latest | |
steps: | |
- name: Configure Git | |
run: | | |
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 | |
- name: Checkout Repository | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Cache Assets Directory | |
id: cache-hit | |
uses: actions/cache@v3 | |
with: | |
path: assets | |
key: ${{ runner.os }}-assets-${{ github.sha }} | |
restore-keys: | | |
${{ runner.os }}-assets-${{ github.sha }} | |
${{ runner.os }}-assets- | |
metadata: | |
name: "Collect Metadata and Files" | |
needs: cache | |
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 }} | |
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 }} | |
steps: | |
- name: Restore Cache | |
uses: actions/cache@v3 | |
with: | |
path: assets | |
key: ${{ runner.os }}-assets-${{ github.sha }} | |
- id: collect | |
name: "(setup) collect items" | |
env: | |
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') }} | |
INPUT_TIMEFRAME: ${{ fromJson(inputs.timeframe || '1') }} | |
VIDEO_START_DATE: ${{ (inputs.start_date || '') }} | |
run: | | |
all_dates=($(cd assets && env ls -t1 --time=ctime -I "*.jpg")) | |
VIDEO_TIMEFRAME=1 | |
if [[ "$IS_SCHEDULED" == "true" ]]; then | |
if [[ "$IS_SCHEDULED_DAILY" == "true" ]]; then | |
VIDEO_TIMEFRAME=1 | |
elif [[ "$IS_SCHEDULED_WEEKLY" == "true" ]]; then | |
VIDEO_TIMEFRAME=7 | |
fi | |
else | |
VIDEO_TIMEFRAME="${INPUT_TIMEFRAME:-1}" | |
fi | |
if ((VIDEO_TIMEFRAME > ${#all_dates[@]})); then | |
VIDEO_TIMEFRAME=${#all_dates[@]} | |
elif ((VIDEO_TIMEFRAME > 60)); then | |
VIDEO_TIMEFRAME=60 | |
fi | |
if [ -z "${VIDEO_START_DATE:-}" ]; then | |
# If a start date is not provided, take the latest date | |
start_date="${all_dates[$((-1 - VIDEO_TIMEFRAME))]}" | |
end_date="${all_dates[-1]}" | |
else | |
start_date="${VIDEO_START_DATE-}" | |
timeframe="${VIDEO_TIMEFRAME-}" | |
end_date="$(date -d "$start_date +${timeframe} days" +%Y-%m-%d)" | |
# if the start_date is later than the latest date in the array | |
# (index -1), then just use ${all_dates[-1]} as the start date. | |
if [[ "$end_date" > "${all_dates[-1]}" ]]; then | |
end_date="${all_dates[-1]}" | |
start_date="$(date -d "$end_date -${timeframe} days" +%Y-%m-%d)" | |
fi | |
# if the start_date is earlier than the earliest date in the | |
# array (index 0), then just use ${all_dates[0]} as the start date. | |
if [[ "$start_date" < "${all_dates[0]}" ]]; then | |
start_date="${all_dates[0]}" | |
end_date="$(date -d "$start_date +${timeframe} days" +%Y-%m-%d)" | |
fi | |
fi | |
date_range="$start_date" | |
if [ "$start_date" != "$end_date" ]; then | |
date_range="$start_date - $end_date" | |
fi | |
echo "timeframe='$VIDEO_TIMEFRAME'" >>$GITHUB_OUTPUT | |
echo "start_date='$start_date'" >>$GITHUB_OUTPUT | |
echo "end_date='$end_date'" >>$GITHUB_OUTPUT | |
echo "date_range='$date_range'" >>$GITHUB_OUTPUT | |
dates=() | |
files=() | |
for date in "${all_dates[@]}"; do | |
if [[ "$date" > "$start_date" || "$date" == "$start_date" ]]; then | |
if [[ "$date" < "$end_date" || "$date" == "$end_date" ]]; then | |
dates+=("$date") | |
files_in_date=($(ls assets/$date/*.jpg)) | |
files=("${files[@]}" "${files_in_date[@]}") | |
fi | |
fi | |
done | |
# stringify the arrays to json | |
all_dates_str="$(printf '%s\n' "${all_dates[@]}" | jq -R . | jq --indent 0 -s .)" | |
dates_str="$(printf '%s\n' "${dates[@]}" | jq -R . | jq --indent 0 -s .)" | |
files_str="$(printf '%s\n' "${files[@]}" | jq -R . | jq --indent 0 -s .)" | |
echo "all_dates='${all_dates_str-}'" >>$GITHUB_OUTPUT | |
echo "dates='${dates_str-}'" >>$GITHUB_OUTPUT | |
echo "files='${files_str-}'" >>$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 | |
generate_video: | |
name: "Generate Timelapse Video" | |
needs: metadata | |
runs-on: ubuntu-latest | |
steps: | |
- 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 | |
- name: Generate Timelapse | |
env: | |
VIDEO_FILES: ${{ needs.metadata.outputs.files }} | |
VIDEO_DATES: ${{ needs.metadata.outputs.dates }} | |
VIDEO_TIMEFRAME: ${{ needs.metadata.outputs.timeframe }} | |
# VIDEO_ALL_DATES: ${{ fromJson(needs.metadata.outputs.all_dates) }} | |
VIDEO_START_DATE: ${{ needs.metadata.outputs.start_date }} | |
VIDEO_END_DATE: ${{ needs.metadata.outputs.end_date }} | |
VIDEO_DATE_RANGE: ${{ needs.metadata.outputs.date_range }} | |
VIDEO_IS_SCHEDULED: ${{ fromJson(needs.metadata.outputs.is_scheduled) }} | |
VIDEO_IS_WORKFLOW_DISPATCH: ${{ fromJson(needs.metadata.outputs.is_workflow_dispatch) }} | |
VIDEO_IS_WORKFLOW_CALL: ${{ fromJson(needs.metadata.outputs.is_workflow_call) }} | |
VIDEO_IS_SCHEDULED_DAILY: ${{ fromJson(needs.metadata.outputs.is_scheduled_daily) }} | |
VIDEO_IS_SCHEDULED_WEEKLY: ${{ fromJson(needs.metadata.outputs.is_scheduled_weekly) }} | |
VIDEO_TITLE: ${{ inputs.video_title }} | |
VIDEO_DESCRIPTION: ${{ inputs.video_description }} | |
VIDEO_FILENAME: ${{ inputs.video_filename }} | |
VIDEO_WIDTH: ${{ inputs.video_width }} | |
VIDEO_FPS: ${{ inputs.video_fps }} | |
VIDEO_CRF: ${{ inputs.video_crf }} | |
VIDEO_PRESET: ${{ inputs.video_preset }} | |
VIDEO_MOVFLAGS: ${{ inputs.video_movflags }} | |
run: | | |
# Create a temp directory for the input files | |
TIMELAPSE_DIR="$(mktemp -d -t timelapse-XXXXXXXXXX)" | |
for date in "${VIDEO_DATES[@]}"; do | |
mkdir -p "$TIMELAPSE_DIR/$date" | |
cp -r "assets/$date" "$TIMELAPSE_DIR" | |
done | |
# Resolve placeholders for title, description, and filename | |
dates=($VIDEO_DATES) | |
start_date="${dates[0]}" | |
end_date="${dates[-1]}" | |
date_range="$start_date" | |
if [ "$start_date" != "$end_date" ]; then | |
date_range="$start_date - $end_date" | |
fi | |
declare -A placeholders | |
placeholders=( | |
["{start_date}"]=$start_date | |
["{end_date}"]=$end_date | |
["{date_range}"]=$date_range | |
) | |
for key in "${!placeholders[@]}"; do | |
VIDEO_TITLE=${VIDEO_TITLE//$key/${placeholders[$key]}} | |
VIDEO_DESCRIPTION=${VIDEO_DESCRIPTION//$key/${placeholders[$key]}} | |
VIDEO_FILENAME=${VIDEO_FILENAME//$key/${placeholders[$key]}} | |
done | |
OUTPUT_PATH="assets/timelapse/$VIDEO_FILENAME" | |
mkdir -p "assets/timelapse" | |
if [ -f "$OUTPUT_PATH" ]; then | |
# If the file already exists, append a timestamp to the filename | |
OUTPUT_PATH="assets/timelapse/$(date +%s)_$VIDEO_FILENAME" | |
fi | |
echo "📝 Writing outputs and environment variables..." | |
echo "VIDEO_TITLE=$VIDEO_TITLE" >>$GITHUB_ENV | |
echo "VIDEO_DESCRIPTION=$VIDEO_DESCRIPTION" >>$GITHUB_ENV | |
echo "VIDEO_FILENAME=$VIDEO_FILENAME" >>$GITHUB_ENV | |
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 \ | |
-framerate "${VIDEO_FPS:-15}" \ | |
-pattern_type glob \ | |
-i "${TIMELAPSE_DIR-}/*/*.jpg" \ | |
-s:v "${VIDEO_WIDTH:-2048}" \ | |
-c:v libx264 \ | |
-crf $VIDEO_CRF \ | |
-preset "${VIDEO_PRESET:-"slow"}" \ | |
-tune film \ | |
-movflags "${VIDEO_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" \ | |
-y "${OUTPUT_PATH:-"assets/timelapse/timelapse_${RANDOM}.mp4"}" | |
# Cleanup | |
rm -rf "${TIMELAPSE_DIR}" | |
- name: Commit and Push | |
env: | |
OUTPUT_PATH: ${{ env.OUTPUT_PATH }} | |
VIDEO_TITLE: ${{ env.VIDEO_TITLE }} | |
START_DATE: ${{ needs.metadata.outputs.start_date }} | |
END_DATE: ${{ needs.metadata.outputs.end_date }} | |
TIMEFRAME: ${{ needs.metadata.outputs.timeframe }} | |
run: | | |
# create a new branch for our timelapse | |
BRANCH_NAME="timelapse/${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 Width** | %-56s |"$'\n' | |
COMMIT_BODY+="| **Video FPS** | %-56s |"$'\n' | |
COMMIT_BODY+="| **Video CRF** | %-56s |"$'\n' | |
COMMIT_BODY+="| **Preset** | %-56s |"$'\n' | |
COMMIT_BODY+="| **Movflags** | %-56s |"$'\n' | |
COMMIT_BODY="$( | |
printf "$COMMIT_BODY\n\n" \ | |
"Value" \ | |
"$VIDEO_TITLE" \ | |
"$VIDEO_DESCRIPTION" \ | |
"$VIDEO_FILENAME" \ | |
"$START_DATE" \ | |
"$END_DATE" \ | |
"$TIMEFRAME" \ | |
"$VIDEO_WIDTH" \ | |
"$VIDEO_FPS" \ | |
"$VIDEO_CRF" \ | |
"$VIDEO_PRESET" \ | |
"$VIDEO_MOVFLAGS" | |
)" | |
# create a new branch, commit, and push | |
git checkout -b "$BRANCH_NAME" | |
git add "$OUTPUT_PATH" | |
git commit -m "feat: 🎬 new timelapse for ${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)' "$START_DATE" "$END_DATE" "$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 }} |