Passed
Pull Request — master (#319)
by Jaisen
01:53
created

GooglePhotos.batch()   A

Complexity

Conditions 3

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nop 1
dl 0
loc 15
rs 9.75
c 0
b 0
f 0
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
            try:
88
                flow = InstalledAppFlow.from_client_secrets_file(self.secrets_file, self.scopes)
89
                creds = flow.run_local_server()
90
                cred_dict = {
91
                    'token': creds.token,
92
                    'refresh_token': creds.refresh_token,
93
                    'id_token': creds.id_token,
94
                    'scopes': creds.scopes,
95
                    'token_uri': creds.token_uri,
96
                    'client_id': creds.client_id,
97
                    'client_secret': creds.client_secret
98
                }
99
100
                # Store the returned authentication tokens to the auth_file.
101
                with open(self.auth_file, 'w') as f:
102
                    f.write(json.dumps(cred_dict))
103
            except:
104
                return
105
106
        self.session = AuthorizedSession(creds)
107
        self.session.headers["Content-type"] = "application/octet-stream"
108
        self.session.headers["X-Goog-Upload-Protocol"] = "raw"
109
110
    def upload(self, path_to_photo):
111
        self.set_session()
112
        if(self.session is None):
113
            self.log('Could not initialize session')
114
            return None
115
116
        self.session.headers["X-Goog-Upload-File-Name"] = basename(path_to_photo)
117
        if(not isfile(path_to_photo)):
118
            self.log('Could not find file: {}'.format(path_to_photo))
119
            return None
120
121
        with open(path_to_photo, 'rb') as f:
122
            photo_bytes = f.read()
123
124
        upload_token = self.session.post(self.upload_url, photo_bytes)
125
        if(upload_token.status_code != 200 or not upload_token.content):
126
            self.log('Uploading media failed: ({}) {}'.format(upload_token.status_code, upload_token.content))
127
            return None
128
129
        create_body = json.dumps({'newMediaItems':[{'description':'','simpleMediaItem':{'uploadToken':upload_token.content.decode()}}]}, indent=4)
130
        resp = self.session.post(self.media_create_url, create_body).json()
131
        if(
132
            'newMediaItemResults' not in resp or
133
            'status' not in resp['newMediaItemResults'][0] or
134
            'message' not in resp['newMediaItemResults'][0]['status'] or
135
            (
136
                resp['newMediaItemResults'][0]['status']['message'] != 'Success' and # photos
137
                resp['newMediaItemResults'][0]['status']['message'] != 'OK' # videos
138
            )
139
140
        ):
141
            self.log('Creating new media item failed: {}'.format(resp['newMediaItemResults'][0]['status']))
142
            return None
143
        
144
        return resp['newMediaItemResults'][0]
145