The QuickTime:CreateDate field can be stored as UTC or as local time depending on camera firmware, making it unreliable without knowing the camera's behavior. The timecode of the first frame is always written by the camera's local clock and is unambiguous. Timecode provides only HH:MM:SS; the calendar date is resolved from Recorded_Date (local time with timezone offset) or Encoded_Date (converted UTC→local), with midnight-crossing detection when the timecode start-hour exceeds the end-time hour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -752,6 +752,64 @@ extract_file_date() {
|
||
| 752 | 752 |
local date_source="" |
| 753 | 753 |
local exif_found=0 |
| 754 | 754 |
|
| 755 |
+ # Timecode of first frame is the most reliable local-time source for video files: |
|
| 756 |
+ # it is written by the camera's own clock at the moment recording starts, with no |
|
| 757 |
+ # UTC-vs-local ambiguity. Try it first (requires mediainfo) before any other method. |
|
| 758 |
+ if [[ "$DATE_SOURCE" != "filesystem" ]] && command -v mediainfo &>/dev/null; then |
|
| 759 |
+ local raw_tc |
|
| 760 |
+ raw_tc=$(mediainfo --Inform="Other;%TimeCode_FirstFrame%" "$file" 2>/dev/null | head -1) |
|
| 761 |
+ if [[ "$raw_tc" =~ ^([0-9]{2}):([0-9]{2}):([0-9]{2}): ]]; then
|
|
| 762 |
+ local tc_hh="${BASH_REMATCH[1]}" tc_mm="${BASH_REMATCH[2]}" tc_ss="${BASH_REMATCH[3]}"
|
|
| 763 |
+ local dt_re='^([0-9]{4}-[0-9]{2}-[0-9]{2})[T ]([0-9]{2}):[0-9]{2}:[0-9]{2}'
|
|
| 764 |
+ local ref_date="" ref_hh="" |
|
| 765 |
+ |
|
| 766 |
+ # Primary: Recorded_Date includes timezone offset so its date+hour are already local. |
|
| 767 |
+ local recorded |
|
| 768 |
+ recorded=$(mediainfo --Inform="General;%Recorded_Date%" "$file" 2>/dev/null | head -1) |
|
| 769 |
+ if [[ "$recorded" =~ $dt_re ]]; then |
|
| 770 |
+ ref_date="${BASH_REMATCH[1]}"
|
|
| 771 |
+ ref_hh="${BASH_REMATCH[2]}"
|
|
| 772 |
+ else |
|
| 773 |
+ # Fallback: Encoded_Date is stored as UTC; convert to local to get the correct |
|
| 774 |
+ # reference date and hour for midnight-crossing detection. |
|
| 775 |
+ local encoded |
|
| 776 |
+ encoded=$(mediainfo --Inform="General;%Encoded_Date%" "$file" 2>/dev/null | head -1) |
|
| 777 |
+ encoded="${encoded% UTC}"
|
|
| 778 |
+ if [[ "$encoded" =~ $dt_re ]]; then |
|
| 779 |
+ local enc_date="${BASH_REMATCH[1]}" enc_time_str
|
|
| 780 |
+ enc_time_str="${encoded#* }"
|
|
| 781 |
+ if [[ "$OSTYPE" == "darwin"* ]]; then |
|
| 782 |
+ local utc_ts local_dt |
|
| 783 |
+ utc_ts=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$encoded" "+%s" 2>/dev/null) |
|
| 784 |
+ local_dt=$(date -j -r "$utc_ts" "+%Y-%m-%d %H" 2>/dev/null) |
|
| 785 |
+ ref_date="${local_dt% *}"
|
|
| 786 |
+ ref_hh="${local_dt#* }"
|
|
| 787 |
+ else |
|
| 788 |
+ ref_date=$(date -d "$encoded UTC" "+%Y-%m-%d" 2>/dev/null) |
|
| 789 |
+ ref_hh=$(date -d "$encoded UTC" "+%H" 2>/dev/null) |
|
| 790 |
+ fi |
|
| 791 |
+ fi |
|
| 792 |
+ fi |
|
| 793 |
+ |
|
| 794 |
+ if [[ -n "$ref_date" && -n "$ref_hh" ]]; then |
|
| 795 |
+ local start_date="$ref_date" |
|
| 796 |
+ # If timecode hour > end-time local hour, the clip crossed midnight: |
|
| 797 |
+ # subtract one day to get the recording-start calendar date. |
|
| 798 |
+ if (( 10#$tc_hh > 10#$ref_hh )); then |
|
| 799 |
+ if [[ "$OSTYPE" == "darwin"* ]]; then |
|
| 800 |
+ start_date=$(date -j -v-1d -f "%Y-%m-%d" "$ref_date" "+%Y-%m-%d" 2>/dev/null) |
|
| 801 |
+ else |
|
| 802 |
+ start_date=$(date -d "$ref_date - 1 day" "+%Y-%m-%d" 2>/dev/null) |
|
| 803 |
+ fi |
|
| 804 |
+ fi |
|
| 805 |
+ if [[ -n "$start_date" ]]; then |
|
| 806 |
+ echo "${start_date} ${tc_hh}:${tc_mm}:${tc_ss}|Timecode"
|
|
| 807 |
+ return 0 |
|
| 808 |
+ fi |
|
| 809 |
+ fi |
|
| 810 |
+ fi |
|
| 811 |
+ fi |
|
| 812 |
+ |
|
| 755 | 813 |
# Filesystem authoritative mode, and GoPro media in auto mode. |
| 756 | 814 |
# GoPro fallback order is THM, LRV, then the MP4 filesystem timestamp. |
| 757 | 815 |
if [[ "$DATE_SOURCE" == "filesystem" ]] || { [[ "$DATE_SOURCE" == "auto" ]] && should_prefer_gopro_filesystem_date "$file"; }; then
|