Mercurial > lbo > hg > photosync
changeset 0:7d1e69d4e2ac
Initial commit
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Fri, 14 Jun 2019 13:59:20 +0200 |
parents | |
children | a119256589bc |
files | Pipfile Pipfile.lock photosync.py |
diffstat | 3 files changed, 335 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Pipfile Fri Jun 14 13:59:20 2019 +0200 @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +google-api-python-client = "*" +google-auth-httplib2 = "*" +google-auth-oauthlib = "*" +python-dateutil = "*" + +[requires] +python_version = "3.7"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Pipfile.lock Fri Jun 14 13:59:20 2019 +0200 @@ -0,0 +1,159 @@ +{ + "_meta": { + "hash": { + "sha256": "92e1fcff9dc3bf8ed8a7de79e9fc8f419f16b9644ec2bf80975549f81c296044" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "cachetools": { + "hashes": [ + "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae", + "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a" + ], + "version": "==3.1.1" + }, + "certifi": { + "hashes": [ + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + ], + "version": "==2019.3.9" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "google-api-python-client": { + "hashes": [ + "sha256:048da0d68564380ee23b449e5a67d4666af1b3b536d2fb0a02cee1ad540fa5ec", + "sha256:5def5a485b1cbc998b8f869456c7bde0c0e6d3d0a5ea1f300b5ef57cb4b1ce8f" + ], + "index": "pypi", + "version": "==1.7.9" + }, + "google-auth": { + "hashes": [ + "sha256:0f7c6a64927d34c1a474da92cfc59e552a5d3b940d3266606c6a28b72888b9e4", + "sha256:20705f6803fd2c4d1cc2dcb0df09d4dfcb9a7d51fd59e94a3a28231fd93119ed" + ], + "version": "==1.6.3" + }, + "google-auth-httplib2": { + "hashes": [ + "sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445", + "sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08" + ], + "index": "pypi", + "version": "==0.0.3" + }, + "google-auth-oauthlib": { + "hashes": [ + "sha256:6a8b0072048940d1f41c23c03576867e577e826fec140a1c7e148ec486e083ba", + "sha256:904d72541fc92adb2026767dd364ab3a51f6e893d64b13cdc6acbd580c841468" + ], + "index": "pypi", + "version": "==0.4.0" + }, + "httplib2": { + "hashes": [ + "sha256:158fbd0ffbba536829d664bf3f32c4f45df41f8f791663665162dfaf21ffd075", + "sha256:d1146939d270f1f1eb8cbf8f5aa72ff37d897faccca448582bb1e180aeb4c6b2" + ], + "version": "==0.13.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "oauthlib": { + "hashes": [ + "sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298", + "sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e" + ], + "version": "==3.0.1" + }, + "pyasn1": { + "hashes": [ + "sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", + "sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e" + ], + "version": "==0.4.5" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:ef721f68f7951fab9b0404d42590f479e30d9005daccb1699b0a51bb4177db96", + "sha256:f309b6c94724aeaf7ca583feb1cc70430e10d7551de5e36edfc1ae6909bcfb3c" + ], + "version": "==0.2.5" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "index": "pypi", + "version": "==2.8.0" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "version": "==2.22.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57", + "sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140" + ], + "version": "==1.2.0" + }, + "rsa": { + "hashes": [ + "sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66", + "sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487" + ], + "version": "==4.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "uritemplate": { + "hashes": [ + "sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", + "sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", + "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d" + ], + "version": "==3.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", + "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" + ], + "version": "==1.25.3" + } + }, + "develop": {} +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/photosync.py Fri Jun 14 13:59:20 2019 +0200 @@ -0,0 +1,161 @@ + +import datetime +import dateutil.parser +import json +import os.path +import pickle +import sqlite3 +import urllib3 +from googleapiclient.discovery import build +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request + +class TokenSource: + SCOPES = ['https://www.googleapis.com/auth/photoslibrary'] + CRED_ID = 'installed.main' + + def __init__(self, db=None, tokensfile=None, clientsecret='clientsecret.json'): + self._db = db + self._tokensfile = tokensfile + self._clientsecret = clientsecret + + def creds(self): + if self._tokensfile and os.path.exists(self._tokensfile): + with open(self._tokensfile, 'rb') as f: + creds = pickle.load(f) + return creds + elif self._db: + creds = self._db.get_credentials(self.CRED_ID) + if creds: + creds = pickle.loads(creds) + return creds + flow = InstalledAppFlow.from_client_secrets_file(self._clientsecret, self.SCOPES) + creds = flow.run_local_server() + if creds and self._tokensfile: + with open(self._tokensfile, 'wb') as f: + pickle.dump(creds, f) + if creds and self._db: + self._db.store_credentials(self.CRED_ID, pickle.dumps(creds)) + return creds + +class PhotosService: + + def __init__(self, tokens=None): + self._token_source = tokens + self._service = build('photoslibrary', 'v1', credentials=tokens.creds()) + self._http = urllib3.PoolManager() + + def list_library(self, start=None, to=None): + """Yields items from the library. + + Arguments: + start: datetime.date + end: datetime.date + + Returns: + [mediaItem] + """ + filters = {} + if start or to: + rng_filter = {'ranges': {}} + if start: + rng_filter['ranges']['startDate'] = {'year': start.year, 'month': start.month, 'day': start.day} + else: + rng_filter['ranges']['startDate'] = {'year': 1999, 'month': 1, 'day': 1} + if not to: + to = datetime.datetime.now().date() + rng_filter['ranges']['endDate'] = {'year': to.year, 'month': to.month, 'day': to.day} + filters['dateFilter'] = rng_filter + pagetoken = None + while True: + resp = self._service.mediaItems().search(body={'pageSize': 25, 'filters': filters, 'pageToken': pagetoken}).execute() + items = resp['mediaItems'] + pagetoken = resp.get('nextPageToken', None) + for i in items: + yield i + if pagetoken is None: + return + + def download_photo(self, id, path): + """Download a photo and store it under its file name in the directory `path`. + """ + photo = self._service.mediaItems().get(mediaItemId=id).execute() + rawurl = photo['baseUrl'] + p = os.path.join(path, photo['filename']) + with open(p, 'wb') as f: + f.write(self._http.request('GET', rawurl).data) + +class DB: + + def __init__(self, path): + self._db = sqlite3.connect(path) + self.initdb() + self._dtparse = dateutil.parser.isoparser() + + def initdb(self): + cur = self._db.cursor() + cur.execute('CREATE TABLE IF NOT EXISTS photos (id TEXT PRIMARY KEY, creationTime TEXT, path TEXT, filename TEXT, offline INTEGER)') + cur.execute('CREATE TABLE IF NOT EXISTS transactions (id TEXT, type TEXT, time INTEGER, path TEXT, filename TEXT)') + cur.execute('CREATE TABLE IF NOT EXISTS oauth (id TEXT PRIMARY KEY, credentials BLOB)') + self._db.commit() + + def store_credentials(self, id, creds): + with self._db as conn: + cur = conn.cursor() + cur.execute('SELECT id FROM oauth WHERE id = ?', (id,)) + if not cur.fetchone(): + cur.execute('INSERT INTO oauth (id, credentials) VALUES (?, ?)', (id, creds)) + return + cur.close() + cur = conn.cursor() + cur.execute('UPDATE oauth SET credentials = ? WHERE id = ?', (creds, id)) + + def get_credentials(self, id): + with self._db as conn: + cur = conn.cursor() + cur.execute('SELECT credentials FROM oauth WHERE id = ?', (id,)) + row = cur.fetchone() + if row: + return row[0] + return None + + def add_online_photo(self, media_item, path): + with self._db as conn: + cur = conn.cursor() + cur.execute('SELECT id FROM photos WHERE id = "{}"'.format(media_item['id'])) + if cur.fetchone(): + print('WARN: Photo already in store.') + cur.close() + return + cur.close() + + creation_time = int(self._dtparse.isoparse(media_item['mediaMetadata']['creationTime']).timestamp()) + conn.cursor().execute( + 'INSERT INTO photos (id, creationTime, path, filename, offline) VALUES (?, ?, ?, ?, 0)', + media_item['id'], creation_time, path, media_item['filename']) + conn.commit() + + def mark_photo_downloaded(self, id): + with self._db as conn: + conn.cursor().execute( + 'UPDATE photos SET offline = 1 WHERE id = {}'.format(id)) + + def most_recent_creation_date(self): + with self._db as conn: + cursor = conn.cursor() + cursor.execute('SELECT creationTime FROM photos ORDER BY creationTime DESC LIMIT 1') + row = cursor.fetchone() + cursor.close() + if row: + return datetime.datetime.fromtimestamp(row[0]) + return datetime.datetime.fromtimestamp(0) + +def main(): + db = DB('sq.lite') + s = PhotosService(tokens=TokenSource(db=db)) + items = s.list_library(to=datetime.date(2019, 7, 7)) + for i in items: + db.add_online_photo(i, 'local') + +if __name__ == '__main__': + main()