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 = [ |
54
|
|
|
'https://www.googleapis.com/auth/photoslibrary', |
55
|
|
|
'https://www.googleapis.com/auth/photoslibrary.appendonly', |
56
|
|
|
'https://www.googleapis.com/auth/photoslibrary.sharing' |
57
|
|
|
] |
58
|
|
|
|
59
|
|
|
self.secrets_file = None |
60
|
|
|
if('secrets_file' in self.config_for_plugin): |
61
|
|
|
self.secrets_file = self.config_for_plugin['secrets_file'] |
62
|
|
|
# 'client_id.json' |
63
|
|
|
self.auth_file = None |
64
|
|
|
if('auth_file' in self.config_for_plugin): |
65
|
|
|
self.auth_file = self.config_for_plugin['auth_file'] |
66
|
|
|
self.session = None |
67
|
|
|
|
68
|
|
|
def after(self, file_path, destination_folder, final_file_path, metadata): |
69
|
|
|
extension = metadata['extension'] |
70
|
|
|
if(extension in Photo.extensions or extension in Video.extensions): |
71
|
|
|
self.log(u'Added {} to db.'.format(final_file_path)) |
72
|
|
|
self.db.set(final_file_path, metadata['original_name']) |
73
|
|
|
else: |
74
|
|
|
self.log(u'Skipping {} which is not a supported media type.'.format(final_file_path)) |
75
|
|
|
|
76
|
|
|
def batch(self): |
77
|
|
|
queue = self.db.get_all() |
78
|
|
|
status = True |
79
|
|
|
count = 0 |
80
|
|
|
for key in queue: |
81
|
|
|
this_status = self.upload(key) |
82
|
|
|
if(this_status): |
83
|
|
|
# Remove from queue if successful then increment count |
84
|
|
|
self.db.delete(key) |
85
|
|
|
count = count + 1 |
86
|
|
|
self.display('{} uploaded successfully.'.format(key)) |
87
|
|
|
else: |
88
|
|
|
status = False |
89
|
|
|
self.display('{} failed to upload.'.format(key)) |
90
|
|
|
return (status, count) |
91
|
|
|
|
92
|
|
|
def before(self, file_path, destination_folder): |
93
|
|
|
pass |
94
|
|
|
|
95
|
|
|
def set_session(self): |
96
|
|
|
# Try to load credentials from an auth file. |
97
|
|
|
# If it doesn't exist or is not valid then catch the |
98
|
|
|
# exception and reauthenticate. |
99
|
|
|
try: |
100
|
|
|
creds = Credentials.from_authorized_user_file(self.auth_file, self.scopes) |
101
|
|
|
except: |
102
|
|
|
try: |
103
|
|
|
flow = InstalledAppFlow.from_client_secrets_file(self.secrets_file, self.scopes) |
104
|
|
|
creds = flow.run_local_server() |
105
|
|
|
cred_dict = { |
106
|
|
|
'token': creds.token, |
107
|
|
|
'refresh_token': creds.refresh_token, |
108
|
|
|
'id_token': creds.id_token, |
109
|
|
|
'scopes': creds.scopes, |
110
|
|
|
'token_uri': creds.token_uri, |
111
|
|
|
'client_id': creds.client_id, |
112
|
|
|
'client_secret': creds.client_secret |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
# Store the returned authentication tokens to the auth_file. |
116
|
|
|
with open(self.auth_file, 'w') as f: |
117
|
|
|
f.write(json.dumps(cred_dict)) |
118
|
|
|
except: |
119
|
|
|
return |
120
|
|
|
|
121
|
|
|
self.session = AuthorizedSession(creds) |
122
|
|
|
self.session.headers["Content-type"] = "application/octet-stream" |
123
|
|
|
self.session.headers["X-Goog-Upload-Protocol"] = "raw" |
124
|
|
|
|
125
|
|
|
def upload(self, path_to_photo): |
126
|
|
|
self.set_session() |
127
|
|
|
if(self.session is None): |
128
|
|
|
self.log('Could not initialize session') |
129
|
|
|
return None |
130
|
|
|
|
131
|
|
|
self.session.headers["X-Goog-Upload-File-Name"] = basename(path_to_photo) |
132
|
|
|
if(not isfile(path_to_photo)): |
133
|
|
|
self.log('Could not find file: {}'.format(path_to_photo)) |
134
|
|
|
return None |
135
|
|
|
|
136
|
|
|
with open(path_to_photo, 'rb') as f: |
137
|
|
|
photo_bytes = f.read() |
138
|
|
|
|
139
|
|
|
upload_token = self.session.post(self.upload_url, photo_bytes) |
140
|
|
|
if(upload_token.status_code != 200 or not upload_token.content): |
141
|
|
|
self.log('Uploading media failed: ({}) {}'.format(upload_token.status_code, upload_token.content)) |
142
|
|
|
return None |
143
|
|
|
|
144
|
|
|
create_body = json.dumps({'newMediaItems':[{'description':'','simpleMediaItem':{'uploadToken':upload_token.content.decode()}}]}, indent=4) |
145
|
|
|
resp = self.session.post(self.media_create_url, create_body).json() |
146
|
|
|
if( |
147
|
|
|
'newMediaItemResults' not in resp or |
148
|
|
|
'status' not in resp['newMediaItemResults'][0] or |
149
|
|
|
'message' not in resp['newMediaItemResults'][0]['status'] or |
150
|
|
|
( |
151
|
|
|
resp['newMediaItemResults'][0]['status']['message'] != 'Success' and # photos |
152
|
|
|
resp['newMediaItemResults'][0]['status']['message'] != 'OK' # videos |
153
|
|
|
) |
154
|
|
|
|
155
|
|
|
): |
156
|
|
|
self.log('Creating new media item failed: {}'.format(resp['newMediaItemResults'][0]['status'])) |
157
|
|
|
return None |
158
|
|
|
|
159
|
|
|
return resp['newMediaItemResults'][0] |
160
|
|
|
|