(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.
Create the new project (button on the right).
I called mine gPhotoSpy
Then on the burgher menu select: Api & Services
> Library
.
In the library, search for "Photo", select Photo Library API
Then, click on the ENABLE
button: it changes to MANAGE
once clicked.
If you click to MANAGE
it gets you straight to the Google Photo API Management page.
On the API & Services, Photo Library API
select Credentials
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)
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).
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
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
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
The first thing we will need, then, is authorization:
>>> from gphotospy import authorize
We then provide the secrets file:
>>> CLIENT_SECRET_FILE = "gphoto_oauth.json"
Now we need to get authorized by Google to proceed:
>>> service = authorize.init(CLIENT_SECRET_FILE)
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)
Since our app is not (yet) verified, Google will show us a fairly intimidating page that warns us that the app is not verified.
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)
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:
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.
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.
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 *
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)
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>
Ok, lets start listing our albums.
>>> first_album = next(album_iterator)
>>> first_album
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"
}
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....'
We could get easily also its title, if present:
>>> first_album.get("title")
'Happy Birthday'
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)
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
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
}
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...'
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
The following code is all Tk boilerplate.
>>> root = Tk()
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')
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>
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)
Let's use urlopen
from urllib.request to get a raw image
>>> image_bytes = urlopen(image_url).read()
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))
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
Now that we added our image it should diplay it in our window.
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()
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)
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:
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.
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 🤞
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:"
Here's my fix:
Question. Is the code in GitHub. Can I provide a PR if I find other issues?
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