155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
"""
|
|
Google Photos plugin object.
|
|
This plugin will queue imported photos into the plugin's database file.
|
|
Using this plugin should have no impact on performance of importing photos.
|
|
|
|
In order to upload the photos to Google Photos you need to run the following command.
|
|
|
|
```
|
|
./elodie.py batch
|
|
```
|
|
|
|
That command will execute the batch() method on all plugins, including this one.
|
|
This plugin's batch() function reads all files from the database file and attempts to
|
|
upload them to Google Photos.
|
|
This plugin does not aim to keep Google Photos in sync.
|
|
Once a photo is uploaded it's removed from the database and no records are kept thereafter.
|
|
|
|
Upload code adapted from https://github.com/eshmu/gphotos-upload
|
|
|
|
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
|
"""
|
|
|
|
import json
|
|
|
|
from os.path import basename, isfile
|
|
|
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
from google.auth.transport.requests import AuthorizedSession
|
|
from google.oauth2.credentials import Credentials
|
|
|
|
from elodie.media.photo import Photo
|
|
from elodie.media.video import Video
|
|
from elodie.plugins.plugins import PluginBase
|
|
|
|
class GooglePhotos(PluginBase):
|
|
"""A class to execute plugin actions.
|
|
|
|
Requires a config file with the following configurations set.
|
|
secrets_file:
|
|
The full file path where to find the downloaded secrets.
|
|
auth_file:
|
|
The full file path where to store authenticated tokens.
|
|
|
|
"""
|
|
|
|
__name__ = 'GooglePhotos'
|
|
|
|
def __init__(self):
|
|
super(GooglePhotos, self).__init__()
|
|
self.upload_url = 'https://photoslibrary.googleapis.com/v1/uploads'
|
|
self.media_create_url = 'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate'
|
|
self.scopes = ['https://www.googleapis.com/auth/photoslibrary.appendonly']
|
|
|
|
self.secrets_file = None
|
|
if('secrets_file' in self.config_for_plugin):
|
|
self.secrets_file = self.config_for_plugin['secrets_file']
|
|
# 'client_id.json'
|
|
self.auth_file = None
|
|
if('auth_file' in self.config_for_plugin):
|
|
self.auth_file = self.config_for_plugin['auth_file']
|
|
self.session = None
|
|
|
|
def after(self, file_path, destination_folder, final_file_path, metadata):
|
|
extension = metadata['extension']
|
|
if(extension in Photo.extensions or extension in Video.extensions):
|
|
self.log(u'Added {} to db.'.format(final_file_path))
|
|
self.db.set(final_file_path, metadata['original_name'])
|
|
else:
|
|
self.log(u'Skipping {} which is not a supported media type.'.format(final_file_path))
|
|
|
|
def batch(self):
|
|
queue = self.db.get_all()
|
|
status = True
|
|
count = 0
|
|
for key in queue:
|
|
this_status = self.upload(key)
|
|
if(this_status):
|
|
# Remove from queue if successful then increment count
|
|
self.db.delete(key)
|
|
count = count + 1
|
|
self.display('{} uploaded successfully.'.format(key))
|
|
else:
|
|
status = False
|
|
self.display('{} failed to upload.'.format(key))
|
|
return (status, count)
|
|
|
|
def before(self, file_path, destination_folder):
|
|
pass
|
|
|
|
def set_session(self):
|
|
# Try to load credentials from an auth file.
|
|
# If it doesn't exist or is not valid then catch the
|
|
# exception and reauthenticate.
|
|
try:
|
|
creds = Credentials.from_authorized_user_file(self.auth_file, self.scopes)
|
|
except:
|
|
try:
|
|
flow = InstalledAppFlow.from_client_secrets_file(self.secrets_file, self.scopes)
|
|
creds = flow.run_local_server()
|
|
cred_dict = {
|
|
'token': creds.token,
|
|
'refresh_token': creds.refresh_token,
|
|
'id_token': creds.id_token,
|
|
'scopes': creds.scopes,
|
|
'token_uri': creds.token_uri,
|
|
'client_id': creds.client_id,
|
|
'client_secret': creds.client_secret
|
|
}
|
|
|
|
# Store the returned authentication tokens to the auth_file.
|
|
with open(self.auth_file, 'w') as f:
|
|
f.write(json.dumps(cred_dict))
|
|
except:
|
|
return
|
|
|
|
self.session = AuthorizedSession(creds)
|
|
self.session.headers["Content-type"] = "application/octet-stream"
|
|
self.session.headers["X-Goog-Upload-Protocol"] = "raw"
|
|
|
|
def upload(self, path_to_photo):
|
|
self.set_session()
|
|
if(self.session is None):
|
|
self.log('Could not initialize session')
|
|
return None
|
|
|
|
self.session.headers["X-Goog-Upload-File-Name"] = basename(path_to_photo)
|
|
if(not isfile(path_to_photo)):
|
|
self.log('Could not find file: {}'.format(path_to_photo))
|
|
return None
|
|
|
|
with open(path_to_photo, 'rb') as f:
|
|
photo_bytes = f.read()
|
|
|
|
upload_token = self.session.post(self.upload_url, photo_bytes)
|
|
if(upload_token.status_code != 200 or not upload_token.content):
|
|
self.log('Uploading media failed: ({}) {}'.format(upload_token.status_code, upload_token.content))
|
|
return None
|
|
|
|
create_body = json.dumps({'newMediaItems':[{'description':'','simpleMediaItem':{'uploadToken':upload_token.content.decode()}}]}, indent=4)
|
|
resp = self.session.post(self.media_create_url, create_body).json()
|
|
if(
|
|
'newMediaItemResults' not in resp or
|
|
'status' not in resp['newMediaItemResults'][0] or
|
|
'message' not in resp['newMediaItemResults'][0]['status'] or
|
|
(
|
|
resp['newMediaItemResults'][0]['status']['message'] != 'Success' and # photos
|
|
resp['newMediaItemResults'][0]['status']['message'] != 'OK' # videos
|
|
)
|
|
|
|
):
|
|
self.log('Creating new media item failed: {}'.format(json.dumps(resp)))
|
|
return None
|
|
|
|
return resp['newMediaItemResults'][0]
|