diff --git a/.gitignore b/.gitignore index 0a0711b..3ac7422 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,112 @@ +# Project specific tags virtualenv_run/ -email_to_photo.json client_id.json client_secret.json +vdirsyncer.conf +build/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/Makefile b/Makefile index 7f4a4ce..0c7b420 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,36 @@ default: run .PHONY: run -run: virtualenv_run - ./virtualenv_run/bin/python main.py +run: email_to_photo.json virtualenv: virtualenv_run virtualenv_run: virtualenv --python python3 virtualenv_run ./virtualenv_run/bin/pip install -r ./requirements.txt + +build/email_to_photo.json: virtualenv_run + mkdir -p build + ./virtualenv_run/bin/python -m google_photo_to_vcard.build_photo_json + +build/vdirsyncer: virtualenv_run + ./vdirsyncer-wrapper discover + +build/vdirsyncer/status/my_contacts/contacts.items: build/vdirsyncer + ./vdirsyncer-wrapper sync + +.PHONY: sync-contacts +sync-contacts: build/vdirsyncer/status/my_contacts/contacts.items + +.PHONY: khard-list +khard-list: build/vdirsyncer/status/my_contacts/contacts.items + ./khard-wrapper list + +build/photos/DONE: build/email_to_photo.json + ./virtualenv_run/bin/python -m google_photo_to_vcard.download_photos + touch build/photos/DONE + +.PHONY: add-photos +add-photos: virtualenv_run build/email_to_photo.json build/vdirsyncer/status/my_contacts/contacts.items + mkdir -p build/photos + ./virtualenv_run/bin/python -m google_photo_to_vcard.add_photo diff --git a/google_photo_to_vcard/__init__.py b/google_photo_to_vcard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/google_photo_to_vcard/add_photo.py b/google_photo_to_vcard/add_photo.py new file mode 100644 index 0000000..bafcbd5 --- /dev/null +++ b/google_photo_to_vcard/add_photo.py @@ -0,0 +1,63 @@ +from pathlib import Path + +import vobject + +from google_photo_to_vcard.util import build_photo_path +from google_photo_to_vcard.util import download_url_to_path +from google_photo_to_vcard.util import read_email_photo_json + + +def open_card(card_path): + with open(card_path, mode='r') as f: + return vobject.readOne(f.read()) + + +def maybe_add_photo(card, photo_path): + if hasattr(card, 'photo'): + print('{} has photo'.format(card.fn.value)) + return False + photo = card.add('photo') + photo.params = { + 'ENCODING': ['b'], + 'TYPE': ['JPEG'], + } + + with open(photo_path, mode='rb') as f: + photo.value = f.read() + + return True + + +def write_card_to_path(card, card_path): + with open(card_path, mode='w') as f: + f.write(card.serialize()) + + +def generate_card_paths(): + base_path = 'build/contacts/contacts' + for card_path in Path(base_path).glob('*.vcf'): + if card_path.is_file(): + yield str(card_path) + + +def generate_cards(): + for card_path in generate_card_paths(): + yield open_card(card_path), card_path + + +def main(): + email_to_photo = read_email_photo_json() + for card, card_path in generate_cards(): + for email_elem in card.contents.get('email', []): + email = email_elem.value + photo_path = Path(build_photo_path(email)) + if not photo_path.exists() and email in email_to_photo: + download_url_to_path(email_to_photo[email], photo_path) + if photo_path.exists(): + if maybe_add_photo(card, photo_path): + write_card_to_path(card, card_path) + print('Added photo to', card.fn.value) + + +if __name__ == '__main__': + main() diff --git a/main.py b/google_photo_to_vcard/build_photo_json.py similarity index 96% rename from main.py rename to google_photo_to_vcard/build_photo_json.py index 398a047..f42f3b8 100644 --- a/main.py +++ b/google_photo_to_vcard/build_photo_json.py @@ -12,6 +12,8 @@ from oauth2client import client from oauth2client import tools from oauth2client.file import Storage +from google_photo_to_vcard.util import write_email_photo_json + try: import argparse flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() @@ -70,11 +72,6 @@ def get_all_results(api, request, key='connections'): logging.info('No next page :(') -def write_map_to_file(email_to_photo): - with open('email_to_photo.json', 'w') as f: - f.write(json.dumps(email_to_photo)) - - def get_credentials(): """Gets valid user credentials from storage. @@ -164,7 +161,7 @@ def main(): )) # pprint(email_to_photo) - write_map_to_file(email_to_photo) + write_email_photo_json(email_to_photo) print('All done!') diff --git a/google_photo_to_vcard/download_photos.py b/google_photo_to_vcard/download_photos.py new file mode 100644 index 0000000..6fe5530 --- /dev/null +++ b/google_photo_to_vcard/download_photos.py @@ -0,0 +1,23 @@ +import urllib.request as request +from pathlib import Path +from urllib.error import HTTPError + +from google_photo_to_vcard.util import build_photo_path +from google_photo_to_vcard.util import download_url_to_path +from google_photo_to_vcard.util import read_email_photo_json + + +def main(): + email_to_photo = read_email_photo_json() + for email, photo_url in email_to_photo.items(): + print(email, photo_url) + photo_path = Path(build_photo_path(email)) + if photo_path.exists(): + print('Photo already downloaded') + continue + else: + download_url_to_path(photo_url, photo_path) + + +if __name__ == '__main__': + main() diff --git a/google_photo_to_vcard/util.py b/google_photo_to_vcard/util.py new file mode 100644 index 0000000..877bce8 --- /dev/null +++ b/google_photo_to_vcard/util.py @@ -0,0 +1,30 @@ +import json +import urllib.request as request +from urllib.error import HTTPError + + +EMAIL_TO_PHOTO_JSON_PATH = 'build/email_to_photo.json' + + +def write_email_photo_json(email_to_photo): + with open(EMAIL_TO_PHOTO_JSON_PATH, 'w') as f: + f.write(json.dumps(email_to_photo)) + + +def read_email_photo_json(): + with open(EMAIL_TO_PHOTO_JSON_PATH, 'r') as f: + return json.loads(f.read()) + + +def build_photo_path(email): + return 'build/photos/{}.jpeg'.format(email) + + +def download_url_to_path(url, path): + try: + with open(path, mode='xb') as f, request.urlopen(url) as r: + f.write(r.read()) + return path + except HTTPError as e: + print(e) + return None diff --git a/khard-wrapper b/khard-wrapper new file mode 100755 index 0000000..eaa50c1 --- /dev/null +++ b/khard-wrapper @@ -0,0 +1,5 @@ +#! /bin/bash +# khard-wrapper +# Wrapper around khard that executes it from within virtualenv using local config + +./virtualenv_run/bin/khard -c ./khard.conf $* diff --git a/khard.conf b/khard.conf new file mode 100644 index 0000000..7e6e9d8 --- /dev/null +++ b/khard.conf @@ -0,0 +1,42 @@ +# example configuration file for khard version >= 0.11.0 +# place it under $HOME/.config/khard/khard.conf + +[addressbooks] +[[contacts]] +path = ./build/contacts/contacts + +[general] +debug = no +default_action = list +editor = vim +merge_editor = vimdiff + +[contact table] +# display names by first or last name: first_name / last_name +display = first_name +# group by address book: yes / no +group_by_addressbook = no +# reverse table ordering: yes / no +reverse = no +# append nicknames to name column: yes / no +show_nicknames = no +# show uid table column: yes / no +show_uids = yes +# sort by first or last name: first_name / last_name +sort = last_name +# localize dates: yes / no +localize_dates = yes + +[vcard] +# extend contacts with your own private objects +# these objects are stored with a leading "X-" before the object name in the vcard files +# every object label may only contain letters, digits and the - character +# example: +# private_objects = Jabber, Skype, Twitter +private_objects = Jabber, Skype, Twitter +# preferred vcard version: 3.0 / 4.0 +preferred_version = 3.0 +# Look into source vcf files to speed up search queries: yes / no +search_in_source_files = no +# skip unparsable vcard files: yes / no +skip_unparsable = no diff --git a/requirements.txt b/requirements.txt index 7903b81..d769fd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ google-api-python-client khard +vdirsyncer diff --git a/vdirsyncer-wrapper b/vdirsyncer-wrapper new file mode 100755 index 0000000..10ff4d4 --- /dev/null +++ b/vdirsyncer-wrapper @@ -0,0 +1,5 @@ +#! /bin/bash +# vdirsyncer-wrapper +# Wrapper around vdirsyncer that executes it from within virtualenv using local config + +./virtualenv_run/bin/vdirsyncer -c ./vdirsyncer.conf $* diff --git a/vdirsyncer.conf.example b/vdirsyncer.conf.example new file mode 100644 index 0000000..527a0ab --- /dev/null +++ b/vdirsyncer.conf.example @@ -0,0 +1,70 @@ +# An example configuration for vdirsyncer. +# +# Move it to ~/.vdirsyncer/config or ~/.config/vdirsyncer/config and edit it. +# Run `vdirsyncer --help` for CLI usage. +# +# Optional parameters are commented out. +# This file doesn't document all available parameters, see +# http://vdirsyncer.pimutils.org/ for the rest of them. + +[general] +# A folder where vdirsyncer can store some metadata about each pair. +status_path = "~/.vdirsyncer/status/" + +# CARDDAV +[pair bob_contacts] +# A `[pair ]` block defines two storages `a` and `b` that should be +# synchronized. The definition of these storages follows in `[storage ]` +# blocks. This is similar to accounts in OfflineIMAP. +a = "bob_contacts_local" +b = "bob_contacts_remote" + +# Synchronize all collections that can be found. +# You need to run `vdirsyncer discover` if new calendars/addressbooks are added +# on the server. + +collections = ["from a", "from b"] + +# Synchronize the "display name" property into a local file (~/.contacts/displayname). +metadata = ["displayname"] + +# To resolve a conflict the following values are possible: +# `null` - abort when collisions occur (default) +# `"a wins"` - assume a's items to be more up-to-date +# `"b wins"` - assume b's items to be more up-to-date +#conflict_resolution = null + +[storage bob_contacts_local] +# A storage references actual data on a remote server or on the local disk. +# Similar to repositories in OfflineIMAP. +type = "filesystem" +path = "~/.contacts/" +fileext = ".vcf" + +[storage bob_contacts_remote] +type = "carddav" +url = "https://nextcloud.example.com/" +#username = +# The password can also be fetched from the system password storage, netrc or a +# custom command. See http://vdirsyncer.pimutils.org/en/stable/keyring.html +#password = + +# CALDAV +[pair bob_calendar] +a = "bob_calendar_local" +b = "bob_calendar_remote" +collections = ["from a", "from b"] + +# Calendars also have a color property +metadata = ["displayname", "color"] + +[storage bob_calendar_local] +type = "filesystem" +path = "~/.calendars/" +fileext = ".ics" + +[storage bob_calendar_remote] +type = "caldav" +url = "https://nextcloud.example.com/" +#username = +#password =