Export Apple photos library for importing it elsewhere

796 views Asked by At

I intented to export all my photos from Mac to any other environment and this suggestion finally provided the base to solve this with applescript. That way the photos are always scaled (getting bigger than the original) but at least it works.

I know it's long but as I struggled that long with this problem, so I post the whole working solution below as an answer. Maybe someone has a better suggestion. I have refused to reverse engineer the contents in sqlite tables Photos is using, as this just may change with the next version.

Problems are various

  • There is no guarantee how Photos walks through the albums and folders. And as it sometimes just stops working, you have to guarantee some order, or you will never finish. So I introduced a way to start at a particular album at a particular photo. The order within the album seems to be stable. The log output written shows the unique ids of album and photo (as names may not be unique) to be able to restart it at this point. Most of the time the second attempt just works.
  • I did not find a way to store photos in order, so I created one single directory for each photo and stored each photo in a separate directory, so I can reimport them in order. Problem is that Photos always uses the original file name and just counts upwards if the name exists already. Using different cameras just makes it worse.
  • All attributes I wrote into a text file per photo, so I can reimport them later.
  • Error handling is quite tricky. The photos library sometimes just requests the user to press a button, which makes it hard to script it.
1

There are 1 answers

0
toaster On

That's unfortunately the best I was able to come up with, it is my 1st applescript. This worked on an old Mac with Sierra and it works on more recent versions with Catalina as well. I'm almost sure the newest version will not complain. I'm still searching for a better solution.

There is little support to run it from inside Apple's script editor, only command line provides all options.

#!/usr/bin/osascript
global startAlbum, startPhoto, match, dummy, match, photoCnt, infoFd

global errorIndicator

on writePhotoAndData(thePath, mediaItem, mediaAlbum)
        set ind to "X"
        using terms from application "Photos"
                set fName to filename of mediaItem
                set fId to id of mediaItem
                set photoCnt to photoCnt + 1
                -- export each media to separate directory -> only chance to keep the order
                if dummy then
                        if infoFd ≠ missing value then
                                set s to id of mediaAlbum & tab & id of mediaItem & tab & thePath & linefeed as text
                                write s to infoFd as «class utf8»
                        end if
                else
                        makeEmptyPosixPath(thePath)
                        set exportPath to POSIX file thePath
                        set infoFile to POSIX file (thePath & "/" & "info.txt")
                        set infoText to "id" & tab & id of mediaItem & linefeed & "file" & tab & filename of mediaItem & linefeed & "album" & tab & name of mediaAlbum & linefeed
                        if exists name of mediaItem then set infoText to infoText & "name" & tab & name of mediaItem & linefeed
                        if exists description of mediaItem then set infoText to infoText & "desc" & tab & description of mediaItem & linefeed
                        if exists date of mediaItem then
                                set d to date of mediaItem
                                set infoText to infoText & "date" & tab & short date string of d & space & time string of d & linefeed
                        end if
                        if exists altitude of mediaItem then set infoText to infoText & "alt" & tab & altitude of mediaItem & linefeed
                        if exists location of mediaItem then set infoText to infoText & "location" & tab & location of mediaItem & linefeed
                        if exists keywords of mediaItem then
                                tell mediaItem to set myKeywords to keywords
                                repeat with keyword in myKeywords
                                        set infoText to infoText & "keyword" & tab & keyword & linefeed
                                end repeat
                        end if
                        set fd to open for access infoFile with write permission
                        set eof fd to 0 -- of fd?
                        write infoText to fd starting at eof as «class utf8»
                        close access fd
                        try
                                tell mediaAlbum
                                        --Not sure whether this does anything, so removed
                                        --set settings to "JPEG - Original Size"
                                        export {mediaItem} to (exportPath as alias)
                                end tell
                                set errorIndicator to 0
                        on error errStr number errNum
                                if errNum = -1712 then --timeout
                                        set ind to "E"
                                        set errorIndicator to errorIndicator + 1
                                        if errorIndicator >= 3 then
                                                error "3 errors in a row - exiting"
                                        end if
                                else
                                        error errStr number errNum
                                end if
                        end try
                end if
                log ind & tab & photoCnt & tab & id of mediaAlbum & tab & id of mediaItem & tab & name of mediaAlbum & tab & filename of mediaItem
        end using terms from
end writePhotoAndData

on walkAlbum(theAlbum, thePath)
        if match = 0 then
                if id of theAlbum = startAlbum then
                        set match to 1
                        if startPhoto is missing value then
                                set match to 2
                        end if
                else
                        return
                end if
        else if match = 1 then
                if id of theAlbum is not equal to startAlbum then
                        set match to 3
                end if
        end if
        set photoNum to 0
        using terms from application "Photos"
                set albumPath to thePath & name of theAlbum & "/"
                repeat with mediaItem in media items of theAlbum
                        set photoNum to photoNum + 1
                        if match = 1 then
                                if id of mediaItem = startPhoto then
                                        set match to 2
                                end if
                        else if match = 2 then
                                set match to 3 --photo after the photo chosen
                        end if
                        if match = 3 then
                                writePhotoAndData(albumPath & photoNum, mediaItem, theAlbum)
                        end if
                end repeat
        end using terms from
end walkAlbum

on walkFolder(theFolder, thePath)
        using terms from application "Photos"
                repeat with containedFolder in folders of theFolder
                        walkFolder(containedFolder, thePath & name of containedFolder & "/")
                end repeat
                repeat with containedAlbum in albums of theFolder
                        walkAlbum(containedAlbum, thePath)
                end repeat
        end using terms from
end walkFolder

on makePosixPath(tPath)
        do shell script "mkdir -p " & quoted form of tPath
end makePosixPath

on makeEmptyPosixPath(tPath)
        do shell script "rm -rf " & quoted form of tPath & " && mkdir -p " & quoted form of tPath
end makeEmptyPosixPath

on makeFolder(tPath)
        do shell script "mkdir -p " & quoted form of POSIX path of tPath
end makeFolder

on walkFile(fileName, fileOffset as integer)
        set thisOffset to 0
        set saveDelim to text item delimiters of AppleScript
        set walkFd to open for access POSIX file fileName
        set rawLine to read walkFd before linefeed as «class utf8»
        repeat
                set thisOffset to thisOffset + 1
                if fileOffset = 0 or thisOffset ≥ fileOffset then
                        set thisLine to rawLine as text
                        set text item delimiters of AppleScript to tab
                        set splitLine to text items of thisLine
                        set text item delimiters of AppleScript to saveDelim
                        set albumIdString to item 1 of splitLine
                        set photoIdString to item 2 of splitLine
                        set pathString to item 3 of splitLine
                        tell application "Photos"
                                set thisAlbum to album id albumIdString
                                set thisMedia to media item id photoIdString
                        end tell
                        writePhotoAndData(pathString, thisMedia, thisAlbum)
                        log "O" & tab & thisOffset
                end if
                try
                        set rawLine to read walkFd before linefeed as «class utf8»
                on error errTxt number errNum
                        if errNum = -39 then --end of file
                                exit repeat
                        else
                                error "Error reading inputfile: " & errTxt
                        end if
                end try
        end repeat
end walkFile

on run (args)
        set caller to class of args as string
        set errorIndicator to 0
        set destPath to POSIX path of (get path to home folder) & "export/photos/"
        set photoCnt to 0
        set startAlbum to missing value
        set startPhoto to missing value
        set match to 3
        set dummy to false
        set walkFileName to missing value
        set walkFileOffset to 0
        set infoFd to missing value
        set infoFileName to missing value
        set chooseDestinationFolder to "Select start folder (defaults to " & destPath & ")"
        set chooseStart to "Give start album and photo"
        set chooseDryRun to "dry-run"
        if caller = "script" then
                log "Running in ScriptEditor:" & name of me
                set options to choose from list {chooseDestinationFolder, chooseStart, chooseDryRun} with title "Configure run" with prompt "Select options" with multiple selections allowed and empty selection allowed
                if options contains chooseDryRun then set dummy to true
                if options contains chooseDestinationFolder then
                        try
                                set destFolder to choose folder with prompt "Choose export directory or cancel for default location" default location (get path to home folder as alias)
                                set destPath to POSIX path of destFolder
                        end try
                end if
                if options contains chooseStart then
                        set res to display dialog "Enter Id of album to start" default answer "" buttons {"OK"} default button 1
                        if length of text returned of res > 0 then
                                set startAlbum to text returned of res
                                set match to 0
                        end if
                        if match = 0 then
                                set res to display dialog "Enter id of photo to resume after (leave empty to start with album)" default answer "" buttons {"OK"} default button 1
                                if length of text returned of res > 0 then
                                        set startPhoto to text returned of res
                                end if
                        end if
                end if
        else if caller = "list" then
                log "Running on the command line:" & name of me
                set cnt to 1
                repeat while cnt ≤ length of args
                        if item cnt of args = "-t" then
                                set cnt to (cnt + 1)
                                set destPath to item cnt of args
                                set cnt to (cnt + 1)
                        else if item cnt of args = "-a" then
                                set match to 0
                                set cnt to (cnt + 1)
                                set startAlbum to item cnt of args
                        else if item cnt of args = "-p" then
                                set cnt to (cnt + 1)
                                set startPhoto to item cnt of args
                        else if item cnt of args = "-h" then
                                return name of me & " [-t target path] [-a start album id] [-a start photo id] [-d] [-i info file path]" & linefeed ¬
                                        & name of me & "[-t target path] -f info file [-o offset]" & linefeed ¬
                                        & name of me & " -h .. this help"
                        else if item cnt of args = "-d" then
                                set dummy to true
                        else if item cnt of args = "-i" then
                                set cnt to (cnt + 1)
                                set infoFileName to item cnt of args
                        else if item cnt of args = "-f" then
                                set cnt to (cnt + 1)
                                set walkFileName to item cnt of args
                        else if item cnt of args = "-o" then
                                set cnt to cnt + 1
                                set walkFileOffset to item cnt of args
                        else
                                error "Invalid option:" & item cnt of args & " - use -h for help"
                        end if
                        set cnt to (cnt + 1)
                end repeat
        end if
        set mySettings to "Destination directory:" & destPath
        if startAlbum is not missing value then set mySettings to mySettings & linefeed & tab & "Start album id:" & tab & startAlbum
        if startPhoto is not missing value then set mySettings to mySettings & linefeed & tab & "Start photo id:" & tab & startPhoto
        if infoFileName ≠ missing value then
                set mySettings to mySettings & linefeed & tab & "Write file '" & infoFileName & "'" & linefeed
                set dummy to true
        end if
        if dummy then set mySettings to mySettings & linefeed & tab & "Dry run only"
        if caller = "script" then
                display dialog mySettings with title "Start exporting?" with icon note
        else if caller = "list" then
                log mySettings
        else
                error "Internal error - caller interface unknown"
        end if
        if infoFileName ≠ missing value then
                set infoFd to open for access POSIX file infoFileName with write permission
                set eof infoFd to 1
        end if
        if walkFileName ≠ missing value then
                walkFile(walkFileName, walkFileOffset)
        else
                walkFolder(application "Photos", destPath)
        end if
        if infoFd ≠ missing value then close access infoFd
        return "Done!"
end run