#pylint: disable=C,R,W0212,W1202,W1203 from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import AuthorizedSession from google.oauth2.credentials import Credentials import json import os.path import logging import datetime from time import sleep import io from PIL import Image, ImageDraw CLIENT_ID_FILE = ".gphoto_oauth.json" TOKEN_FILE = ".gphoto_token" class GooglePhotoException(Exception): def __init__(self, errCode, errMsg, resp): super().__init__(errCode, errMsg) self.errCode = errCode self.errMsg = errMsg self.resp = resp def __repr__(self): return "GooglePhotoException " + repr(self.errCode) + ": " + repr(self.errMsg) class GooglePhoto: SCOPES = ['https://www.googleapis.com/auth/photoslibrary', 'https://www.googleapis.com/auth/photoslibrary.sharing'] ALLOWED_EXTENSIONS = ('BMP', 'GIF', 'HEIC', 'ICO', 'JPG', 'PNG', 'TIFF', 'WEBP', 'RAW', '3GP', '3G2', 'ASF', 'AVI', 'DIVX', 'M2T', 'M2TS', 'M4V', 'MKV', 'MMV', 'MOD', 'MOV', 'MP4', 'MPG', 'MTS', 'TOD', 'WMV') def __init__(self): self.clientIdFile = CLIENT_ID_FILE self.tokenFile = TOKEN_FILE self.__getAuthorizedSession() def __startAuthorizationFlow(self): flow = InstalledAppFlow.from_client_secrets_file( self.clientIdFile, scopes = self.SCOPES) credentials = flow.run_local_server(host='localhost', port=8081, authorization_prompt_message="", success_message='The auth flow is complete; you may close this window.', open_browser=True) return credentials def __getAuthorizedSession(self): cred = None try: cred = Credentials.from_authorized_user_file(self.tokenFile, self.SCOPES) except OSError as err: logging.debug("Error opening auth token file - {0}".format(err)) except ValueError: logging.debug("Error loading auth tokens - Incorrect format") if not cred: cred = self.__startAuthorizationFlow() try: cred_dict = { 'token': cred.token, 'refresh_token': cred.refresh_token, 'id_token': cred.id_token, 'scopes': cred.scopes, 'token_uri': cred.token_uri, 'client_id': cred.client_id, 'client_secret': cred.client_secret } with open(self.tokenFile, 'w') as fp: json.dump(cred_dict, fp) except OSError as err: logging.debug("Could not save auth tokens - {0}".format(err)) self.session = AuthorizedSession(cred) def get(self, url, **kwargs): return self.__request(url, get=True, **kwargs) def post(self, url, **kwargs): return self.__request(url, get=False, **kwargs) def __request(self, url, get = True, jsonResponse = True, **kwargs): for i in range(5): logging.debug(f"Sending {'GET' if get else 'POST'} request: {url}") for key, value in kwargs.items(): if key == "data": continue logging.debug(f" {key}: {value}") if get: resp = self.session.get(url, **kwargs) else: resp = self.session.post(url, **kwargs) logging.debug(f"Server response: {resp}") if resp.status_code == 200: break if resp.status_code == 429: logging.info("Quota exceeded, sleeping a bit") sleep(30) elif 500 <= resp.status_code < 600: logging.info("Something 5xx went wrong, sleping a bit") sleep(2 ** i) else: raise GooglePhotoException(resp.status_code, resp.text, resp) else: raise GooglePhotoException(resp.status_code, resp.text, resp) if jsonResponse: return resp.json() return resp def getMediaItems(self, albumId, pageSize = None): params = {} params['albumId'] = albumId if pageSize: params['pageSize'] = pageSize while True: resp = self.post('https://photoslibrary.googleapis.com/v1/mediaItems:search', params=params) if 'mediaItems' in resp: for mi in resp["mediaItems"]: yield mi if 'nextPageToken' in resp: params["pageToken"] = resp["nextPageToken"] else: return else: return def main(): logging.basicConfig(level=logging.INFO) api = GooglePhoto() albumTitle = f"TestAlbum{datetime.datetime.now().strftime('%d_%m_%Y-%H:%M:%S')}" print(albumTitle) albumId = api.post('https://photoslibrary.googleapis.com/v1/albums', json={"album":{"title": albumTitle}})["id"] print(albumId) for i in range(100, 0, -1): img = Image.new('RGB', (128, 128), color = (73, 109, 137)) d = ImageDraw.Draw(img) d.text((10,10), f"{i}", fill=(255, 255, 0)) with io.BytesIO() as fp: img.save(fp, format="jpeg") fp.seek(0) photo_bytes = fp.read() print (f"Uploading item {i:3.0f}", end="\r") headers = {} headers["Content-type"] = "application/octet-stream" headers["X-Goog-Upload-Protocol"] = "raw" headers["X-Goog-Upload-Content-Type"] = "image/jpeg" upload_token = api.post('https://photoslibrary.googleapis.com/v1/uploads', data=photo_bytes, jsonResponse=False) create_body = json.dumps({ "albumId":albumId, "newMediaItems":[ {"description":"", "simpleMediaItem": {"fileName": f"{i}.jpg", "uploadToken":upload_token.content.decode()}}]}, indent=4) api.post('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', data=create_body) print (" ", end="\r") def printState(msg): print(msg) a = api.get(f'https://photoslibrary.googleapis.com/v1/albums/{albumId}') print(f"MediaCount: {a['mediaItemsCount']}") print(f"Search result: {len(list(api.getMediaItems(albumId)))}") print(f"Search result[pageSize=100]: {len(list(api.getMediaItems(albumId, 100)))}") print(f"Search result[pageSize=90]: {len(list(api.getMediaItems(albumId, 90)))}") print(f"Search result[pageSize=10]: {len(list(api.getMediaItems(albumId, 10)))}") # Right after uploading images, mediaItemsCount # and actual returned mediaItem list still consistent printState("After upload") print(f"Open {albumTitle} in Google Photos and change ordering to 'Recently added'.") input("Press Enter to continue...") # After changing order in Google Photos still consistent printState("After changing order on Google Photos") mediaItems = sorted([mi["id"] for mi in api.getMediaItems(albumId)]) chunkSize = 25 for i in range(0, len(mediaItems), chunkSize): chunk = mediaItems[i:i + chunkSize] request = { "mediaItemIds": chunk} print(f"Removing and adding {len(chunk)} items") api.post(f"https://photoslibrary.googleapis.com/v1/albums/{albumId}:batchRemoveMediaItems", json=request) api.post(f"https://photoslibrary.googleapis.com/v1/albums/{albumId}:batchAddMediaItems", json=request) # After removing/adding the whole list mediaItemsCount still correct, # Google Photos shows complete album, # but returned mediaItems list is wrong printState("After Remove+add") if __name__ == '__main__': main()