271 lines
8.9 KiB
Python
271 lines
8.9 KiB
Python
"""
|
|
ComicVine API Information & Documentation:
|
|
https://comicvine.gamespot.com/api/
|
|
https://comicvine.gamespot.com/api/documentation
|
|
"""
|
|
import logging
|
|
from django.conf import settings
|
|
|
|
import requests
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ComicVineClient(object):
|
|
"""
|
|
Interacts with the ``search`` resource of the ComicVine API. Requires an
|
|
account on https://comicvine.gamespot.com/ in order to obtain an API key.
|
|
"""
|
|
|
|
# All API requests made by this client will be made to this URL.
|
|
API_URL = "https://www.comicvine.com/api/search/"
|
|
|
|
# A valid User-Agent header must be set in order for our API requests to
|
|
# be accepted, otherwise our request will be rejected with a
|
|
# **403 - Forbidden** error.
|
|
HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:7.0) "
|
|
"Gecko/20130825 Firefox/36.0"
|
|
}
|
|
|
|
# A set of valid resource types to return in results.
|
|
RESOURCE_TYPES = {
|
|
"character",
|
|
"issue",
|
|
"location",
|
|
"object",
|
|
"person",
|
|
"publisher",
|
|
"story_arc",
|
|
"team",
|
|
"volume",
|
|
}
|
|
|
|
def __init__(self, api_key, expire_after=300):
|
|
"""
|
|
Store the API key in a class variable, and install the requests cache,
|
|
configuring it using the ``expire_after`` parameter.
|
|
|
|
:param api_key: Your personal ComicVine API key.
|
|
:type api_key: str
|
|
:param expire_after: The number of seconds to retain an entry in cache.
|
|
:type expire_after: int or None
|
|
"""
|
|
|
|
self.api_key = api_key
|
|
|
|
def search(self, query, offset=0, limit=10, resources=None):
|
|
"""
|
|
Perform a search against the API, using the provided query term. If
|
|
required, a list of resource types to filter search results to can
|
|
be included.
|
|
|
|
Take the JSON contained in the response and provide it to the custom
|
|
``Response`` object's constructor. Return the ``Response`` object.
|
|
|
|
:param query: The search query with which to make the request.
|
|
:type query: str
|
|
:param offset: The index of the first record returned.
|
|
:type offset: int or None
|
|
:param limit: How many records to return **(max 10)**
|
|
:type limit: int or None
|
|
:param resources: A list of resources to include in the search results.
|
|
:type resources: list or None
|
|
:type use_cache: bool
|
|
|
|
:return: The response object containing the results of the search
|
|
query.
|
|
:rtype: comicvine_search.response.Response
|
|
"""
|
|
|
|
params = self._request_params(query, offset, limit, resources)
|
|
json_data = self._query_api(params)
|
|
|
|
return json_data
|
|
|
|
def _request_params(self, query, offset, limit, resources):
|
|
"""
|
|
Construct a dict containing the required key-value pairs of parameters
|
|
required in order to make the API request.
|
|
|
|
The documentation for the ``search`` resource can be found at
|
|
https://comicvine.gamespot.com/api/documentation#toc-0-30.
|
|
|
|
Regarding 'limit', as per the documentation:
|
|
|
|
The number of results to display per page. This value defaults to
|
|
10 and can not exceed this number.
|
|
|
|
:param query: The search query with which to make the request.
|
|
:type query: str
|
|
:param offset: The index of the first record returned.
|
|
:type offset: int
|
|
:param limit: How many records to return **(max 10)**
|
|
:type limit: int
|
|
:param resources: A list of resources to include in the search results.
|
|
:type resources: list or None
|
|
|
|
:return: A dictionary of request parameters.
|
|
:rtype: dict
|
|
"""
|
|
|
|
return {
|
|
"api_key": self.api_key,
|
|
"format": "json",
|
|
"limit": min(10, limit), # hard limit of 10
|
|
"offset": max(0, offset), # cannot provide negative offset
|
|
"query": query,
|
|
"resources": self._validate_resources(resources),
|
|
}
|
|
|
|
def _validate_resources(self, resources):
|
|
"""
|
|
Provided a list of resources, first convert it to a set and perform an
|
|
intersection with the set of valid resource types, ``RESOURCE_TYPES``.
|
|
Return a comma-separted string of the remaining valid resources, or
|
|
None if the set is empty.
|
|
|
|
:param resources: A list of resources to include in the search results.
|
|
:type resources: list or None
|
|
|
|
:return: A comma-separated string of valid resources.
|
|
:rtype: str or None
|
|
"""
|
|
|
|
if not resources:
|
|
return None
|
|
|
|
valid_resources = self.RESOURCE_TYPES & set(resources)
|
|
return ",".join(valid_resources) if valid_resources else None
|
|
|
|
def _query_api(self, params):
|
|
"""
|
|
Query the ComicVine API's ``search`` resource, providing the required
|
|
headers and parameters with the request. Optionally allow the caller
|
|
of the function to disable the request cache.
|
|
|
|
If an error occurs during the request, handle it accordingly. Upon
|
|
success, return the JSON from the response.
|
|
|
|
:param params: Parameters to include with the request.
|
|
:type params: dict
|
|
:param use_cache: Toggle the use of requests_cache.
|
|
:type use_cache: bool
|
|
|
|
:return: The JSON contained in the response.
|
|
:rtype: dict
|
|
"""
|
|
|
|
# Since we're performing the identical action regardless of whether
|
|
# or not the request cache is to be used, store the procedure in a
|
|
# local function to avoid repetition.
|
|
def __httpget():
|
|
response = requests.get(
|
|
self.API_URL, headers=self.HEADERS, params=params
|
|
)
|
|
|
|
if not response.ok:
|
|
self._handle_http_error(response)
|
|
|
|
return response.json()
|
|
|
|
return __httpget()
|
|
|
|
def _handle_http_error(self, response):
|
|
"""
|
|
Provided a ``requests.Response`` object, if the status code is
|
|
anything other than **200**, we will treat it as an error.
|
|
|
|
Using the response's status code, determine which type of exception to
|
|
raise. Construct an exception message from the response's status code
|
|
and reason properties before raising the exception.
|
|
|
|
:param response: The requests.Response object returned by the HTTP
|
|
request.
|
|
:type response: requests.Response
|
|
|
|
:raises ComicVineUnauthorizedException: if no API key provided.
|
|
:raises ComicVineForbiddenException: if no User-Agent header provided.
|
|
:raises ComicVineApiException: if an unidentified error occurs.
|
|
"""
|
|
|
|
exception = {
|
|
401: Exception,
|
|
403: Exception,
|
|
}.get(response.status_code, Exception)
|
|
message = f"{response.status_code} {response.reason}"
|
|
|
|
raise exception(message)
|
|
|
|
|
|
def lookup_comic_from_comicvine(title: str) -> dict:
|
|
original_title = title
|
|
|
|
issue_number = None
|
|
volume_nubmer = None
|
|
resource_type = "issue"
|
|
if "Issue " in title:
|
|
resource_type = "issue"
|
|
issue_number = title.split("Issue ")[1]
|
|
volume_number = None
|
|
if "Volume " in title:
|
|
resource_type = "volume"
|
|
volume_number = title.split("Volume ")[1]
|
|
|
|
api_key = getattr(settings, "COMICVINE_API_KEY", "")
|
|
if not api_key:
|
|
logger.warning("No ComicVine API key configured, not looking anything up")
|
|
return {}
|
|
|
|
client = ComicVineClient(
|
|
api_key=getattr(settings, "COMICVINE_API_KEY", None)
|
|
)
|
|
|
|
raw_results = client.search(title).get("results")
|
|
results = [
|
|
r
|
|
for r in raw_results
|
|
if r.get("resource_type") == resource_type
|
|
]
|
|
if not results:
|
|
logger.warning("No comic found on ComicVine")
|
|
return {}
|
|
|
|
found_result = None
|
|
for result in results:
|
|
if result.get("issue_number") == str(issue_number):
|
|
found_result = result
|
|
break
|
|
if result.get("volume_number") == str(volume_number):
|
|
found_result = result
|
|
break
|
|
|
|
if not found_result:
|
|
found_result = results[0]
|
|
|
|
logger.info("ComicVine results", extra={"results": results})
|
|
|
|
if not found_result:
|
|
logger.warning("No matches found on ComicVine")
|
|
return {}
|
|
|
|
title = found_result.get("name")
|
|
|
|
if found_result.get("volume"):
|
|
title = found_result.get("volume").get("name")
|
|
|
|
data_dict = {
|
|
"title": title,
|
|
"original_title": original_title,
|
|
"issue_number": found_result.get("issue_number"),
|
|
"volume_number": found_result.get("volume_number"),
|
|
"cover_url": found_result.get("image").get("original_url"),
|
|
"comicvine_id": found_result.get("id"),
|
|
"comicvine_data": found_result,
|
|
"summary": found_result.get("description"),
|
|
"publish_date": found_result.get("cover_date"),
|
|
"first_publish_year": found_result.get("cover_date", "")[:4]
|
|
}
|
|
|
|
return data_dict
|