Last active
February 1, 2021 14:24
-
-
Save SidShetye/ddf9b9870ea0ddb4bf112a58b0b0fbcc to your computer and use it in GitHub Desktop.
Scan through all albums and export media items (movies, images etc) into a suitable folder. There is custom logic to pick the appropriate destination folder.
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
------------------------------------------------ | |
-- Settings Start: Change these as needed | |
global gDest | |
set gDest to "/Volumes/MacPhotos/Pictures/ExportAlbums/" as POSIX file as text -- the destination folder (use a valid path) | |
global gLogFile | |
set gLogFile to gDest & "ExportAlbumToFolders.log" | |
global gKeywordOnSuccess | |
set gKeywordOnSuccess to "exported" | |
-- Name of our special unsorted/catch-all album. We'll group images within into YYYY-MM folders | |
-- if needed use 'smart albums' to create an album with this name (tip: try the 'album is not any' rule) | |
global gUnsortedAlbum | |
set gUnsortedAlbum to "unsorted" | |
set allowUserToSelectAlbums to false as boolean | |
-- Settings End | |
------------------------------------------------ | |
my makeFolder(gDest) | |
tell application "Photos" | |
set allAlbumNames to name of albums | |
if allowUserToSelectAlbums then | |
set albumNames to choose from list allAlbumNames with prompt "Select some albums" with multiple selections allowed | |
-- DEBUGGING | |
--set albumNames to {gUnsortedAlbum} | |
else | |
set albumNames to allAlbumNames | |
end if | |
-- Sort for some deterministic pattern we as humans can follow | |
set albumNames to my sortList(albumNames) | |
if albumNames is not false then -- not cancelled | |
repeat with albumName in albumNames | |
if albumName starts with gUnsortedAlbum then | |
-- special case: noalbum needs each image processed with it's own timestamp | |
-- because they can span many months/years and not just the first image | |
-- in an album | |
set allPhotos to (get media items of album albumName) | |
repeat with mediaItem in allPhotos | |
-- Extract Album date | |
set albumFirstMediaDate to date of mediaItem | |
-- Create list of media items | |
set mediaItems to {mediaItem} | |
-- Export the list of media items | |
my exportThisAlbum(albumName, mediaItems, albumFirstMediaDate) | |
end repeat | |
else | |
-- usual case: all other albums processed as single unit each | |
-- Extract Album date | |
set albumYear to 1900 as integer | |
repeat with mediaItem in (get media items of album albumName) | |
set albumFirstMediaDate to date of mediaItem | |
exit repeat -- only need first | |
end repeat | |
-- Create list of media items | |
set mediaItems to (get media items of album albumName) | |
-- Export the list of media items | |
my exportThisAlbum(albumName, mediaItems, albumFirstMediaDate) | |
end if | |
end repeat | |
end if -- main block | |
end tell | |
on exportThisAlbum(albumName, mediaItems, albumFirstMediaDate) | |
tell application "Photos" | |
with timeout of 1200 seconds -- give 20 mins instead of 2 minutes ... | |
-- filter raw list based on "already processed" tag/keyword ... | |
set mediaItemsToAttempt to {} | |
repeat with mediaItem in mediaItems | |
if keywords of mediaItem does not contain gKeywordOnSuccess then | |
set end of mediaItemsToAttempt to mediaItem | |
end if | |
end repeat | |
-- Any work to do? | |
if (count of mediaItemsToAttempt) = 0 then | |
set logMsg to "Skipping album name: " & albumName & ". All it's media items already have the " & gKeywordOnSuccess & " keyword." | |
my logThis(logMsg) | |
return | |
end if | |
-- Generate destination folder name | |
set albumYear to (text -4 thru -1 of ("0000" & (year of albumFirstMediaDate))) | |
set leafFolderName to my generateLeafFolderName(albumFirstMediaDate, albumName) | |
set destFolder to gDest & albumYear & ":" & leafFolderName -- path separator is : instead of \ ... weird | |
set logMsg to "Exporting album name: " & albumName & " to " & destFolder | |
my logThis(logMsg) | |
-- Create the destination folder | |
my makeFolder(destFolder) | |
-- export this filtered list | |
export mediaItemsToAttempt to (destFolder as alias) without using originals | |
end timeout | |
-- if successful add the gKeywordOnSuccess keyword/tag | |
repeat with mediaItem in mediaItemsToAttempt | |
set existingKeywords to keywords of mediaItem | |
if existingKeywords is missing value then | |
set existingKeywords to {} | |
end if | |
if existingKeywords does not contain gKeywordOnSuccess then | |
set (keywords of mediaItem) to existingKeywords & gKeywordOnSuccess | |
end if | |
end repeat | |
end tell | |
end exportThisAlbum | |
on generateLeafFolderName(theDate, albumName) | |
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)) | |
set datePrefix to yyyy & "-" & mm & "-" & dd | |
-- special case: unsorted album which may contain images spanning | |
-- years/months in some random order" | |
if albumName starts with gUnsortedAlbum then | |
--drop dd, cluster into months to avoid too many folders with too few files | |
return yyyy & "-" & mm | |
end if | |
--special case: legacy iPhoto imported events auto-prefixed by months | |
set monthsList to {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} | |
if my textStartsWith(albumName, monthsList) then | |
return datePrefix | |
end if | |
-- special case: album name already has a decent date prefix | |
if albumName starts with (yyyy & "-" & mm) then | |
return albumName | |
else | |
return datePrefix & " " & albumName | |
end if | |
end generateLeafFolderName | |
-- /////////////////////////////////////////// | |
-- // 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, gLogFile, 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 | |
-- /////////////////////////////////////////// | |
-- // GENERAL UTILITY | |
-- /////////////////////////////////////////// | |
on makeFolder2(tPath) | |
my logThis("make folder via finder:" & "gDest:" & gDest & " and tPath:" & tPath) | |
tell application "Finder" | |
make new folder at gDest with properties {name:tPath} | |
end tell | |
end makeFolder2 | |
on makeFolder(tPath) | |
do shell script "mkdir -p " & quoted form of POSIX path of tPath | |
end makeFolder | |
on textStartsWith(inputText, listOfStrings) | |
repeat with listItem in listOfStrings | |
if inputText starts with listItem then return true | |
end repeat | |
false | |
end textStartsWith | |
on sortList(theList) | |
set theIndexList to {} | |
set theSortedList to {} | |
repeat (length of theList) times | |
set theLowItem to "" | |
repeat with a from 1 to (length of theList) | |
if a is not in theIndexList then | |
set theCurrentItem to item a of theList as text | |
if theLowItem is "" then | |
set theLowItem to theCurrentItem | |
set theLowItemIndex to a | |
else if theCurrentItem comes before theLowItem then | |
set theLowItem to theCurrentItem | |
set theLowItemIndex to a | |
end if | |
end if | |
end repeat | |
set end of theSortedList to theLowItem | |
set end of theIndexList to theLowItemIndex | |
end repeat | |
return theSortedList | |
end sortList |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for sharing.
You may want to move the sorting (line 32) after checking for cancellation.