ordigi/elodie/plugins/plugins.py
Jaisen Mathai 12c17c9cac Add a plugin to upload photos to Google Photos (#319)
Fixes #315.

This PR aims to address the [recent changes](https://www.blog.google/products/photos/simplifying-google-photos-and-google-drive/) in Google Photos + Google Drive where syncing between the two is no longer supported.

It works by uploading photos as part of the import process to add a copy of every photo in your library to Google Photos. Google Drive is not required for this plugin to work.

This plugin lets you have all your photos in Google Photos without relying on Google Drive. You can use another cloud storage service like iCloud or Dropbox or no cloud storage at all.

- [x] Add tests for `after()` plugin methods.
- [x] Add support for storage/async support.
- [x] Include plugins into code coverage.
- [x] Sweep code and clean up and add comments.
2019-07-12 01:44:57 -07:00

221 lines
7.6 KiB
Python

"""
Plugin object.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
from __future__ import print_function
from builtins import object
import io
from json import dumps, loads
from importlib import import_module
from os.path import dirname, dirname, isdir, isfile
from os import mkdir
from sys import exc_info
from traceback import format_exc
from elodie.compatability import _bytes
from elodie.config import load_config_for_plugin, load_plugin_config
from elodie.constants import application_directory
from elodie import log
class ElodiePluginError(Exception):
"""Exception which can be thrown by plugins to return failures.
"""
pass
class PluginBase(object):
"""Base class which all plugins should inherit from.
Defines stubs for all methods and exposes logging and database functionality
"""
__name__ = 'PluginBase'
def __init__(self):
# Loads the config for the plugin from config.ini
self.config_for_plugin = load_config_for_plugin(self.__name__)
self.db = PluginDb(self.__name__)
def after(self, file_path, destination_folder, final_file_path, metadata):
pass
def batch(self):
pass
def before(self, file_path, destination_folder):
pass
def log(self, msg):
# Writes an info log not shown unless being run in --debug mode.
log.info(dumps(
{self.__name__: msg}
))
def display(self, msg):
# Writes a log for all modes and will be displayed.
log.all(dumps(
{self.__name__: msg}
))
class PluginDb(object):
"""A database module which provides a simple key/value database.
The database is a JSON file located at %application_directory%/plugins/%pluginname.lower()%.json
"""
def __init__(self, plugin_name):
self.db_file = '{}/plugins/{}.json'.format(
application_directory,
plugin_name.lower()
)
# If the plugin db directory does not exist, create it
if(not isdir(dirname(self.db_file))):
mkdir(dirname(self.db_file))
# If the db file does not exist we initialize it
if(not isfile(self.db_file)):
with io.open(self.db_file, 'wb') as f:
f.write(_bytes(dumps({})))
def get(self, key):
with io.open(self.db_file, 'r') as f:
db = loads(f.read())
if(key not in db):
return None
return db[key]
def set(self, key, value):
with io.open(self.db_file, 'r') as f:
data = f.read()
db = loads(data)
db[key] = value
new_content = dumps(db, ensure_ascii=False).encode('utf8')
with io.open(self.db_file, 'wb') as f:
f.write(new_content)
def get_all(self):
with io.open(self.db_file, 'r') as f:
db = loads(f.read())
return db
def delete(self, key):
with io.open(self.db_file, 'r') as f:
db = loads(f.read())
# delete key without throwing an exception
db.pop(key, None)
new_content = dumps(db, ensure_ascii=False).encode('utf8')
with io.open(self.db_file, 'wb') as f:
f.write(new_content)
class Plugins(object):
"""Plugin object which manages all interaction with plugins.
Exposes methods to load plugins and execute their methods.
"""
def __init__(self):
self.plugins = []
self.classes = {}
self.loaded = False
def load(self):
"""Load plugins from config file.
"""
# If plugins have been loaded then return
if self.loaded == True:
return
plugin_list = load_plugin_config()
for plugin in plugin_list:
plugin_lower = plugin.lower()
try:
# We attempt to do the following.
# 1. Load the module of the plugin.
# 2. Instantiate an object of the plugin's class.
# 3. Add the plugin to the list of plugins.
#
# #3 should only happen if #2 doesn't throw an error
this_module = import_module('elodie.plugins.{}.{}'.format(plugin_lower, plugin_lower))
self.classes[plugin] = getattr(this_module, plugin)()
# We only append to self.plugins if we're able to load the class
self.plugins.append(plugin)
except:
log.error('An error occurred initiating plugin {}'.format(plugin))
log.error(format_exc())
self.loaded = True
def run_all_after(self, file_path, destination_folder, final_file_path, metadata):
"""Process `before` methods of each plugin that was loaded.
"""
self.load()
pass_status = True
for cls in self.classes:
this_method = getattr(self.classes[cls], 'after')
# We try to call the plugin's `before()` method.
# If the method explicitly raises an ElodiePluginError we'll fail the import
# by setting pass_status to False.
# If any other error occurs we log the message and proceed as usual.
# By default, plugins don't change behavior.
try:
this_method(file_path, destination_folder, final_file_path, metadata)
log.info('Called after() for {}'.format(cls))
except ElodiePluginError as err:
log.warn('Plugin {} raised an exception in run_all_before: {}'.format(cls, err))
log.error(format_exc())
log.error('false')
pass_status = False
except:
log.error(format_exc())
return pass_status
def run_batch(self):
self.load()
pass_status = True
for cls in self.classes:
this_method = getattr(self.classes[cls], 'batch')
# We try to call the plugin's `before()` method.
# If the method explicitly raises an ElodiePluginError we'll fail the import
# by setting pass_status to False.
# If any other error occurs we log the message and proceed as usual.
# By default, plugins don't change behavior.
try:
this_method()
log.info('Called batch() for {}'.format(cls))
except ElodiePluginError as err:
log.warn('Plugin {} raised an exception in run_batch: {}'.format(cls, err))
log.error(format_exc())
pass_status = False
except:
log.error(format_exc())
return pass_status
def run_all_before(self, file_path, destination_folder):
"""Process `before` methods of each plugin that was loaded.
"""
self.load()
pass_status = True
for cls in self.classes:
this_method = getattr(self.classes[cls], 'before')
# We try to call the plugin's `before()` method.
# If the method explicitly raises an ElodiePluginError we'll fail the import
# by setting pass_status to False.
# If any other error occurs we log the message and proceed as usual.
# By default, plugins don't change behavior.
try:
this_method(file_path, destination_folder)
log.info('Called before() for {}'.format(cls))
except ElodiePluginError as err:
log.warn('Plugin {} raised an exception in run_all_after: {}'.format(cls, err))
log.error(format_exc())
pass_status = False
except:
log.error(format_exc())
return pass_status