Skip to content

Instantly share code, notes, and snippets.

@gretel
Created July 11, 2024 00:15
Show Gist options
  • Save gretel/83a9d4ec3024cc8e07af45837a9a0ec6 to your computer and use it in GitHub Desktop.
Save gretel/83a9d4ec3024cc8e07af45837a9a0ec6 to your computer and use it in GitHub Desktop.
Video File Date Adjustment Script - set file data according to the metadata of the video content
#!/bin/sh
# Video File Date Adjustment Script
#
# This script traverses a directory (and its subdirectories) to process video files
# (mp4, avi, mkv, mov) and their containing directories. For each video file, it:
#
# 1. Extracts the creation date from the file's metadata, prioritizing:
# a) com.apple.quicktime.creationdate
# b) container metadata
# c) video stream metadata
# 2. Checks if the file's current creation time is valid (not in the future).
# 3. If metadata is available and valid, sets the file's creation time to match the metadata.
# 4. If metadata is unavailable or the date is in the future, sets the creation time to current time minus one minute.
# 5. Sets the file's modification time to the current time if any changes were made.
# 6. Updates the containing directory's modification time to match the oldest file within it.
#
# The script outputs a line for each processed video file in the format:
# filename|status|set_date|file_creation_time
#
# If any errors occur during processing, they are logged to stderr.
#
# Usage:
# ./videodate.sh [directory_path]
#
# If no directory path is provided, the script processes the current directory.
#
# Dependencies: ffprobe (part of ffmpeg), date, stat, touch, find, sed, cut, basename, dirname
#
# Note: This script is designed for FreeBSD systems. Some modifications may be
# needed for other Unix-like systems.
set -e
filename_width=50
error_exit() {
echo "error: $1" >&2
exit 1
}
check_dependencies() {
required_commands="ffprobe date stat touch find sed cut basename dirname"
for cmd in $required_commands; do
if ! command -v "$cmd" > /dev/null 2>&1; then
error_exit "required command '$cmd' not found. please install it and try again."
fi
done
if ! ffprobe -version | grep -q "ffprobe version"; then
error_exit "ffprobe found, but unable to determine version. please ensure ffmpeg is correctly installed."
fi
}
truncate_name() {
name="$1"
if [ ${#name} -gt $filename_width ]; then
truncated=$(echo "$name" | cut -c1-$((filename_width-3)))
printf "%-${filename_width}s" "${truncated}..."
else
printf "%-${filename_width}s" "$name"
fi
}
get_creation_date() {
file="$1"
ffprobe_output=$(ffprobe -v quiet -print_format json -show_format -show_streams "$file" 2>/dev/null)
# Check for QuickTime creation date
quicktime_date=$(echo "$ffprobe_output" | sed -n 's/.*"com.apple.quicktime.creationdate": "\([^"]*\)".*/\1/p')
if [ -n "$quicktime_date" ]; then
echo "quicktime|$quicktime_date"
return
fi
# Check for container metadata
container_date=$(echo "$ffprobe_output" | sed -n 's/.*"creation_time": "\([^"]*\)".*/\1/p' | head -n 1)
if [ -n "$container_date" ]; then
echo "container|$container_date"
return
fi
# Check for stream metadata
stream_date=$(echo "$ffprobe_output" | sed -n 's/.*"tags": {[^}]*"creation_time": "\([^"]*\)".*/\1/p' | head -n 1)
if [ -n "$stream_date" ]; then
echo "stream|$stream_date"
return
fi
echo "" # return empty string if no date found
}
print_aligned_output() {
printf "%s|%-25s|%-29s|%s\n" "$1" "$2" "$3" "$4"
}
process_file() {
file="$1"
if [ ! -f "$file" ]; then
echo "error: file does not exist: $file" >&2
return
fi
filename=$(basename "$file")
# Skip files starting with a dot
case "$filename" in
._*) return ;;
esac
truncated_filename=$(truncate_name "$filename")
current_time=$(date +%s)
one_minute_ago=$((current_time - 60))
metadata_result=$(get_creation_date "$file")
if [ -z "$metadata_result" ]; then
new_time=$one_minute_ago
status="no_metadata_set_recent"
else
metadata_source=$(echo "$metadata_result" | cut -d'|' -f1)
metadata_date=$(echo "$metadata_result" | cut -d'|' -f2)
formatted_date=$(echo "$metadata_date" | sed 's/\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\)T\([0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\).*/\1 \2/')
if [ -z "$formatted_date" ]; then
new_time=$one_minute_ago
status="date_format_failed_set_recent"
else
if ! metadata_seconds=$(date -j -f "%Y-%m-%d %H:%M:%S" "$formatted_date" "+%s" 2>/dev/null); then
new_time=$one_minute_ago
status="date_parse_failed_set_recent"
else
if [ "$metadata_seconds" -gt "$current_time" ]; then
new_time=$one_minute_ago
status="future_date_set_recent"
else
new_time=$metadata_seconds
status="${metadata_source}_metadata"
fi
fi
fi
fi
current_birth_time=$(stat -f "%B" "$file")
if [ "$current_birth_time" -eq "$new_time" ]; then
print_aligned_output "$truncated_filename" "already_correct" "$(date -r "$new_time")" "$(date -r "$current_birth_time")"
return
fi
if ! touch -h -t "$(date -r "$new_time" "+%Y%m%d%H%M.%S")" "$file"; then
echo "error: failed to update creation time for $file" >&2
print_aligned_output "$truncated_filename" "update_failed" "$(date -r "$new_time")" ""
return
fi
if ! touch -h -m -t "$(date -r "$current_time" "+%Y%m%d%H%M.%S")" "$file"; then
echo "error: failed to update modification time for $file" >&2
print_aligned_output "$truncated_filename" "mod_time_update_failed" "$(date -r "$new_time")" ""
return
fi
new_birth_time=$(stat -f "%B" "$file")
print_aligned_output "$truncated_filename" "$status" "$(date -r "$new_time")" "$(date -r "$new_birth_time")"
# Update the containing directory
dir=$(dirname "$file")
update_directory "$dir"
}
update_directory() {
dir="$1"
if [ ! -d "$dir" ]; then
echo "error: directory does not exist: $dir" >&2
return
fi
# Find the oldest file in the directory
oldest_file=$(find "$dir" -type f -not -name ".*" -exec stat -f "%m %N" {} + | sort -n | head -1 | cut -d' ' -f2-)
if [ -z "$oldest_file" ]; then
return
fi
oldest_time=$(stat -f "%m" "$oldest_file")
current_dir_time=$(stat -f "%m" "$dir")
if [ "$current_dir_time" -ne "$oldest_time" ]; then
if ! touch -h -t "$(date -r "$oldest_time" "+%Y%m%d%H%M.%S")" "$dir"; then
echo "error: failed to update directory time for $dir" >&2
fi
fi
}
traverse_directory() {
if [ ! -d "$1" ]; then
error_exit "not a directory: $1"
fi
# Print header
print_aligned_output "File" "Status" "Set Date" "Actual Creation Time"
find "$1" -type f \( -name "*.mp4" -o -name "*.avi" -o -name "*.mkv" -o -name "*.mov" \) | while read -r file; do
process_file "$file" || echo "error: failed to process file: $file" >&2
done
}
# main script
check_dependencies
if [ $# -eq 0 ]; then
start_dir="."
elif [ $# -eq 1 ]; then
start_dir="$1"
if [ ! -d "$start_dir" ]; then
error_exit "specified path is not a directory: $start_dir"
fi
else
error_exit "usage: $0 [directory_path]"
fi
traverse_directory "$start_dir"
echo "processing complete."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment