Generate 3day Timelapse for 2023-10-06 #8
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 -j -v+${timeframe}d -f "%Y-%m-%d" "$start_date" +%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 -j -v-${timeframe}d -f "%Y-%m-%d" "$end_date" +%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 -j -v+${timeframe}d -f "%Y-%m-%d" "$start_date" +%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: brew install ffmpeg | |
- 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 "path=$OUTPUT_PATH" >>$GITHUB_OUTPUT | |
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 | |
# Generate the timelapse | |
ffmpeg \ | |
-framerate $VIDEO_FPS \ | |
-pattern_type glob \ | |
-i "$TIMELAPSE_DIR/*/*.jpg" \ | |
-s:v $VIDEO_WIDTH \ | |
-c:v libx264 \ | |
-crf $VIDEO_CRF \ | |
-preset $VIDEO_PRESET \ | |
-tune film \ | |
-movflags $VIDEO_MOVFLAGS \ | |
-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" | |
# Cleanup | |
rm -rf "$TIMELAPSE_DIR" | |
- name: Commit and Push | |
env: | |
OUTPUT_PATH: ${{ env.OUTPUT_PATH }} | |
VIDEO_TITLE: ${{ env.VIDEO_TITLE }} | |
run: | | |
# Upload the timelapse to the repository | |
git add "$OUTPUT_PATH" | |
git commit -m "🎬 new timelapse: '$VIDEO_TITLE'" | |
git push | |
- 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 }} |