// script settings | |
// | |
// https://ithoughthecamewithyou.com/post/how-to-backup-google-photos-to-google-drive-automatically-after-july-2019-with-apps-script | |
// | |
var ClientID = ''; | |
var ClientSecret = ''; | |
var BackupFolder = 'Google Photos'; | |
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 MILLIS_PER_MINUTE = 60000; | |
var MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60; | |
var MILLIS_PER_DAY = MILLIS_PER_HOUR *24; | |
var now = new Date(); | |
var from = new Date(now.getTime() - MILLIS_PER_DAY) | |
var tomo = new Date(now.getTime() + MILLIS_PER_DAY) | |
var request = { | |
"pageSize":"100", | |
"filters": { | |
"dateFilter": { | |
"ranges": [ | |
{ | |
"startDate": { | |
"year": from.getFullYear(), | |
"month": from.getMonth() + 1, | |
"day": from.getDate() | |
}, | |
"endDate": { | |
"year": tomo.getFullYear(), | |
"month": tomo.getMonth() + 1, | |
"day": tomo.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 downloadCount = downloadList.length; | |
for (var i = 0; i < downloadCount; i++) { | |
var filename = downloadList[i].filename; | |
Logger.log('Downloading ' + downloadList[i].filename); | |
var response = UrlFetchApp.fetch(downloadList[i].baseUrl, { | |
headers: { | |
Authorization: 'Bearer ' + photos.getAccessToken() | |
} | |
}); | |
var files = folder.getFilesByName(filename); | |
if (!files.hasNext()) { | |
var blob = response.getBlob(); | |
blob.setName(filename); | |
folder.createFile(blob); | |
} | |
} | |
} | |
// 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