Passed
Push — master ( 9e42ed...b2d36d )
by Jaisen
02:09
created

GooglePhotos.set_session()   A

Complexity

Conditions 4

Size

Total Lines 29
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 22
nop 1
dl 0
loc 29
rs 9.352
c 0
b 0
f 0
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