-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cfd0f51481d17f77586997/direnvrc" "sha256-YBzqskFZxmNb3kYVoKD9ZixoPXJh1C9ZvTLGFRkauZ0=" | ||
|
||
use devenv |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
|
||
# Devenv | ||
.devenv* | ||
devenv.local.nix | ||
|
||
/.pre-commit-config.yaml | ||
/.vscode | ||
/__pycache__ |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,20 @@ | ||
# py-modtime | ||
Simple Python script to change exif and modtime of files in a folder. | ||
|
||
I threw this together to work with some Google Takeout archives where it could walk through jpg's and update the modtime and exif data. | ||
|
||
# Using | ||
|
||
This package uses Nix and devenv, if you have devenv installed it's as simple as running `devenv shell` and then from there: | ||
|
||
`python update_image_dates.py` | ||
|
||
All of the dependencies are automatically managed / installed by Nix. | ||
|
||
# Known Issues | ||
|
||
- [ ] Exif data is being stripped | ||
|
||
# Credits | ||
|
||
[Mike Hindle, Unsplash](https://unsplash.com/photos/4PHsxHspavg) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
{ | ||
"nodes": { | ||
"devenv": { | ||
"locked": { | ||
"dir": "src/modules", | ||
"lastModified": 1687106808, | ||
"narHash": "sha256-RGDvxJA0bwfh6nx5qY/5CoDGZd13R4PIFG0JT729Yl4=", | ||
"owner": "cachix", | ||
"repo": "devenv", | ||
"rev": "93ee7875ff1622d496da80414724bae0263c1d23", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"dir": "src/modules", | ||
"owner": "cachix", | ||
"repo": "devenv", | ||
"type": "github" | ||
} | ||
}, | ||
"flake-compat": { | ||
"flake": false, | ||
"locked": { | ||
"lastModified": 1673956053, | ||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", | ||
"owner": "edolstra", | ||
"repo": "flake-compat", | ||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "edolstra", | ||
"repo": "flake-compat", | ||
"type": "github" | ||
} | ||
}, | ||
"flake-utils": { | ||
"inputs": { | ||
"systems": "systems" | ||
}, | ||
"locked": { | ||
"lastModified": 1685518550, | ||
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", | ||
"owner": "numtide", | ||
"repo": "flake-utils", | ||
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "numtide", | ||
"repo": "flake-utils", | ||
"type": "github" | ||
} | ||
}, | ||
"gitignore": { | ||
"inputs": { | ||
"nixpkgs": [ | ||
"pre-commit-hooks", | ||
"nixpkgs" | ||
] | ||
}, | ||
"locked": { | ||
"lastModified": 1660459072, | ||
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", | ||
"owner": "hercules-ci", | ||
"repo": "gitignore.nix", | ||
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "hercules-ci", | ||
"repo": "gitignore.nix", | ||
"type": "github" | ||
} | ||
}, | ||
"nixpkgs": { | ||
"locked": { | ||
"lastModified": 1687103638, | ||
"narHash": "sha256-dwy/TK6Db5W7ivcgmcxUykhFwodIg0jrRzOFt7H5NUc=", | ||
"owner": "NixOS", | ||
"repo": "nixpkgs", | ||
"rev": "91430887645a0953568da2f3e9a3a3bb0a0378ac", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "NixOS", | ||
"ref": "nixpkgs-unstable", | ||
"repo": "nixpkgs", | ||
"type": "github" | ||
} | ||
}, | ||
"nixpkgs-stable": { | ||
"locked": { | ||
"lastModified": 1685801374, | ||
"narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=", | ||
"owner": "NixOS", | ||
"repo": "nixpkgs", | ||
"rev": "c37ca420157f4abc31e26f436c1145f8951ff373", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "NixOS", | ||
"ref": "nixos-23.05", | ||
"repo": "nixpkgs", | ||
"type": "github" | ||
} | ||
}, | ||
"pre-commit-hooks": { | ||
"inputs": { | ||
"flake-compat": "flake-compat", | ||
"flake-utils": "flake-utils", | ||
"gitignore": "gitignore", | ||
"nixpkgs": [ | ||
"nixpkgs" | ||
], | ||
"nixpkgs-stable": "nixpkgs-stable" | ||
}, | ||
"locked": { | ||
"lastModified": 1686668298, | ||
"narHash": "sha256-AADh9NqHh6X2LOem4BvI7oCkMm+JPCSCE7iIw5nn0VA=", | ||
"owner": "cachix", | ||
"repo": "pre-commit-hooks.nix", | ||
"rev": "5b6b54d3f722aa95cbf4ddbe35390a0af8c0015a", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "cachix", | ||
"repo": "pre-commit-hooks.nix", | ||
"type": "github" | ||
} | ||
}, | ||
"root": { | ||
"inputs": { | ||
"devenv": "devenv", | ||
"nixpkgs": "nixpkgs", | ||
"pre-commit-hooks": "pre-commit-hooks" | ||
} | ||
}, | ||
"systems": { | ||
"locked": { | ||
"lastModified": 1681028828, | ||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", | ||
"owner": "nix-systems", | ||
"repo": "default", | ||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", | ||
"type": "github" | ||
}, | ||
"original": { | ||
"owner": "nix-systems", | ||
"repo": "default", | ||
"type": "github" | ||
} | ||
} | ||
}, | ||
"root": "root", | ||
"version": 7 | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ pkgs, ... }: | ||
|
||
{ | ||
# https://devenv.sh/packages/ | ||
packages = [ | ||
pkgs.git | ||
pkgs.python310Packages.pip | ||
pkgs.python310Packages.pillow | ||
pkgs.python310Packages.piexif | ||
pkgs.python310Packages.pytest | ||
]; | ||
|
||
enterShell = '' | ||
python --version | ||
''; | ||
|
||
# https://devenv.sh/languages/ | ||
languages.python.enable = true; | ||
|
||
# https://devenv.sh/pre-commit-hooks/ | ||
pre-commit.hooks = { | ||
# lint shell scripts | ||
shellcheck.enable = true; | ||
|
||
# format Python code | ||
black.enable = true; | ||
}; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
inputs: | ||
nixpkgs: | ||
url: github:NixOS/nixpkgs/nixpkgs-unstable |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import os | ||
import shutil | ||
import tempfile | ||
import unittest | ||
from datetime import datetime | ||
from PIL import Image | ||
from update_image_dates import update_image_dates | ||
|
||
|
||
class TestUpdateImageDates(unittest.TestCase): | ||
def setUp(self): | ||
# create temporary directory and files for testing | ||
self.temp_dir = tempfile.TemporaryDirectory() | ||
self.temp_files = [ | ||
"2022-01-01.jpg", | ||
"2022_01_02(1).jpg", | ||
"20220103_120000_1.jpg", | ||
] | ||
for filename in self.temp_files: | ||
shutil.copyfile( | ||
"assets/unsplash.jpg", os.path.join(self.temp_dir.name, filename) | ||
) | ||
|
||
# check that there is a file in the temporary directory for each filename | ||
for filename in self.temp_files: | ||
if not os.path.isfile(os.path.join(self.temp_dir.name, filename)): | ||
raise ValueError(f"File {filename} was not created.") | ||
|
||
def tearDown(self): | ||
# delete temporary directory and files | ||
self.temp_dir.cleanup() | ||
|
||
def test_update_image_dates(self): | ||
# run the function on the temporary directory | ||
update_image_dates(os.path.abspath(self.temp_dir.name)) | ||
|
||
# check that the modification times and EXIF data were updated correctly | ||
for filename in self.temp_files: | ||
if not os.path.isfile(os.path.join(self.temp_dir.name, filename)): | ||
raise ValueError(f"File {filename} was not created.") | ||
|
||
filepath = os.path.join(self.temp_dir.name, filename) | ||
img = Image.open(filepath) | ||
exif = img._getexif() | ||
if exif is None: | ||
raise ValueError(f"EXIF data not found in {filepath}") | ||
|
||
# TODO: figure out why the EXIF data is not being updated. | ||
print(f"Checking {filepath}") | ||
print(f" EXIF data: {exif}") | ||
|
||
# check modification time | ||
mtime = os.path.getmtime(filepath) | ||
self.assertEqual(datetime.fromtimestamp(mtime), exif[306]) | ||
|
||
# check EXIF data | ||
self.assertEqual(exif[36867], exif[36868]) | ||
self.assertEqual(exif[36867], exif[306]) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import os | ||
import re | ||
import sys | ||
from datetime import datetime | ||
from PIL import Image, ExifTags | ||
from piexif import dump | ||
import time | ||
|
||
# pattern to match the three formats: | ||
# 1. YYYY-MM-DD.jpg | ||
# 2. YYYY_MM_DD(int).jpg | ||
# 3. YYYYMMDD_HHMMSS_int.jpg | ||
pattern = re.compile( | ||
r"(?P<year>\d{4})(?:[-_](?P<month>\d{2})[-_](?P<day>\d{2})|(?P<monthday>\d{4}))?(?:\((?P<count>\d+)\))?(_(?P<hour>\d{2})(?P<minute>\d{2})(?P<second>\d{2})_)?" | ||
) | ||
|
||
|
||
def update_image_dates(dir_path): | ||
if dir_path is None: | ||
print("No directory path provided.") | ||
sys.exit(1) | ||
|
||
# check if the path is a directory | ||
if not os.path.isdir(dir_path): | ||
print(f"Path {dir_path} is not a directory.") | ||
sys.exit(1) | ||
|
||
for filename in os.listdir(dir_path): | ||
path = os.path.join(dir_path, filename) | ||
if not os.path.isfile(path): | ||
print(f"Skipping {path} because it is not a file.") | ||
continue | ||
if path.endswith(".jpg"): | ||
update_image_date(path) | ||
|
||
|
||
def update_image_date(filepath): | ||
match = pattern.search(os.path.basename(filepath)) | ||
if match: | ||
( | ||
year, | ||
month, | ||
day, | ||
monthday, | ||
unknown1, | ||
unknown2, | ||
hour, | ||
minute, | ||
second, | ||
*extra, | ||
) = match.groups() | ||
|
||
if len(extra) > 3: | ||
raise ValueError(f"Too many values in match.groups(): {extra}") | ||
|
||
if monthday is not None: | ||
month, day = monthday[:2], monthday[2:] | ||
|
||
if hour is None: | ||
hour = minute = second = "00" | ||
|
||
# create datetime object | ||
dt = datetime( | ||
int(year), int(month), int(day), int(hour), int(minute), int(second) | ||
) | ||
|
||
# open image | ||
img = Image.open(filepath) | ||
exif_dict = img._getexif() | ||
if exif_dict is not None: | ||
for tag, value in img._getexif().items(): | ||
if tag in ExifTags.TAGS: | ||
exif_dict[ExifTags.TAGS[tag]] = exif_dict.pop(tag) | ||
else: | ||
exif_dict = {} | ||
|
||
# update EXIF data | ||
dt = datetime( | ||
int(year), int(month), int(day), int(hour), int(minute), int(second) | ||
) | ||
fmtd = dt.strftime("%Y:%m:%d %H:%M:%S") | ||
exif_dict["DateTimeOriginal"] = fmtd | ||
exif_dict["DateTimeDigitized"] = fmtd | ||
exif_dict["DateTime"] = fmtd | ||
|
||
print( | ||
f"Updating {filepath} to {year}-{month}-{day} {hour}:{minute}:{second} with EXIF data {exif_dict}" | ||
) | ||
|
||
# prepare EXIF data | ||
exif_bytes = dump(exif_dict) | ||
|
||
# save image with new EXIF data | ||
img.save(filepath, "jpeg", exif=exif_bytes) | ||
|
||
# change file modification time | ||
os.utime(filepath, (dt.timestamp(), dt.timestamp())) | ||
|
||
|
||
if __name__ == "__main__": | ||
dir_path = input("Enter the directory path (eg. ~/Pictures): ") | ||
update_image_dates(dir_path) |