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