""" interface with whatever connectors the app has """ from datetime import datetime import importlib import logging import re from urllib.parse import urlparse from django.dispatch import receiver from django.db.models import signals from requests import HTTPError from bookwyrm import book_search, models from bookwyrm.settings import SEARCH_TIMEOUT from bookwyrm.tasks import app logger = logging.getLogger(__name__) class ConnectorException(HTTPError): """when the connector can't do what was asked""" def search(query, min_confidence=0.1, return_first=False): """find books based on arbitary keywords""" if not query: return [] results = [] # Have we got a ISBN ? isbn = re.sub(r"[\W_]", "", query) maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 start_time = datetime.now() for connector in get_connectors(): result_set = None if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "": # Search on ISBN try: result_set = connector.isbn_search(isbn) except Exception as err: # pylint: disable=broad-except logger.exception(err) # if this fails, we can still try regular search # if no isbn search results, we fallback to generic search if not result_set: try: result_set = connector.search(query, min_confidence=min_confidence) except Exception as err: # pylint: disable=broad-except # we don't want *any* error to crash the whole search page logger.exception(err) continue if return_first and result_set: # if we found anything, return it return result_set[0] if result_set: results.append( { "connector": connector, "results": result_set, } ) if (datetime.now() - start_time).seconds >= SEARCH_TIMEOUT: break if return_first: return None return results def first_search_result(query, min_confidence=0.1): """search until you find a result that fits""" # try local search first result = book_search.search(query, min_confidence=min_confidence, return_first=True) if result: return result # otherwise, try remote endpoints return search(query, min_confidence=min_confidence, return_first=True) or None def get_connectors(): """load all connectors""" for info in models.Connector.objects.filter(active=True).order_by("priority").all(): yield load_connector(info) def get_or_create_connector(remote_id): """get the connector related to the object's server""" url = urlparse(remote_id) identifier = url.netloc if not identifier: raise ValueError("Invalid remote id") try: connector_info = models.Connector.objects.get(identifier=identifier) except models.Connector.DoesNotExist: connector_info = models.Connector.objects.create( identifier=identifier, connector_file="bookwyrm_connector", base_url=f"https://{identifier}", books_url=f"https://{identifier}/book", covers_url=f"https://{identifier}/images/covers", search_url=f"https://{identifier}/search?q=", priority=2, ) return load_connector(connector_info) @app.task(queue="low_priority") def load_more_data(connector_id, book_id): """background the work of getting all 10,000 editions of LoTR""" connector_info = models.Connector.objects.get(id=connector_id) connector = load_connector(connector_info) book = models.Book.objects.select_subclasses().get(id=book_id) connector.expand_book_data(book) def load_connector(connector_info): """instantiate the connector class""" connector = importlib.import_module( f"bookwyrm.connectors.{connector_info.connector_file}" ) return connector.Connector(connector_info.identifier) @receiver(signals.post_save, sender="bookwyrm.FederatedServer") # pylint: disable=unused-argument def create_connector(sender, instance, created, *args, **kwargs): """create a connector to an external bookwyrm server""" if instance.application_type == "bookwyrm": get_or_create_connector(f"https://{instance.server_name}")