Skip to content

Instantly share code, notes, and snippets.

@cedricvidal
Last active February 13, 2023 17:28
Show Gist options
  • Save cedricvidal/1354bdceb0bbc9c5099e0207f0620fb4 to your computer and use it in GitHub Desktop.
Save cedricvidal/1354bdceb0bbc9c5099e0207f0620fb4 to your computer and use it in GitHub Desktop.
Fix Apple Photos timestamps Applescript script
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions
# This script fixes timestamps of photos currently selected in OSX Photos application.
# It does so by reading a CSV file containing mappings from file names to timestamps.
# The user is requested to select the CSV file when running this script.
# Author: Cedric Vidal (https://vidal.biz)
# Usage:
# 1. Select in Apple Photos the photos and videos for which you want to fix the timestamp.
# Start with a small test subset to verify that the script is working as expected.
# 2. Run the script
# 3. Select the CSV file containing the mapping from file name to timestamp
# 4. The script will load the mapping, this can take a while if it contains
# thousands of entries or more. Be patient. For each photo or video in
# the selection, it will try to find a CSV mapping entry with the same
# filename (the part before the last "/" is ignored). If there is an entry,
# it will parse the timestamp as a date and update the photo's date. Otherwise,
# it will simply skip the photo and carry on.
# Warning: No validation of the CSV file is done!
# The CSV must follow the example:
#./Pictures/Instagram/IMG_20191006_160831_485.jpg,2019:10:10 06:53:01-07:00
#./Pictures/Instagram/IMG_20191007_080007_040.jpg,2019:10:10 06:53:01-07:00
#./Pictures/Instagram/IMG_20190919_211953_686.jpg,2019:10:10 06:53:02-07:00
# Only the filename is used for matching, the full path is ignored so it may
# set wrong timestamps if two files have the same name but different paths and timestamps.
# The method to extract the timestamps can vary and is out of scope of
# this script.
on run
log ("Loading mapping")
set mapping to loadMapping()
log ("Loading done")
tell application "Photos"
activate
set imageSel to (get selection) # Media Item class = PHAsset class
repeat with i from 1 to length of imageSel
# Documentation for PhotosKit's Media Item class returned by selection
# https://developer.apple.com/documentation/photos/phasset
set img to item i of imageSel
set img_date to (the date of img) as date
set img_key to (the filename of img) as string
set img_date_fix to my getDateFromDict(img_key, mapping)
if ((missing value ≠ img_date_fix) and ((missing value is img_date) or (img_date_fix < img_date))) then
log ("Fixing File " & (img_key) & " -> " & (img_date) & " -> " & img_date_fix)
set the date of img to img_date_fix
else
log ("Skipping File " & (img_key) & " -> " & (img_date) & " -> " & img_date_fix)
end if
end repeat
end tell
log ("Done")
end run
on getDateFromDict(k, dct)
set dateStr to getFromDict(k, dct)
if missing value = dateStr then
missing value
else
parseDate(dateStr)
end if
end getDateFromDict
on parseDate(dateStr)
set the text item delimiters to {"-", ":", " "}
set dateArray to items 1 through 3 of text items of dateStr
set dateObj to {item 2 of dateArray, item 3 of dateArray, item 1 of dateArray}
set dateDate to date ("" & dateObj)
set the text item delimiters to space
dateDate
end parseDate
on loadMapping()
set DELIM to {","}
set Ly to {}
set acsv to (choose file)
set csvList to read acsv using delimiter linefeed
set {TID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, DELIM}
repeat with arow in csvList
set ritems to every text item of arow
set rpath to (item 1 of ritems)
set rfilename to getFilename(rpath)
set rdate to (item 2 of ritems)
set end of Ly to {rfilename, rdate}
end repeat
set AppleScript's text item delimiters to TID
dictFromList(Ly)
end loadMapping
on getFilename(path)
set {TID, text item delimiters} to {AppleScript's text item delimiters, "/"}
set pathArray to text items of path
set filename to last item of pathArray
set text item delimiters to TID
filename
end getFilename
-- REUSABLE GENERAL FUNCTIONS ---------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript
-- Just :: a -> Maybe a
on Just(x)
-- Constructor for an inhabited Maybe (option type) value.
-- Wrapper containing the result of a computation.
{type:"Maybe", Nothing:false, Just:x}
end Just
-- Nothing :: Maybe a
on Nothing()
-- Constructor for an empty Maybe (option type) value.
-- Empty wrapper returned where a computation is not possible.
{type:"Maybe", Nothing:true}
end Nothing
-- assocs :: Map k a -> [(k, a)]
on assocs(m)
set c to class of m
if list is c then
zip(enumFromTo(1, length of m), m)
else if record is c then
tell current application to set dict to ¬
dictionaryWithDictionary_(m) of its NSDictionary
zip((sortedArrayUsingSelector_("compare:") of ¬
allKeys() of dict) as list, ¬
(allValues() of dict) as list)
else
{}
end if
end assocs
-- deleteKey :: k -> Dict -> Dict
on deleteKey(k, rec)
tell current application to set nsDct to ¬
dictionaryWithDictionary_(rec) of its NSMutableDictionary
removeObjectForKey_(k) of nsDct
nsDct as record
end deleteKey
-- elems :: Map k a -> [a]
-- elems :: Set a -> [a]
on elems(x)
if record is class of x then -- Dict
tell current application to allValues() ¬
of dictionaryWithDictionary_(x) ¬
of its NSDictionary as list
else -- Set
(allObjects() of x) as list
end if
end elems
-- enumFromTo :: Int -> Int -> [Int]
on enumFromTo(m, n)
if m ≤ n then
set lst to {}
repeat with i from m to n
set end of lst to i
end repeat
lst
else
{}
end if
end enumFromTo
-- dictFromList :: [(k, v)] -> Dict
on dictFromList(kvs)
set tpl to unzip(kvs)
script go
on |λ|(x)
x as string
end |λ|
end script
tell current application
(its (NSDictionary's dictionaryWithObjects:(item 2 of tpl) ¬
forKeys:(my map(go, item 1 of tpl)))) as record
end tell
end dictFromList
-- identity :: a -> a
on identity(x)
-- The argument unchanged.
x
end identity
-- insertDict :: String -> a -> Dict -> Dict
on insertDict(k, v, rec)
tell current application
tell dictionaryWithDictionary_(rec) of its NSMutableDictionary
its setValue:v forKey:(k as string)
it as record
end tell
end tell
end insertDict
-- keys :: Dict -> [String]
on keys(rec)
tell current application
(allKeys() of its (NSDictionary's dictionaryWithDictionary:rec)) as list
end tell
end keys
-- lookupDict :: a -> Dict -> Maybe b
on lookUpDict(k, dct)
-- Just the value of k in the dictionary,
-- or Nothing if k is not found.
set ca to current application
set v to (ca's NSDictionary's dictionaryWithDictionary:dct)'s objectForKey:k
if missing value ≠ v then
Just(item 1 of ((ca's NSArray's arrayWithObject:v) as list))
else
Nothing()
end if
end lookUpDict
on getFromDict(k, dct)
-- Just the value of k in the dictionary,
-- or Nothing if k is not found.
set ca to current application
set v to (ca's NSDictionary's dictionaryWithDictionary:dct)'s objectForKey:k
if missing value ≠ v then
item 1 of ((ca's NSArray's arrayWithObject:v) as list)
else
missing value
end if
end getFromDict
-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
-- The list obtained by applying f
-- to each element of xs.
tell mReturn(f)
set lng to length of xs
set lst to {}
repeat with i from 1 to lng
set end of lst to |λ|(item i of xs, i, xs)
end repeat
return lst
end tell
end map
-- maybe :: b -> (a -> b) -> Maybe a -> b
on maybe(v, f, mb)
-- The 'maybe' function takes a default value, a function, and a 'Maybe'
-- value. If the 'Maybe' value is 'Nothing', the function returns the
-- default value. Otherwise, it applies the function to the value inside
-- the 'Just' and returns the result.
if Nothing of mb then
v
else
tell mReturn(f) to |λ|(Just of mb)
end if
end maybe
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
-- 2nd class handler function lifted into 1st class script wrapper.
if script is class of f then
f
else
script
property |λ| : f
end script
end if
end mReturn
-- min :: Ord a => a -> a -> a
on min(x, y)
if y < x then
y
else
x
end if
end min
-- unzip :: [(a,b)] -> ([a],[b])
on unzip(xys)
set xs to {}
set ys to {}
repeat with xy in xys
set end of xs to item 1 of xy
set end of ys to item 2 of xy
end repeat
return {xs, ys}
end unzip
-- zip :: [a] -> [b] -> [(a, b)]
on zip(xs, ys)
set lng to min(length of xs, length of ys)
set lst to {}
repeat with i from 1 to lng
set end of lst to {item i of xs, item i of ys}
end repeat
return lst
end zip
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment