--- /dev/null
+# https://developers.google.com/accounts/docs/OAuth2ForDevices
+# https://developers.google.com/drive/web/auth/web-server
+# https://developers.google.com/google-apps/calendar/v3/reference/calendars
+# https://developers.google.com/picasa-web/
+
+import sys
+import urllib
+try:
+ import httplib # python2
+except ImportError:
+ import http.client # python3
+import os.path
+import json
+import time
+from oauth2client.client import OAuth2Credentials
+import gdata.calendar.service
+import gdata.docs.service
+import gdata.photos.service, gdata.photos
+from apiclient.discovery import build
+import httplib2
+from apiclient.discovery import build
+import datetime
+import ssl
+
+class OAuth:
+ def __init__(self, client_id, client_secret):
+ print "Initializing oauth token..."
+ self.client_id = client_id
+ self.client_secret = client_secret
+ #print 'Client id: %s' % (client_id)
+ #print 'Client secret: %s' % (client_secret)
+ self.token = None
+ self.device_code = None
+ self.verfication_url = None
+ self.token_file = 'client_secrets.json'
+ self.scope = [
+ #'https://www.googleapis.com/auth/calendar',
+ #'https://www.googleapis.com/auth/drive',
+ 'https://docs.google.com/feeds',
+ 'https://www.googleapis.com/auth/calendar.readonly',
+ 'https://picasaweb.google.com/data/',
+ 'https://photos.googleapis.com/data/',
+ #'http://picasaweb.google.com/data/',
+ #'https://www.google.com/calendar/feeds/',
+ ]
+ self.host = 'accounts.google.com'
+ self.reset_connection()
+ self.load_token()
+ self.last_action = 0
+ self.ssl_ctx = None
+
+ # this setup is isolated because it eventually generates a BadStatusLine
+ # exception, after which we always get httplib.CannotSendRequest errors.
+ # When this happens, we try re-creating the exception.
+ def reset_connection(self):
+ self.ssl_ctx = ssl.create_default_context(cafile='/usr/local/etc/ssl/cert.pem')
+ httplib.HTTPConnection.debuglevel = 2
+ self.conn = httplib.HTTPSConnection(self.host, context=self.ssl_ctx)
+
+ def load_token(self):
+ token = None
+ if os.path.isfile(self.token_file):
+ f = open(self.token_file)
+ json_token = f.read()
+ self.token = json.loads(json_token)
+ f.close()
+
+ def save_token(self):
+ f = open(self.token_file, 'w')
+ f.write(json.dumps(self.token))
+ f.close()
+
+ def has_token(self):
+ if self.token != None:
+ print "We have a token!"
+ else:
+ print "We have no token."
+ return self.token != None
+
+ def get_user_code(self):
+ self.conn.request(
+ "POST",
+ "/o/oauth2/device/code",
+ urllib.urlencode({
+ 'client_id': self.client_id,
+ 'scope' : ' '.join(self.scope)
+ }),
+ {"Content-type": "application/x-www-form-urlencoded"})
+ response = self.conn.getresponse()
+ if response.status == 200:
+ data = json.loads(response.read())
+ self.device_code = data['device_code']
+ self.user_code = data['user_code']
+ self.verification_url = data['verification_url']
+ self.retry_interval = data['interval']
+ else:
+ print response.status
+ print response.read()
+ sys.exit()
+ return self.user_code
+
+ def get_new_token(self):
+ # call get_device_code if not already set
+ if not self.user_code:
+ print "getting user code"
+ self.get_user_code()
+
+ while self.token == None:
+ self.conn.request(
+ "POST",
+ "/o/oauth2/token",
+ urllib.urlencode({
+ 'client_id' : self.client_id,
+ 'client_secret' : self.client_secret,
+ 'code' : self.device_code,
+ 'grant_type' : 'http://oauth.net/grant_type/device/1.0'
+ }),
+ {"Content-type": "application/x-www-form-urlencoded"})
+ response = self.conn.getresponse()
+ if response.status == 200:
+ data = json.loads(response.read())
+ if 'access_token' in data:
+ self.token = data
+ self.save_token()
+ else:
+ time.sleep(self.retry_interval + 2)
+ else:
+ print "failed to get token"
+ print response.status
+ print response.read()
+
+ def refresh_token(self):
+ if self.checking_too_often():
+ print "Not refreshing yet, too soon..."
+ return False
+ else:
+ print 'Trying to refresh oauth token...'
+ self.reset_connection()
+ refresh_token = self.token['refresh_token']
+ self.conn.request(
+ "POST",
+ "/o/oauth2/token",
+ urllib.urlencode({
+ 'client_id' : self.client_id,
+ 'client_secret' : self.client_secret,
+ 'refresh_token' : refresh_token,
+ 'grant_type' : 'refresh_token'
+ }),
+ {"Content-type": "application/x-www-form-urlencoded"})
+
+ response = self.conn.getresponse()
+ self.last_action = time.time()
+ if response.status == 200:
+ data = json.loads(response.read())
+ if 'access_token' in data:
+ self.token = data
+ # in fact we NEVER get a new refresh token at this point
+ if not 'refresh_token' in self.token:
+ self.token['refresh_token'] = refresh_token
+ self.save_token()
+ return True
+ print "Unexpected response %d to renewal request" % response.status
+ print response.read()
+ return False
+
+ def checking_too_often(self):
+ now = time.time()
+ return (now - self.last_action) <= 30
+
+ # https://developers.google.com/picasa-web/
+ def photos_service(self):
+ headers = {
+ "Authorization": "%s %s" % (self.token['token_type'], self.token['access_token'])
+ }
+ client = gdata.photos.service.PhotosService(additional_headers=headers)
+ return client
+
+ # https://developers.google.com/drive/
+ def docs_service(self):
+ cred = OAuth2Credentials(self.token['access_token'],
+ self.client_id,
+ self.client_secret,
+ self.token['refresh_token'],
+ datetime.datetime.now(),
+ 'http://accounts.google.com/o/oauth2/token',
+ 'KitchenKiosk/0.9')
+ http = httplib2.Http(disable_ssl_certificate_validation=True)
+ http = cred.authorize(http)
+ service = build('drive', 'v2', http)
+ return service
+
+ # https://developers.google.com/google-apps/calendar/
+ def calendar_service(self):
+ cred = OAuth2Credentials(self.token['access_token'],
+ self.client_id,
+ self.client_secret,
+ self.token['refresh_token'],
+ datetime.datetime.now(),
+ 'http://accounts.google.com/o/oauth2/token',
+ 'KitchenKiosk/0.9')
+ http = httplib2.Http(disable_ssl_certificate_validation=True)
+ http = cred.authorize(http)
+ service = build('calendar', 'v3', http)
+ return service
--- /dev/null
+#!/usr/local/bin/python
+
+# Copyright (c) 2015 Scott Gasch
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+import traceback
+import os
+from threading import Thread
+import time
+import gdata_oauth
+from datetime import datetime
+import sets
+import gdata_oauth
+import gdata.photos, gdata.photos.service
+import gdata
+import secrets
+
+class mirror_picasaweb:
+ """A program to mirror my picasaweb photos and make a local webpage out of it."""
+
+ album_whitelist = sets.ImmutableSet([
+ '1-Wire Project',
+ 'Alex 6.0..8.0 years old',
+ 'Barn',
+ 'Bangkok and Phuket, 2003',
+ 'Blue Angels... Seafair',
+ 'Dunn Gardens',
+ 'East Coast, 2011',
+ 'East Coast, 2013',
+ 'Friends',
+ 'Gasches',
+ 'Gasch Wedding',
+ 'Hiking and Ohme Gardens',
+ 'Hiking',
+ 'Kiosk Project',
+ 'Krakow 2009',
+ 'NJ 2015',
+ 'Oahu 2010'
+ 'Ocean Shores 2009',
+ 'Ohme Gardens',
+ 'Olympic Sculpture Park',
+ 'Paintings',
+ 'Puerto Vallarta',
+ 'Photos from posts',
+ 'Random',
+ 'SFO 2014',
+ 'Soccer',
+ 'Skiing with Alex',
+ 'Tuscany 2008',
+ 'Yosemite 2010',
+ 'Zoo',
+ ])
+
+ def __init__(self):
+ self.client = oauth.photos_service()
+ self.album = None
+ self.photo = None
+
+ def write_album_header(self, f, album):
+ f.write("""
+<HEAD>
+<TITLE>%s</TITLE>
+<BODY>
+<H2><A HREF="/scott/photos">Album List</A> | <A HREF="%s">%s</A></H2>
+""" % (album.title.text,
+ album.link[0].href,
+ album.title.text))
+ if album.location.text is not None:
+ f.write("<B>Location:</B> %s <BR>\n" % album.location.text)
+ if album.summary.text is not None:
+ f.write("<B>Summary:</B> %s <BR>\n" % album.summary.text)
+ if album.updated.text is not None:
+ f.write("<B>Updated:</B> %s <BR>\n" % album.updated.text)
+ f.write("""
+<HR>
+<TABLE STYLE="float:left; margin:0 5px 20px 0;" WIDTH=100%%>""")
+
+ def add_photo_to_album(self, f, photo, url, number):
+ descr = photo.summary.text
+ camera = photo.exif.model.text if photo.exif.model else 'unknown'
+ if photo.timestamp:
+ ts = photo.timestamp.datetime().strftime('%d %b %Y')
+ else:
+ ts = 'unknown'
+ if photo.commentCount and photo.commentCount > 0:
+ comments = self.client.GetFeed(
+ '/data/feed/api/user/default/albumid/%s/photoid/%s?kind=comment' % (
+ self.album.gphoto_id.text, photo.gphoto_id.text))
+ else:
+ comments = None
+ thumbnail = photo.media.thumbnail[1].url
+
+ if number % 2 == 0:
+ f.write('<TR>')
+ f.write("""
+<TD ALIGN="center" WIDTH=33%%>
+ <TABLE>
+ <TR>
+ <TD ALIGN="center">
+ <CENTER>
+ <A HREF="%s">
+ <DIV CLASS="img-shadow"><IMG SRC="%s" ALT="%s"></DIV>
+ </A>
+ </CENTER>
+ </TD>
+ </TR>
+ <TR>
+ <TD>
+ <DIV><CENTER>
+ <B STYLE="font-size: 60%%; color: #406b38">%s</B> <BR>
+ <B>Date</B>: %s <BR>
+ <B>Camera</B>: %s <BR>""" % (
+ url,
+ thumbnail,
+ descr,
+ descr,
+ ts,
+ camera))
+ if comments is not None:
+ for comment in comments.entry:
+ print '%s <BR>' % comment.content.text
+ f.write("""
+ </CENTER></DIV>
+ </TD>
+ </TABLE>
+</TD>""")
+ if number % 2 == 1:
+ f.write('</TR>')
+
+ def write_album_footer(self, f):
+ f.write("""
+</TABLE>
+<CENTER>
+<FONT style="font-size:60%;color:#406b38">
+ Are you interested in <A HREF="/svn/picasa/trunk">how this page was made</A>?
+</FONT>
+</CENTER>
+</BODY>
+</HTML>""")
+
+ def write_index_header(self, f):
+ f.write("""
+<HEAD>
+<TITLE>Scott's Photo Albums</TITLE>
+</HEAD>
+<BODY>
+<TABLE WIDTH=100%%>
+ <TR>
+ <TD WIDTH=50%%>
+ <H2><A HREF="/scott/photos">Album List</A></H2>
+ </TD>
+ <TD></TD>
+ </TR>""")
+
+ def add_album_to_index(self, f, album, count):
+ thumbnail = album.media.thumbnail[0].url
+ if count % 2 == 0:
+ f.write('<TR>\n')
+ f.write("""
+<TD>
+ <TABLE>
+ <TR>
+ <TD WIDTH=200>
+ <CENTER>
+ <A HREF="%s.html">
+ <IMG ALIGN=CENTER WIDTH=160 HEIGHT=160 SRC="%s">
+ </A>
+ </CENTER>
+ </TD>
+ <TD>
+ <B>Title</B>: %s <BR>
+ <B>Subtitle</B>: %s <BR>
+ <B>Date</B>: %s <BR>
+ <B>%s</B> photo(s) <BR>
+ <B>%d</B> Mb""" % (
+ album.name.text,
+ thumbnail,
+ album.title.text,
+ album.summary.text,
+ album.published.text,
+ album.numphotos.text,
+ int(album.bytesUsed) / (1024 * 1024)))
+ f.write("""
+ </TD>
+ </TR>
+ </TABLE>
+</TD>""")
+ if count % 2 == 1:
+ f.write('</TR>\n')
+
+ def write_index_footer(self, f):
+ f.write("""
+</TABLE>
+</BODY>""")
+
+ def run(self):
+ attempts = 0
+ while attempts < 3:
+ attempts += 1
+ try:
+ self.run_internal()
+ return
+ except (gdata.service.RequestError,
+ gdata.photos.service.GooglePhotosException):
+ print "******** TRYING TO REFRESH PHOTOS CLIENT *********"
+ oauth.refresh_token()
+ self.client = oauth.photos_service()
+ print "Tried 3x, giving up."
+
+ def run_internal(self):
+ albums = self.client.GetUserFeed().entry
+ album_count = 0
+ index = open('./index.html', 'w')
+ self.write_index_header(index)
+ for album in albums:
+ if album.title.text not in mirror_picasaweb.album_whitelist:
+ print '--> Skipping "%s" (%s)' % (album.title.text, album.access.text)
+ continue
+ print '--> Doing "%s" (%s)' % (album.title.text, album.access.text)
+ self.album = album
+ self.add_album_to_index(index, album, album_count)
+ filename = './%s.html' % album.name.text
+ detail = open(filename, 'w')
+ self.write_album_header(detail, album)
+ photos = self.client.GetFeed(
+ '/data/feed/api/user/%s/albumid/%s?kind=photo&imgmax=1024u' %
+ (secrets.account, album.gphoto_id.text))
+ photo_count = 0
+ for photo in photos.entry:
+ resolution = 0
+ for x in photo.media.content:
+ if "video" in x.type and int(x.height) > resolution:
+ url = x.url
+ resolution = int(x.height)
+ elif resolution == 0:
+ url = x.url
+ resolution = int(x.height)
+ self.photo = photo
+ self.add_photo_to_album(detail, photo, url, photo_count)
+ photo_count += 1
+ self.write_album_footer(detail)
+ detail.close()
+ album_count += 1
+ self.write_index_footer(index)
+ index.close()
+
+if __name__ == "__main__":
+ oauth = gdata_oauth.OAuth(secrets.client_id,
+ secrets.client_secret)
+ if not oauth.has_token():
+ user_code = oauth.get_user_code()
+ print '------------------------------------------------------------'
+ print 'Go to %s and enter the code "%s" (no quotes, case-sensitive)' % (
+ oauth.verification_url, user_code)
+ oauth.get_new_token()
+ x = mirror_picasaweb()
+ x.run()