Last active
November 15, 2025 18:49
-
-
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.
This file contains hidden or 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
| #!/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