Last active
January 7, 2020 08:59
-
-
Save abfo/65dd3353d1df127aee8d9dc4c038d007 to your computer and use it in GitHub Desktop.
Apps script to add photos from Google Photos to Google Drive automatically, see https://ithoughthecamewithyou.com/post/how-to-backup-google-photos-to-google-drive-automatically-after-july-2019-with-apps-script for instructions.
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
// script settings | |
var ClientID = ''; | |
var ClientSecret = ''; | |
var BackupFolder = 'Google Photos'; | |
var MaxLength = 50000000; | |
var AlertEmail = '' | |
function onOpen() { | |
var ui = SpreadsheetApp.getUi(); | |
ui.createMenu('Google Photos Backup') | |
.addItem('Authorize if needed (does nothing if already authorized)', 'showSidebar') | |
.addItem('Backup Now', 'runBackup') | |
.addItem('Reset Settings', 'clearProps') | |
.addToUi(); | |
} | |
function runBackup() { | |
var photos = getPhotosService(); | |
var nextPageToken = null; | |
var downloadList = []; | |
// Limit of around 10,000 photos | |
for (var page = 1; page <= 1000; page++) { | |
Logger.log('Requesting page ' + page); | |
var date = new Date(); | |
date.setDate(date.getDate() - 1); | |
var request = { | |
"pageSize":"100", | |
"filters": { | |
"dateFilter": { | |
"ranges": [ | |
{ | |
"startDate": { | |
"year": date.getFullYear(), | |
"month": date.getMonth() + 1, | |
"day": date.getDate() | |
}, | |
"endDate": { | |
"year": date.getFullYear(), | |
"month": date.getMonth() + 1, | |
"day": date.getDate() | |
} | |
} | |
] | |
} | |
} | |
}; | |
if (nextPageToken != null) { | |
request["pageToken"] = nextPageToken; | |
} | |
var response = UrlFetchApp.fetch('https://photoslibrary.googleapis.com/v1/mediaItems:search', { | |
headers: { | |
Authorization: 'Bearer ' + photos.getAccessToken() | |
}, | |
'method' : 'post', | |
'contentType' : 'application/json', | |
'payload' : JSON.stringify(request, null, 2) | |
}); | |
var json = JSON.parse(response.getContentText()); | |
if ('mediaItems' in json) { | |
var photoCount = json.mediaItems.length; | |
for (var i = 0; i < photoCount; i++) { | |
var filename = json.mediaItems[i].filename; | |
var baseUrl = json.mediaItems[i].baseUrl; | |
if ('photo' in json.mediaItems[i].mediaMetadata) { | |
// if the photo property exists it's a photo and use =d to download, otherwise a video and we need to use =dv | |
baseUrl += '=d'; | |
} else { | |
baseUrl += '=dv'; | |
} | |
downloadList.push({ | |
"filename" : filename, | |
"baseUrl" : baseUrl | |
}); | |
} | |
} | |
if ('nextPageToken' in json) { | |
nextPageToken = json.nextPageToken; | |
} else { | |
break; | |
} | |
} | |
var folder; | |
var folders = DriveApp.getFoldersByName(BackupFolder); | |
while (folders.hasNext()) { | |
folder = folders.next(); | |
break; | |
} | |
var alerts = []; | |
var downloadCount = downloadList.length; | |
for (var i = 0; i < downloadCount; i++) { | |
var filename = downloadList[i].filename; | |
Logger.log('Downloading ' + downloadList[i].filename); | |
while (true) { | |
var files = folder.getFilesByName(filename); | |
if (!files.hasNext()) { | |
break; | |
} | |
// duplicate... keep adding (1) to the start until it isn't any more | |
filename = '(1) ' + filename; | |
} | |
var response = UrlFetchApp.fetch(downloadList[i].baseUrl, { | |
headers: { | |
Authorization: 'Bearer ' + photos.getAccessToken() | |
} | |
}); | |
// check size | |
var responseHeaders = response.getHeaders(); | |
var contentLength = responseHeaders['Content-Length']; | |
if (contentLength >= MaxLength) { | |
// need to alert... | |
alerts.push('Cannot download ' + downloadList[i].filename + ' as it is too large.'); | |
} else { | |
// small enough to download | |
var blob = response.getBlob(); | |
blob.setName(filename); | |
folder.createFile(blob); | |
} | |
} | |
var alertCount = alerts.length; | |
if (alertCount > 0) { | |
MailApp.sendEmail(AlertEmail, 'Google Photos Backup large file failures on ' + new Date(), alerts.join('\r\n')); | |
} | |
} | |
// functions below adapted from Google OAuth example at https://github.com/googlesamples/apps-script-oauth2 | |
function getPhotosService() { | |
// Create a new service with the given name. The name will be used when | |
// persisting the authorized token, so ensure it is unique within the | |
// scope of the property store. | |
return OAuth2.createService('photos') | |
// Set the endpoint URLs, which are the same for all Google services. | |
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth') | |
.setTokenUrl('https://accounts.google.com/o/oauth2/token') | |
// Set the client ID and secret, from the Google Developers Console. | |
.setClientId(ClientID) | |
.setClientSecret(ClientSecret) | |
// Set the name of the callback function in the script referenced | |
// above that should be invoked to complete the OAuth flow. | |
.setCallbackFunction('authCallback') | |
// Set the property store where authorized tokens should be persisted. | |
.setPropertyStore(PropertiesService.getUserProperties()) | |
// Set the scopes to request (space-separated for Google services). | |
// see https://developers.google.com/fit/rest/v1/authorization for a list of Google Fit scopes | |
.setScope('https://www.googleapis.com/auth/photoslibrary.readonly') | |
// Below are Google-specific OAuth2 parameters. | |
// Sets the login hint, which will prevent the account chooser screen | |
// from being shown to users logged in with multiple accounts. | |
.setParam('login_hint', Session.getActiveUser().getEmail()) | |
// Requests offline access. | |
.setParam('access_type', 'offline') | |
// Forces the approval prompt every time. This is useful for testing, | |
// but not desirable in a production application. | |
//.setParam('approval_prompt', 'force'); | |
} | |
function showSidebar() { | |
var photos = getPhotosService(); | |
if (!photos.hasAccess()) { | |
var authorizationUrl = photos.getAuthorizationUrl(); | |
var template = HtmlService.createTemplate( | |
'<a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>. ' + | |
'Close this after you have finished.'); | |
template.authorizationUrl = authorizationUrl; | |
var page = template.evaluate(); | |
SpreadsheetApp.getUi().showSidebar(page); | |
} else { | |
// ... | |
} | |
} | |
function authCallback(request) { | |
var photos = getPhotosService(); | |
var isAuthorized = photos.handleCallback(request); | |
if (isAuthorized) { | |
return HtmlService.createHtmlOutput('Success! You can close this tab.'); | |
} else { | |
return HtmlService.createHtmlOutput('Denied. You can close this tab'); | |
} | |
} | |
function clearProps() { | |
PropertiesService.getUserProperties().deleteAllProperties(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment