Skip to content

Instantly share code, notes, and snippets.

@abfo
Last active January 7, 2020 08:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abfo/65dd3353d1df127aee8d9dc4c038d007 to your computer and use it in GitHub Desktop.
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.
// 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