Skip to content

Instantly share code, notes, and snippets.

@SidShetye
Last active February 1, 2021 14:24
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SidShetye/ddf9b9870ea0ddb4bf112a58b0b0fbcc to your computer and use it in GitHub Desktop.
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.
------------------------------------------------
-- 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
@nkalvi
Copy link

nkalvi commented Dec 9, 2019

Thanks for sharing.

You may want to move the sorting (line 32) after checking for cancellation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment