1
|
|
|
""" |
2
|
|
|
Google Photos plugin object. |
3
|
|
|
This plugin will queue imported photos into the plugin's database file. |
4
|
|
|
Using this plugin should have no impact on performance of importing photos. |
5
|
|
|
|
6
|
|
|
In order to upload the photos to Google Photos you need to run the following command. |
7
|
|
|
|
8
|
|
|
``` |
9
|
|
|
./elodie.py batch |
10
|
|
|
``` |
11
|
|
|
|
12
|
|
|
That command will execute the batch() method on all plugins, including this one. |
13
|
|
|
This plugin's batch() function reads all files from the database file and attempts to |
14
|
|
|
upload them to Google Photos. |
15
|
|
|
This plugin does not aim to keep Google Photos in sync. |
16
|
|
|
Once a photo is uploaded it's removed from the database and no records are kept thereafter. |
17
|
|
|
|
18
|
|
|
Upload code adapted from https://github.com/eshmu/gphotos-upload |
19
|
|
|
|
20
|
|
|
.. moduleauthor:: Jaisen Mathai <[email protected]> |
21
|
|
|
""" |
22
|
|
|
from __future__ import print_function |
23
|
|
|
|
24
|
|
|
import json |
25
|
|
|
|
26
|
|
|
from os.path import basename, isfile |
27
|
|
|
|
28
|
|
|
from google_auth_oauthlib.flow import InstalledAppFlow |
29
|
|
|
from google.auth.transport.requests import AuthorizedSession |
30
|
|
|
from google.oauth2.credentials import Credentials |
31
|
|
|
|
32
|
|
|
from elodie.media.photo import Photo |
33
|
|
|
from elodie.media.video import Video |
34
|
|
|
from elodie.plugins.plugins import PluginBase |
35
|
|
|
|
36
|
|
|
class GooglePhotos(PluginBase): |
37
|
|
|
"""A class to execute plugin actions. |
38
|
|
|
|
39
|
|
|
Requires a config file with the following configurations set. |
40
|
|
|
secrets_file: |
41
|
|
|
The full file path where to find the downloaded secrets. |
42
|
|
|
auth_file: |
43
|
|
|
The full file path where to store authenticated tokens. |
44
|
|
|
|
45
|
|
|
""" |
46
|
|
|
|
47
|
|
|
__name__ = 'GooglePhotos' |
48
|
|
|
|
49
|
|
|
def __init__(self): |
50
|
|
|
super(GooglePhotos, self).__init__() |
51
|
|
|
self.upload_url = 'https://photoslibrary.googleapis.com/v1/uploads' |
52
|
|
|
self.media_create_url = 'https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate' |
53
|
|
|
self.scopes = ['https://www.googleapis.com/auth/photoslibrary.appendonly'] |
54
|
|
|
|
55
|
|
|
self.secrets_file = None |
56
|
|
|
if('secrets_file' in self.config_for_plugin): |
57
|
|
|
self.secrets_file = self.config_for_plugin['secrets_file'] |
58
|
|
|
# 'client_id.json' |
59
|
|
|
self.auth_file = None |
60
|
|
|
if('auth_file' in self.config_for_plugin): |
61
|
|
|
self.auth_file = self.config_for_plugin['auth_file'] |
62
|
|
|
self.session = None |
63
|
|
|
|
64
|
|
|
def after(self, file_path, destination_folder, final_file_path, metadata): |
65
|
|
|
extension = metadata['extension'] |
66
|
|
|
if(extension in Photo.extensions or extension in Video.extensions): |
67
|
|
|
self.log(u'Added {} to db.'.format(final_file_path)) |
68
|
|
|
self.db.set(final_file_path, metadata['original_name']) |
69
|
|
|
else: |
70
|
|
|
self.log(u'Skipping {} which is not a supported media type.'.format(final_file_path)) |
71
|
|
|
|
72
|
|
|
def batch(self): |
73
|
|
|
queue = self.db.get_all() |
74
|
|
|
status = True |
75
|
|
|
count = 0 |
76
|
|
|
for key in queue: |
77
|
|
|
this_status = self.upload(key) |
78
|
|
|
if(this_status): |
79
|
|
|
# Remove from queue if successful then increment count |
80
|
|
|
self.db.delete(key) |
81
|
|
|
count = count + 1 |
82
|
|
|
self.display('{} uploaded successfully.'.format(key)) |
83
|
|
|
else: |
84
|
|
|
status = False |
85
|
|
|
self.display('{} failed to upload.'.format(key)) |
86
|
|
|
return (status, count) |
87
|
|
|
|
88
|
|
|
def before(self, file_path, destination_folder): |
89
|
|
|
pass |
90
|
|
|
|
91
|
|
|
def set_session(self): |
92
|
|
|
# Try to load credentials from an auth file. |
93
|
|
|
# If it doesn't exist or is not valid then catch the |
94
|
|
|
# exception and reauthenticate. |
95
|
|
|
try: |
96
|
|
|
creds = Credentials.from_authorized_user_file(self.auth_file, self.scopes) |
97
|
|
|
except: |
98
|
|
|
try: |
99
|
|
|
flow = InstalledAppFlow.from_client_secrets_file(self.secrets_file, self.scopes) |
100
|
|
|
creds = flow.run_local_server() |
101
|
|
|
cred_dict = { |
102
|
|
|
'token': creds.token, |
103
|
|
|
'refresh_token': creds.refresh_token, |
104
|
|
|
'id_token': creds.id_token, |
105
|
|
|
'scopes': creds.scopes, |
106
|
|
|
'token_uri': creds.token_uri, |
107
|
|
|
'client_id': creds.client_id, |
108
|
|
|
'client_secret': creds.client_secret |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
# Store the returned authentication tokens to the auth_file. |
112
|
|
|
with open(self.auth_file, 'w') as f: |
113
|
|
|
f.write(json.dumps(cred_dict)) |
114
|
|
|
except: |
115
|
|
|
return |
116
|
|
|
|
117
|
|
|
self.session = AuthorizedSession(creds) |
118
|
|
|
self.session.headers["Content-type"] = "application/octet-stream" |
119
|
|
|
self.session.headers["X-Goog-Upload-Protocol"] = "raw" |
120
|
|
|
|
121
|
|
|
def upload(self, path_to_photo): |
122
|
|
|
self.set_session() |
123
|
|
|
if(self.session is None): |
124
|
|
|
self.log('Could not initialize session') |
125
|
|
|
return None |
126
|
|
|
|
127
|
|
|
self.session.headers["X-Goog-Upload-File-Name"] = basename(path_to_photo) |
128
|
|
|
if(not isfile(path_to_photo)): |
129
|
|
|
self.log('Could not find file: {}'.format(path_to_photo)) |
130
|
|
|
return None |
131
|
|
|
|
132
|
|
|
with open(path_to_photo, 'rb') as f: |
133
|
|
|
photo_bytes = f.read() |
134
|
|
|
|
135
|
|
|
upload_token = self.session.post(self.upload_url, photo_bytes) |
136
|
|
|
if(upload_token.status_code != 200 or not upload_token.content): |
137
|
|
|
self.log('Uploading media failed: ({}) {}'.format(upload_token.status_code, upload_token.content)) |
138
|
|
|
return None |
139
|
|
|
|
140
|
|
|
create_body = json.dumps({'newMediaItems':[{'description':'','simpleMediaItem':{'uploadToken':upload_token.content.decode()}}]}, indent=4) |
141
|
|
|
resp = self.session.post(self.media_create_url, create_body).json() |
142
|
|
|
if( |
143
|
|
|
'newMediaItemResults' not in resp or |
144
|
|
|
'status' not in resp['newMediaItemResults'][0] or |
145
|
|
|
'message' not in resp['newMediaItemResults'][0]['status'] or |
146
|
|
|
( |
147
|
|
|
resp['newMediaItemResults'][0]['status']['message'] != 'Success' and # photos |
148
|
|
|
resp['newMediaItemResults'][0]['status']['message'] != 'OK' # videos |
149
|
|
|
) |
150
|
|
|
|
151
|
|
|
): |
152
|
|
|
self.log('Creating new media item failed: {}'.format(json.dumps(resp))) |
153
|
|
|
return None |
154
|
|
|
|
155
|
|
|
return resp['newMediaItemResults'][0] |
156
|
|
|
|