DEV Community

Cover image for Manage your Google Photo account with Python! (p.1)
Davide Del Papa
Davide Del Papa

Posted on • Updated on

Manage your Google Photo account with Python! (p.1)

(Photo by Brandon Erlinger-Ford on Unsplash)

Chances are that if you have an Android device with a Google account connected to it, you have already lots of media saved in Google Photos. How can we interact with this data in Pyhton? Google has the offical support only for PHP and Java; otherwise, we should really interact with it through REST.

gPhotoSpy is an unofficial python package that tries to fill in this gap.

Fair Diclaimer: I'm also the author of gphotospy; so now you know. It's a thing one must say, anyway... Collaboration and ideas are very welcome. I've made this package through lock-down, to pass some time in a useful and instructing way.

Set up OAuth authentication keys

To start we need to set up the OAuth authentication keys and enable Google Photo Library.

Point your browser to the Google Console.
On the bar select the dropdown near the current project to create a new project.

Alt Text

Create the new project (button on the right).

Alt Text

I called mine gPhotoSpy

Then on the burgher menu select: Api & Services > Library.

Alt Text

In the library, search for "Photo", select Photo Library API

Alt Text

Then, click on the ENABLE button: it changes to MANAGE once clicked.

Alt Text

If you click to MANAGE it gets you straight to the Google Photo API Management page.

Alt Text

On the API & Services, Photo Library API select Credentials

Alt Text

Then, select the +CREDENTIALS and select in the dropdown that appears: OAuth client ID.

Select the type of app to be created (for desktop application select "other", the last one!) and give it a name. (I used gPhotoSpyOAuth as name, but any will do)

Alt Text

Watchout! If you are an occasional user, you should before this thing select the authorization display (OAuth Consent Screen), and make it public, if you are not a g-Suite user ($$$). If you have never done it before the interface already prompts you to do so at this point, so follow the link and instructions.

Don't bother saving the keys right away, in fact, coming back to the main interface, you can download it as a .json file, which is more useful.
Select in the row of the OAuth 2.0 Client ID just created the download button (rightmost one; in the following image is in the bottom-right corner).

Alt Text

Download the .json file in the root of the project, and give it a meaningful name (because it must be used afterwards).

I gave it the name gphoto_oauth.json which is the one I will refer to in code, but feel free to change it as you will.

Remember to keep all these secret files far from git pushes if you are committing your porject to a repository; thus, add the downloaded file to the .gitignore.

Let's get started

First things first, let's install gphotospy using pip:

pip install gphotospy
Enter fullscreen mode Exit fullscreen mode

It will install the official Google packages google-api-python-client, google-auth-httplib2, google-auth-oauthlib, and the oauth2client if not already installed.

We will need also a library to handle images: PIL(Pilow)

So if you do not have it installed, remember to

pip install Pillow
Enter fullscreen mode Exit fullscreen mode

Next we need the imports. Google Photos' REST API is basically divided into three 'modules' plus some utility services, such as OAuth and the media uploads. gphotospy has three modules to cover the API, plus a module for authorization and one for uploading media (this last one is also covered by a convenience wrapper in the Media module):

gphotospy
├── album
├── authorize
├── media
├── sharedalbum
└── upload
Enter fullscreen mode Exit fullscreen mode

The first thing we will need, then, is authorization:

>>> from gphotospy import authorize
Enter fullscreen mode Exit fullscreen mode

We then provide the secrets file:

>>> CLIENT_SECRET_FILE = "gphoto_oauth.json"
Enter fullscreen mode Exit fullscreen mode

Now we need to get authorized by Google to proceed:

>>> service = authorize.init(CLIENT_SECRET_FILE)
Enter fullscreen mode Exit fullscreen mode

The first time around the service wil open your browser to the auth page (in any case it will show also in the terminal the link to follow)

Alt Text

Since our app is not (yet) verified, Google will show us a fairly intimidating page that warns us that the app is not verified.

Alt Text

Once we finish building our app and we want to pass it to production we have to submit it to Google for review; however for personal use and for testing purposes we do not need to verify our app, we can continue by clicking on Advanced (bottom-left corner of the warning message)

Alt Text

Now, after another intimidating message, we have finally the link Go to which will finally complete the auth process.

We now need to chose our Google account and manage permissions:

Alt Text

We need to authorize now two kinds of permissions (called scopes): managing the whole library (media and albums), and sharing.

Once we do authorize them, we have yet another page for recap, just to be sure we really really really (are you sure?) want to authorize our app.

Alt Text

All jokes apart, as to date gphotospy does not allow to have other (more restricted) scopes (the permissions) than these two, such as read access only (it might be a feature in one of the future releases of the library).
Basically with these two scopes the user allows the app to access and manage the whole library; well, with some limitations (which we will cover in another tutorial).

So, after giving our consent (remember you have to repeat after me in a loud voice "My name is Major Mira Killian, and I give my consent", otherwise Google will not allow your app) we can close the browser's tab/window and continue: fianally we are authorized.

Alt Text

Once the auth is done, the library saves a pickled file, with extension .token containing the authorization token. This will allow for repeated uses without the need to authorize the app again all the times.

Warning! If you are using a versioning system keep this token file out of your commints. For GIT add it to a .gitignore file (together with the authorization keys)

Note: If you are going to sumbit your app to Google for review, there are two pages with guidelines you should really read before starting to write out your app: UX Guidelines and the Acceptable Use Policy. It is better to read them (UX most carefully) in order to structure your app's UX from the ground up, in a way acceptable for Google policies.

Back to the code.

Reading Media

Let's start to work with albums. We need first to import the album manager and all other facilities

>>> from gphotospy.album import *
Enter fullscreen mode Exit fullscreen mode

We need now to construct our album manager. We pass to the Album class constuctor the service object we got through authorization

>>> album_manager = Album(service)
Enter fullscreen mode Exit fullscreen mode

In order to list the albums, we will use the Album.list() method, which returns an iterator over the list of albums. Google Photos API lists album in a paginated way, and the list iterator takes care of this in the background. More info in the docstrings of the method, with help(album_manager.list).

>>> album_iterator = album_manager.list()
>>> album_iterator
<generator object Album.list at 0x7fb35cfd5750>
Enter fullscreen mode Exit fullscreen mode

Ok, lets start listing our albums.

>>> first_album = next(album_iterator)
>>> first_album
Enter fullscreen mode Exit fullscreen mode

The above command will take a little to execute, because it will first poll the API endpoint.
The other calls will be faster, because the list iterator caches up to 50 albums, before asking for the next batch to the API endpoint. This behavior can be changed through the Album.set_pagination() method (see Album.list() docstring for more info); however it is not recommended to change it, as the number of calls to the API will increase, and there is a daily Quota set by Google.

The list iterator returns a JSON object of the kind:

{
  "id": "album_id",
  "title": "album_title", // Or None
  "productUrl": "album_product_url",
  "coverPhotoBaseUrl": "album_cover_base_url", // do not use directly, it needs extra parameters
  "coverPhotoMediaItemId": "album_cover_media_item_id", // you can use this to get the cover item id
  "isWriteable": "whether_you_can_write_to_this_album_or_not",
  "shareInfo": {}, // Object present only for albums shared through the API
  "mediaItemsCount": "number_of_media_items_in_album"
}
Enter fullscreen mode Exit fullscreen mode

The object is mapped straight to a Python Dictionary.
To work with the album we must get its id.

>>> first_album_id = first_album.get("id")
>>> first_album_id
'AB1M5bIdR1....'
Enter fullscreen mode Exit fullscreen mode

We could get easily also its title, if present:

>>> first_album.get("title")
'Happy Birthday'
Enter fullscreen mode Exit fullscreen mode

Once we have the album id we can list all media present in the album.
However, in order to do so, we have first to import and construct the media manager

>>> from gphotospy.media import *
>>> media_manager = Media(service)
Enter fullscreen mode Exit fullscreen mode

Now we can get a list of all media present in the album

>>> album_media_list = list(media_manager.search_album(first_album_id))
>>> len(album_media_list)
10
Enter fullscreen mode Exit fullscreen mode

Media.search_album() returns an iterator that we will consume right away with list() and assign to the variable album_media_list. Whatchout because list() consumes the whole iterator: if the album contains thousands of pictures the list() method gets them all. In this case we have only 10 pics.

Each media item is a JSON object (mapped to a dictionary in Python), in this form:

{
  "id": "media_id",
  "description": "media_description", // If present
  "productUrl": "media_product_url",
  "baseUrl": "media_base_url", // do not use directly, it needs extra parameters
  "mimeType": "media mime-type",
  "mediaMetadata": {}, // Object with metadata. For a photo it can include the camera's settings, etc..
  "contributorInfo": {}, // Object containing the sharing info
  "filename": "assigned_file_name" // If present
}
Enter fullscreen mode Exit fullscreen mode

Since it is the second time we have seen this, there is a difference between "productUrl" and "baseUrl".

  • The productUrl is a link to the media item inside Google Photos; thus it requires to be signed in to view it

  • The baseUrl allow you to access the bytes of the media items. This allows you, with the right parameters, to download the media item or display it within your app.

Warning: baseUrl is valid for 60 minutes only. To use it in production we need to store the item's id and ask for the URL again if it is expired.

View an image

Let's view an image using what we have learned about the baseUrl property. I have checked that the element in the position 0 inside the list is a picture (with get("mimeType"): it gives 'image/jpeg'); so, let's get its baseUrl

>>> base_url = album_media_list[0].get("baseUrl")
>>> base_url
'https://lh3.googleusercontent.com/lr/AFBm1...'
Enter fullscreen mode Exit fullscreen mode

As you can see (in the code above it is cropped but) the baseUrl is fairly long, and available for only 60 minutes. Let's hurry up.

For images we can show them with Tk + Pillow.
The following is all the imports needed for this operation (we will use urlopen to open the raw image):

>>> import io
>>> from urllib.request import urlopen
>>> from tkinter import *
>>> from gphotospy.authorize import get_credentials
Enter fullscreen mode Exit fullscreen mode

The following code is all Tk boilerplate.

>>> root = Tk()
Enter fullscreen mode Exit fullscreen mode

At this point a Tk window should appear; just leave it as is and get back to the terminal
Let's create and pack a canvas

>>> canvas = Canvas(root, width=320, height=320, bg='white')
>>> canvas.pack(side='top', fill='both', expand='yes')
Enter fullscreen mode Exit fullscreen mode

I've chose a small canvas, 320X320: feel free to make it bigger if you want to.

Now we construct our url. The parameters we need are the maximum hight and maximum width.
The url then must be constructed this way:

<baseUrl>=w<maximum-widht-in-pixel>-h<maximum-hight-in-pixel>
Enter fullscreen mode Exit fullscreen mode

In this case the windows is 320X320, so we will leave a border and get an image that is, at most, 300 pixels high, and 300 pixel wide.

Thus

>>> image_url = "{}=w300-h300".format(base_url)
Enter fullscreen mode Exit fullscreen mode

Let's use urlopen from urllib.request to get a raw image

>>> image_bytes = urlopen(image_url).read()
Enter fullscreen mode Exit fullscreen mode

Now that we have a raw image, we pass it to Pillow's Image to construct an image for us

>>> img = Image.open(io.BytesIO(image_bytes))
Enter fullscreen mode Exit fullscreen mode

Now we prapare the image to be displayed, then we display it to the Tk windows

>>> photo = ImageTk.PhotoImage(img)
>>> canvas.create_image(10, 10, image=photo, anchor='nw')
1
Enter fullscreen mode Exit fullscreen mode

Now that we added our image it should diplay it in our window.

Alt Text

Mission accomplished !!!

If you need to put all this in a script, do not forget to pass the control to the main loop, otherwise the script will end as soon as the image is displayed, and the window will be terminated.

The following is to use in a script to pass the control to the main loop and keep the window "living on":

root.mainloop()
Enter fullscreen mode Exit fullscreen mode

For now that's all.
We will see some more in another tutorial.

Code

You can find the whole code in this gist.

Check out also the examples folder in the repo, I've tried to cover all the basics there. In any case I intend to continue this series of tutorials, so stay tuned!

Top comments (4)

Collapse
 
clanmills profile image
Robin Mills • Edited

Wow. Great Work. Your code worked first time for me and showed me a photo from my Google Photo Library that I took on Thursday.

Now, I'm going to read your code and discover how you did that. Amazing.

I'm the retired maintainer of Exiv2 the C++ Metadata Library. exiv2.org

I'm rebuilding my web-site from scratch because it's 20 years old, has 80,000 photos in 2000 albums, 400,000 files and occupies 10GB on the web-server. I'm going to store the photos on Google Photos and generate the photo albums in JavaScript. I've made good progress and confident that the web-site will be less than 100mb by the end of September. Here's a typical album web-page: clanmills.com/2021/Lizzie

The code for the page is:

505 rmills@rmillsm1:~/clanmills $ cat 2021/Lizzie/default.shtml 
<!--#include virtual="/albumhead.inc" -->
<!--#include virtual="/menu.inc" -->
<!--#include virtual="/albumtail.inc" -->
<script type="text/javascript">
   var title   = "Lizzie"
   var data    = 'https://photos.app.goo.gl/sQRaVMZiyTr5T74y8'
   var updated = '2021-08-20 14:52:25'
   buildPage();
</script>
506 rmills@rmillsm1:~/clanmills $ 
Enter fullscreen mode Exit fullscreen mode

The next job is to understand enough Python/REST/Photos magic to generate the list of album files automatically. The script will run once a day at home and update clanmills.com. So, when I take photos with my phone, they be posted on clanmills.com without moving a finger.

Thanks to your python code, I'm confident of success. Thank You Very Much.

Collapse
 
davidedelpapa profile image
Davide Del Papa

You are welcome. I made the lib to script access to my wedding pics myself, so I guess your use case should be not too difficult to tackle as well.
Good luck 🤞

Collapse
 
clanmills profile image
Robin Mills

Davide: I love your code. It's beautiful and clear.

I fixed a tiny bug in album.py. When I iterate the albums (I have 250 albums) the loop dies at the end saying "object isn't interable:"

    'title': 'BoysInSanJose2002'}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Python/3.8/site-packages/gphotospy/album.py", line 533, in list
    for album in curr_list:
TypeError: 'NoneType' object is not iterable
Enter fullscreen mode Exit fullscreen mode

Here's my fix:

0a1
> from collections.abc import Iterable
533,534c534,536
<             for album in curr_list:
<                 yield album
---
>             if isinstance(curr_list, Iterable):
>                 for album in curr_list:
>                     yield album
Enter fullscreen mode Exit fullscreen mode

Question. Is the code in GitHub. Can I provide a PR if I find other issues?

Collapse
 
happy985 profile image
Happy985

Hello,

When I creat "OAuth client ID" I choose "Web application" because there are not "other"in the new version.
So I download my Json file as you said. but I execute this line code
"service = authorize.init(CLIENT_SECRET_FILE)"
I obtain this Error.
So what is the problem please
Thank you

File "C:\Users\Admin\Anaconda3\lib\site-packages\gphotospy\authorize.py", line 55, in init
credentials = get_credentials(secrets)

File "C:\Users\Admin\Anaconda3\lib\site-packages\gphotospy\authorize.py", line 34, in get_credentials
credentials = app_flow.run_local_server()

File "C:\Users\Admin\Anaconda3\lib\site-packages\google_auth_oauthlib\flow.py", line 458, in run_local_server
host, port, wsgi_app, handler_class=_WSGIRequestHandler

File "C:\Users\Admin\Anaconda3\lib\wsgiref\simple_server.py", line 153, in make_server
server = server_class((host, port), handler_class)

File "C:\Users\Admin\Anaconda3\lib\socketserver.py", line 452, in init
self.server_bind()

File "C:\Users\Admin\Anaconda3\lib\wsgiref\simple_server.py", line 50, in server_bind
HTTPServer.server_bind(self)

File "C:\Users\Admin\Anaconda3\lib\http\server.py", line 137, in server_bind
socketserver.TCPServer.server_bind(self)

File "C:\Users\Admin\Anaconda3\lib\socketserver.py", line 466, in server_bind
self.socket.bind(self.server_address)

OSError: [WinError 10013] Une tentative d’accès à un socket de manière interdite par ses autorisations d’accès a été tentée