Initial checking of picasa project. master
authorScott Gasch <[email protected]>
Thu, 2 Jun 2016 02:39:03 +0000 (19:39 -0700)
committerScott Gasch <[email protected]>
Thu, 2 Jun 2016 02:39:03 +0000 (19:39 -0700)
gdata_oauth.py [new file with mode: 0644]
mirror_picasa.py [new file with mode: 0755]
secrets.py [new file with mode: 0644]

diff --git a/gdata_oauth.py b/gdata_oauth.py
new file mode 100644 (file)
index 0000000..e7296e5
--- /dev/null
@@ -0,0 +1,204 @@
+# 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
diff --git a/mirror_picasa.py b/mirror_picasa.py
new file mode 100755 (executable)
index 0000000..88ae3fc
--- /dev/null
@@ -0,0 +1,269 @@
+#!/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()
diff --git a/secrets.py b/secrets.py
new file mode 100644 (file)
index 0000000..9e71d6c
--- /dev/null
@@ -0,0 +1,5 @@
+#!/usr/local/bin/python
+
+client_id = '<your_id>.apps.googleusercontent.com'
+client_secret = '<your_secret>'
+account = '<your_google_account>'