Skip to content

Generate 3day Timelapse for #13

Generate 3day Timelapse for

Generate 3day Timelapse for #13

Workflow file for this run

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
TOTAL_DATES="${#all_dates[@]}"
MAX_TIMEFRAME=60
if ((VIDEO_TIMEFRAME > TOTAL_DATES)); then
VIDEO_TIMEFRAME="$TOTAL_DATES"
elif ((VIDEO_TIMEFRAME > MAX_TIMEFRAME)); then
VIDEO_TIMEFRAME="$MAX_TIMEFRAME"
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 $(jq -r '.[]' <<<"${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 }}