【Python】Google Photos API でアルバムの画像取得
はじめに
我が家ではこどもたちの写真を撮影月ごとにGoogle Photosの共有アルバムにまとめて両親に共有しています。
これらの写真をAPIで取得して、フォトフレームなど使えるといいなーと思っていましたが、以前はGoogle Photos のAPIは整備されておらず、Picasa APIのみでした。
(Picasa自体も既にサービス終了しており、APIもあまりメンテナンスされている雰囲気でもなくいつか終わりそうな気がしたので、こっちを使うことは避けてました。)
気が付くと2018年にGoogle Photos APIがリリースされていたので、これを使っての写真取得を試してみたいと思います。
最終的には、年月を指定すると、その月の共有アルバムに含められている写真を取得する、というプログラムにしたいと思います。
公式リファレンスはこちらです
Credentialを取得
まず初めに、APIを使用するのに必要なCredentialを取得します。
Google Photos APを使うにはOAuthでのユーザー認証が必要になります。
OAuthの認証にはPythonのクライアントライブラリが提供されているのでそちらを使います。
上記の公式にも下記の記述がある通りGoogle Photos APIでは、Service Account方式は使えないようです。
Note that the Library API does not support service accounts; to use this API, users must be signed in to a valid Google Account.
なので、OAuth 2.0 for Web Server Applicationsか、OAuth 2.0 for Installed Applicationsのどちらかをつかうのですが、今回はWeb Serverを立てるわけではないので、Installed Applicationsタイプで実装しました。
client_secretの取得
まずはOAuthの認証に必要なclient secretを取得します。 Google API Consoleにアクセス。
左上の「APIとサービス」より「認証情報」を選択
「+認証情報を作成」をクリック
これでダウンロードよりclient_secret.json
をダウンロードします。
credentialの取得
ダウンロードしたclient_secret.json
を読み取り、credentialを取得します。
# google_photos.py import google.oauth2.credentials import google_auth_oauthlib.flow SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] API_SERVICE_NAME = 'photoslibrary' API_VERSION = 'v1' CLIENT_SECRET_FILE = 'client_secret.json' flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE,scopes=SCOPES) credentials = flow.run_console()
上記プログラムを実行すると、
$ python google_photos.py Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={$cliens_id}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary&state={$state}&prompt=consent&access_type=offline Enter the authorization code:
のように、URLが出力されるので、上記URLをブラウザに入力すると以下の画面になるので、Googleアカウントでログインしてください。
ログイン後は、以下のように認可コードが払い出されるので、コピーしてターミナルに貼り付けます。
上記でCredentialは取得出来るのですが、その中にはtokenやRefresh tokenも含まれており、その後はRefresh tokenを使用してCredentialを取得することが出来るようになるため、毎回ログインする必要がなくなります。
しかし、現在のgoogle_auth_oauthlib
まだcredentialの保存をサポートしていないので、credentialをそのままjsonのファイルで保存して、今後はRefresh Tokenを使用してcredentialを取得するようにします。
google-auth-oauthlib does not currently have support for credentials storage. It may be added in the future. See oauth2client deprecation for more details.
credentialの中にはdatetimeオブジェクトが入っているので、↓のコードを頂戴してcredentialをシリアライズして保存しておきます。
def support_datetime_default(o): if isinstance(o, datetime): return o.isoformat() raise TypeError(repr(o) + " is not JSON serializable") with open(CREDENTIAL_FILE, mode='w') as f_credential_w: f_credential_w.write(json.dumps(vars(credentials), default=support_datetime_default, sort_keys=True))
こうすることで、一度credentialを取得したあとは、ログインをし直す必要なく、Refresh Tokenでcredentialの取得が出来るようになります。
if os.path.exists(CREDENTIAL_FILE): with open(CREDENTIAL_FILE) as f_credential_r: credentials_json = json.loads(f_credential_r.read()) credentials = google.oauth2.credentials.Credentials( credentials_json['token'], refresh_token=credentials_json['_refresh_token'], token_uri=credentials_json['_token_uri'], client_id=credentials_json['_client_id'], client_secret=credentials_json['_client_secret'] )
credentialを取得後は以下のようにServiceを取得します。
service = build( API_SERVICE_NAME, API_VERSION, credentials=credentials )
今後このserviceで各種APIを叩いていきます。
これまでのコードの全体像は以下のようになります。
# google_photos.py import google.oauth2.credentials import google_auth_oauthlib.flow import json,os from datetime import datetime,date from googleapiclient.discovery import build SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] API_SERVICE_NAME = 'photoslibrary' API_VERSION = 'v1' CLIENT_SECRET_FILE = 'client_secret.json' CREDENTIAL_FILE = 'credential.json' def support_datetime_default(o): if isinstance(o, datetime): return o.isoformat() raise TypeError(repr(o) + " is not JSON serializable") def getCredentials(): if os.path.exists(CREDENTIAL_FILE): with open(CREDENTIAL_FILE) as f_credential_r: credentials_json = json.loads(f_credential_r.read()) credentials = google.oauth2.credentials.Credentials( credentials_json['token'], refresh_token=credentials_json['_refresh_token'], token_uri=credentials_json['_token_uri'], client_id=credentials_json['_client_id'], client_secret=credentials_json['_client_secret'] ) else: flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE,scopes=SCOPES) credentials = flow.run_console() with open(CREDENTIAL_FILE, mode='w') as f_credential_w: f_credential_w.write(json.dumps(vars(credentials), default=support_datetime_default, sort_keys=True)) return credentials def main(): credentials = getCredentials() service = build( API_SERVICE_NAME, API_VERSION, credentials=credentials ) if __name__ == "__main__": main()
各アルバム内の写真を取得
共有アルバムIDの取得
アルバムIDは以下のように取得できます。
sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute()
我が家では前述の通り、子供たちの写真を共有アルバムにて両親に共有しています。
その際に子供の写真については、「yyyymm01〜」の命名規則でアルバム名をつけているので、
該当するアルバム名を取得します。
nextPageToken = '' albums = {} while True: sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute() for sharedAlbum in sharedAlbums['sharedAlbums']: if 'title' in sharedAlbum: m = re.search(r'^\d\d\d\d\d\d01', sharedAlbum['title']) if m: month = m.group() if not (month in albums): albums[m.group()] = [] albums[m.group()].append(sharedAlbum['id']) if 'nextPageToken' in sharedAlbums: nextPageToken = sharedAlbums['nextPageToken'] else: break
その際に子供の写真については、「yyyymm01〜」の命名規則でアルバム名をつけているので、
この運用を始めてからは基本的に一月に1共有アルバムになったのですが、
この運用ルールで実施できていなかった頃に、一月の間に複数アルバムを作ってしまっているので、
以下のように複数取得出来るようにしています。
if not (month in albums): albums[m.group()] = [] albums[m.group()].append(sharedAlbum['id'])
また最終的には、年月を指定すると、その月の写真をとってくる、というプログラムにしたいので、毎回共有アルバムのIDを取得するのは面倒なので、
予め子供の写真を登録している共有アルバムのリストを取得してファイルに保存しておき、
写真を取得するときは、指定した年月の共有アルバムIDリストのファイルから読み込む、という動きにしました。
MONTH_ALBUMS_FILE = 'month_albums.json' def updateAlbumList(service): nextPageToken = '' albums = {} while True: sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute() for sharedAlbum in sharedAlbums['sharedAlbums']: if 'title' in sharedAlbum: m = re.search(r'^\d\d\d\d\d\d01', sharedAlbum['title']) if m: month = m.group() if not (month in albums): albums[m.group()] = [] monthAlbums[m.group()].append(sharedAlbum['id']) if 'nextPageToken' in sharedAlbums: nextPageToken = sharedAlbums['nextPageToken'] else: break with open(MONTH_ALBUMS_FILE, mode='w') as f_month_albums_w: f_month_albums_w.write(json.dumps(albums, default=support_datetime_default, sort_keys=True)) def getAlbumIds(months): albumIds = [] with open(MONTH_ALBUMS_FILE) as f_month_album_r: monthAlbumJson = json.loads(f_month_album_r.read()) for month in months: monthAlbumIds = monthAlbumJson[month] for monthAlbumId in monthAlbumIds: albumIds.append(monthAlbumId) return albumIds
共有アルバムのIDを取得してMONTH_ALBUMS_FILE
というファイルに保存しておくupdateAlbumList
という関数と、
年月をyyyymm01
形式で渡すと共有アルバムのIDを返却するgetAlbumIds
という関数とで分けました。
共有アルバム内の写真を取得
いよいよ写真の取得ですが、写真の取得は
mediaItems.searchというIFで取得ができます。 リクエストbodyのJSONの中にアルバムIDを含めてリクエストすると、そのアルバム内の写真を取得できます。
以下ように取得できます。
body = { 'albumId' : albumId, 'pageSize' : 50, } mediaItems = service.mediaItems().search(body=body).execute()
アルバムIDの配列を渡されたら、そのアルバムID内の写真を取得するような関数にしました。
def getPhotos(service, albumIds): photos = [] for albumId in albumIds: nextPageTokenMediaItems = '' while True: photo = {} body = { 'albumId' : albumId, 'pageSize' : 50, 'pageToken' : nextPageTokenMediaItems } mediaItems = service.mediaItems().search(body=body).execute() # print(mediaItems) for mediaItem in mediaItems['mediaItems']: photo['id'],photo['url'] = mediaItem['id'],mediaItem['baseUrl'] photos.append(photo) if 'nextPageToken' in mediaItems: nextPageTokenMediaItems = mediaItems['nextPageToken'] else: break print(photos)
最終的な全体の実装は以下の通りです。
from googleapiclient.discovery import build import google.oauth2.credentials import google_auth_oauthlib.flow import json,os,re from datetime import datetime,date SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] API_SERVICE_NAME = 'photoslibrary' API_VERSION = 'v1' CLIENT_SECRET_FILE = 'client_secret.json' REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' CREDENTIAL_FILE = 'credential.json' MONTH_ALBUMS_FILE = 'month_albums.json' def support_datetime_default(o): if isinstance(o, datetime): return o.isoformat() raise TypeError(repr(o) + " is not JSON serializable") def getCredentials(): if os.path.exists(CREDENTIAL_FILE): with open(CREDENTIAL_FILE) as f_credential_r: credentials_json = json.loads(f_credential_r.read()) credentials = google.oauth2.credentials.Credentials( credentials_json['token'], refresh_token=credentials_json['_refresh_token'], token_uri=credentials_json['_token_uri'], client_id=credentials_json['_client_id'], client_secret=credentials_json['_client_secret'] ) else: flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE,scopes=SCOPES) credentials = flow.run_console() with open(CREDENTIAL_FILE, mode='w') as f_credential_w: f_credential_w.write(json.dumps(vars(credentials), default=support_datetime_default, sort_keys=True)) return credentials def updateAlbumList(service): nextPageToken = '' albums = {} while True: sharedAlbums = service.sharedAlbums().list(pageSize=20,pageToken=nextPageToken).execute() for sharedAlbum in sharedAlbums['sharedAlbums']: if 'title' in sharedAlbum: m = re.search(r'^\d\d\d\d\d\d01', sharedAlbum['title']) if m: month = m.group() if not (month in albums): albums[m.group()] = [] albums[m.group()].append(sharedAlbum['id']) if 'nextPageToken' in sharedAlbums: nextPageToken = sharedAlbums['nextPageToken'] else: break with open(MONTH_ALBUMS_FILE, mode='w') as f_month_albums_w: f_month_albums_w.write(json.dumps(albums, default=support_datetime_default, sort_keys=True)) def getPhotos(service, albumIds): photos = [] for albumId in albumIds: nextPageTokenMediaItems = '' while True: photo = {} body = { 'albumId' : albumId, 'pageSize' : 50, 'pageToken' : nextPageTokenMediaItems } mediaItems = service.mediaItems().search(body=body).execute() # print(mediaItems) for mediaItem in mediaItems['mediaItems']: photo['id'],photo['url'] = mediaItem['id'],mediaItem['baseUrl'] photos.append(photo) if 'nextPageToken' in mediaItems: nextPageTokenMediaItems = mediaItems['nextPageToken'] else: break print(photos) def getAlbumIds(months): albumIds = [] with open(MONTH_ALBUMS_FILE) as f_month_album_r: monthAlbumJson = json.loads(f_month_album_r.read()) for month in months: monthAlbumIds = monthAlbumJson[month] for monthAlbumId in monthAlbumIds: albumIds.append(monthAlbumId) return albumIds def main(): credentials = getCredentials() service = build( API_SERVICE_NAME, API_VERSION, credentials=credentials ) updateAlbumList(service) # ここは必要に応じて実施するように後々処理を分けるつもり months = ['20190101','20190201'] albumIds = getAlbumIds(months) getPhotos(service, albumIds) if __name__ == "__main__": main()
まとめ
Pythonを使って、Google Photos APIを使ってみました。
共有アルバムから写真は取得出来るようになったので、今度はAlexaと連携して、声で指示した写真を取得出来るようにしたいと思います。
2021年3月15日追記
一年経ったぐらいに久しぶりにこのコードを動かしてみたらエラーが出るようになってしまったので修正しました。