Created
December 28, 2019 16:34
-
-
Save akrabat/ebc66501744cb7fa0871cf8255de09b9 to your computer and use it in GitHub Desktop.
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
-- | |
-- For each album, add a hierarchical keyword of the format | |
-- "PhotosExport>folder1>folder2>album" to each photo. | |
-- | |
-- Copyright 2019 Rob Allen. | |
-- License: MIT - https://akrabat.com/license/mit/ | |
-- | |
-- Variables to control how many albums to process per run | |
-- change appropriately per run if you need to do in batches | |
set theStartAlbumIndex to 1 -- theStartAlbumIndex is 1 based, not zero based | |
set theNumberOfAlbumsToProcess to 1000000 | |
global theLogFile | |
set theLogFile to ("/Users/rob/Desktop/" as POSIX file as text) & "KeywordAlbum.log" | |
-- Variables to keep track of what we're doing | |
set theCount to 1 | |
set theEndAlbumIndex to theStartAlbumIndex + theNumberOfAlbumsToProcess | |
set mgStart to current date | |
set thePhotoCount to 0 | |
-- top level albums to skip with "PhotosExport>" prefix added | |
set albumsToSkip to {"PhotosExport>Search", "PhotosExport>Today", "PhotosExport>Camera Remote", "PhotosExport>Instagram", "PhotosExport>PlayMemories Mobile"} | |
my logThis("Started. Processing " & theNumberOfAlbumsToProcess & " albumns starting from album " & theStartAlbumIndex) | |
with timeout of 72000 seconds | |
tell application "Photos" | |
set allAlbums to albums | |
my logThis("Number of Albums: " & (count of allAlbums)) | |
repeat with theAlbum in allAlbums | |
-- Start fake loop to simulate "continue". See https://stackoverflow.com/a/1035260/23060 | |
repeat 1 times | |
if theCount is less than theStartAlbumIndex then | |
exit repeat -- "continue" | |
end if | |
-- Get the hierarchy keyword | |
set theKeyword to my getAlbumKeyword(theAlbum) | |
set theMsg to "" & theCount & ": Album: " & name of theAlbum & ", " & theKeyword | |
if albumsToSkip contains theKeyword then | |
set theMsg to theMsg & ". *** Skipping ***" | |
my logThis(theMsg) | |
exit repeat -- "continue" | |
end if | |
my logThis(theMsg) | |
-- get all the photos in this album and add the keywords | |
set thePhotos to every media item in theAlbum | |
repeat with thePhoto in thePhotos | |
set thePhotoCount to thePhotoCount + 1 | |
my addKeywordsToItem(thePhoto, theKeyword) | |
-- log "Added keywords to '" & filename of thePhoto & "'" | |
end repeat | |
end repeat -- End of simulation of continue | |
set theCount to theCount + 1 | |
if theCount is theEndAlbumIndex then | |
exit repeat | |
end if | |
end repeat | |
end tell | |
end timeout | |
set mgStop to current date | |
my logThis("Number of photos processed: " & thePhotoCount) | |
my logThis("Finished. This run took " & (mgStop - mgStart) & " seconds.") | |
my writeToFile("", theLogFile, true) | |
-- FINISHED | |
on getAlbumKeyword(targetObject) | |
set theLevel to 1 | |
set theParents to {} | |
-- start with the album name | |
set theKeword to the name of targetObject | |
-- interate up over all parents of targetObject | |
tell application "Photos" | |
repeat | |
try | |
set theName to the name of parent of targetObject | |
set thisID to id of parent of targetObject | |
-- prefix with parent name | |
set theKeword to theName & ">" & theKeword | |
-- update targetObject to be its parent | |
if class of parent of targetObject is folder then | |
set targetObject to folder id thisID | |
else if class of parent of targetObject is album then | |
set targetObject to album id thisID | |
end if | |
on error | |
-- all done: no parent of targetObject | |
set theKeword to "PhotosExport>" & theKeword | |
return theKeword | |
end try | |
end repeat | |
end tell | |
end getAlbumKeyword | |
on addKeywordsToItem(mediaItem, theKeyword) | |
tell application "Photos" | |
set existingKeywords to (keywords of mediaItem) | |
if existingKeywords is missing value then | |
set existingKeywords to {} | |
end if | |
set (keywords of mediaItem) to my uniqueItems(existingKeywords & {theKeyword}) | |
end tell | |
end addKeywordsToItem | |
on uniqueItems(a) | |
-- from https://macscripter.net/viewtopic.php?id=8372 | |
-- A "Serge" object. Accessing items of a list is done | |
-- faster with object references. Why? We don't know. | |
script o | |
property p : a | |
end script | |
-- "Serge" doesn't speed up appending to a list | |
set b to {} | |
repeat with i from 1 to a's length | |
-- A "Garvey" tell statement. In some situations, the 2 calls | |
-- to "it" are faster than 2 calls to "o's p's item i". | |
tell o's p's item i to if ({it} is not in b) then set b's end to it | |
end repeat | |
return b | |
end uniqueItems | |
-- /////////////////////////////////////////// | |
-- // LOGGING | |
-- /////////////////////////////////////////// | |
on getCurrentTimestamp(theDate) | |
set yyyy to text -4 thru -1 of ("0000" & (year of theDate)) | |
set mm to text -2 thru -1 of ("00" & ((month of theDate) as integer)) | |
set dd to text -2 thru -1 of ("00" & (day of theDate)) | |
set hh to text -2 thru -1 of ("00" & (hours of theDate)) | |
set mins to text -2 thru -1 of ("00" & (minutes of theDate)) | |
set ss to text -2 thru -1 of ("00" & (seconds of theDate)) | |
return yyyy & ":" & mm & ":" & dd & ":" & hh & ":" & mins & ":" & ss | |
end getCurrentTimestamp | |
on logThis(theText) | |
set theText to (my getCurrentTimestamp((current date))) & ": " & theText | |
log theText --to console | |
my writeToFile(theText, theLogFile, true) -- and persist to log file | |
end logThis | |
on writeToFile(thisData, targetFile, shouldAppend) -- (string, file path as string, boolean) | |
try | |
set the targetFile to the targetFile as text | |
set the openTargetFile to open for access file targetFile with write permission | |
if shouldAppend is false then set eof of the openTargetFile to 0 | |
-- write the line and a \n character .. | |
write thisData & return to the openTargetFile starting at eof | |
close access the openTargetFile | |
return true | |
on error errorMessage number errorNumber | |
log "Exception logging. Details: " & errorMessage & " Error number " & errorNumber & ". Data to be written was: " & thisData | |
try | |
close access file targetFile | |
end try | |
return false | |
end try | |
end writeToFile | |
on convertListToString(theList) | |
set AppleScript's text item delimiters to ", " | |
set theString to theList as string | |
set AppleScript's text item delimiters to "" | |
return theString | |
end convertListToString | |
Great script. Thank you! However it only operates on top-level albums and not recursive within folders, so the foldername>albumname keyword isn't applied on images in albums within folders. Am I doing something wrong or has Apple changed something in Ventura?
Apple changed something, but I've not needed it recently, so haven't looked into it. This comment on my blog has one solution.
Thank you!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for sharing this information, Rob! I ran your KeywordAlbum.applescript on my Photos library consisting of 23 819 images across 307 albums. It worked like a charm, even with Norwegian letters and special characters in file- and album names. Zero errors. I have a mixture of RAW + JPEG and PNGs from different cameras, including Nikon, Canon and Leica. You really saved me a lot of hassle.