Skip to content

Instantly share code, notes, and snippets.

@noln
Last active November 15, 2025 18:49
Show Gist options
  • Select an option

  • Save noln/39fd67ffbbf6d69ecb21d1d3a1f6c886 to your computer and use it in GitHub Desktop.

Select an option

Save noln/39fd67ffbbf6d69ecb21d1d3a1f6c886 to your computer and use it in GitHub Desktop.
Downloading videos from Google Photos in bulk results in the created date and last modified date being set to when the video was downloaded, not when it was created. This script takes a list of video files (as in the output of `ls -> files.txt` or suchlike), and if it recognises a timestamp in it, sets created date to that.
#!/usr/bin/env python3
"""
update_file_times.py
For every file in the current directory:
* If the name matches the GoPro pattern GX??????_#############.MP4
→ use the 13-digit millisecond epoch as the new timestamp.
* If the name matches the Pixel pattern PXL_YYYYMMDD_HHMMSSsss*.mp4
→ use YYYY-MM-DD HH:MM:SS.sss (milliseconds) as the new timestamp.
* Otherwise the file is skipped.
The script sets both the modification time (mtime) and the creation time
(birth time) on macOS (the only platform where birth time is mutable via
Python). On other platforms only mtime is changed.
Run with:
python3 update_file_times.py
"""
import os
import re
import sys
import time
from pathlib import Path
# ----------------------------------------------------------------------
# Regexes for the two filename patterns
# ----------------------------------------------------------------------
# GoPro: GX010140_1728651880764.MP4 → 13-digit ms epoch after the underscore
gopro_re = re.compile(r'^GX\d+_\d{13}\.MP4$', re.IGNORECASE)
# Pixel: PXL_20240816_140534848.mp4
# YYYYMMDD_HHMMSSsss (sss = milliseconds)
pixel_re = re.compile(
r'^PXL_(\d{8})_(\d{6})(\d{3})', re.IGNORECASE
)
def epoch_ms_to_ts(ms: int) -> float:
"""Convert milliseconds since epoch to a float timestamp for os.utime."""
return ms / 1000.0
def pixel_str_to_ts(date: str, time6: str, ms: str) -> float:
"""
Build a timestamp from the three captured groups:
date = '20240816'
time6 = '140534' (HHMMSS)
ms = '848' (milliseconds)
"""
dt_str = f"{date[:4]}-{date[4:6]}-{date[6:8]} {time6[:2]}:{time6[2:4]}:{time6[4:6]}.{ms}"
# strptime can parse the milliseconds part when we include the dot
return time.mktime(time.strptime(dt_str, "%Y-%m-%d %H:%M:%S.%f"))
def main() -> None:
cwd = Path.cwd()
updated = 0
skipped = 0
for entry in cwd.iterdir():
if not entry.is_file():
continue
fname = entry.name
ts = None
# ---- GoPro pattern ------------------------------------------------
m = gopro_re.match(fname)
if m:
epoch_ms = int(fname.split('_')[1].split('.')[0])
ts = epoch_ms_to_ts(epoch_ms)
# ---- Pixel camera pattern ------------------------------------------------
else:
m = pixel_re.match(fname)
if m:
ts = pixel_str_to_ts(m.group(1), m.group(2), m.group(3))
# ---- If we have a timestamp, apply it -----------------------------
if ts is not None:
try:
# os.utime sets mtime and atime; pass ns=(atime_ns, mtime_ns)
# macOS also honors birth time via os.setattr (Python 3.9+)
os.utime(entry, times=(ts, ts))
# macOS birth time (creation time)
if sys.platform == "darwin":
# Python 3.9+ provides os.setattr with follow_symlinks=False
try:
os.setattr(entry, "NS_BIRTHTIME", int(ts * 1_000_000_000))
except AttributeError:
# Fallback for older Python – use ctypes (optional)
pass
updated += 1
print(f"UPDATED {fname} → {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))}")
except Exception as e:
print(f"ERROR on {fname}: {e}", file=sys.stderr)
else:
skipped += 1
print("\nSummary")
print(f" Updated : {updated} files")
print(f" Skipped : {skipped} files")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment