change to version 2
parent
40487de474
commit
9339c73b10
160
api/opds_json.py
160
api/opds_json.py
|
@ -30,8 +30,9 @@ FORMAT_TO_MIMETYPE = {'pdf':"application/pdf",
|
||||||
'text':"text/html"}
|
'text':"text/html"}
|
||||||
|
|
||||||
UNGLUEIT_URL= 'https://unglue.it'
|
UNGLUEIT_URL= 'https://unglue.it'
|
||||||
ACQUISITION = "application/vnd.opds.acquisition+json"
|
ACQUISITION = "application/opds+json"
|
||||||
FACET_RELATION = "opds:facet"
|
FACET_RELATION = "opds:facet"
|
||||||
|
JSONCONTEXT = "http://opds-spec.org/opds.jsonld"
|
||||||
|
|
||||||
def feeds():
|
def feeds():
|
||||||
for facet_path in facets.get_all_facets('Format'):
|
for facet_path in facets.get_all_facets('Format'):
|
||||||
|
@ -53,92 +54,106 @@ def isbn_node(isbn):
|
||||||
|
|
||||||
def work_node(work, facet=None):
|
def work_node(work, facet=None):
|
||||||
|
|
||||||
content={}
|
metadata = {"@type": "http://schema.org/EBook"}
|
||||||
|
links = []
|
||||||
|
images = []
|
||||||
|
acquires = []
|
||||||
|
content = {
|
||||||
|
"metadata": metadata,
|
||||||
|
"links": links,
|
||||||
|
"images": images,
|
||||||
|
"acquire": acquires
|
||||||
|
}
|
||||||
# title
|
# title
|
||||||
content["title"] = work.title
|
metadata["title"] = work.title
|
||||||
|
|
||||||
# id
|
# id
|
||||||
content.update(text_node('id', "{base}{url}".format(base=UNGLUEIT_URL,url=reverse('work_identifier',kwargs={'work_id':work.id}))))
|
links.append({
|
||||||
|
"rel": "self",
|
||||||
|
"href": "{base}{url}?work={id}".format(
|
||||||
|
base=UNGLUEIT_URL,
|
||||||
|
url=reverse('opdsjson_acqusition', args=['all']),
|
||||||
|
id=work.id
|
||||||
|
),
|
||||||
|
"type": "application/opds-publication+json"
|
||||||
|
})
|
||||||
|
|
||||||
updated = None
|
updated = None
|
||||||
|
|
||||||
# links for all ebooks
|
# links for all ebooks
|
||||||
ebooks = facet.filter_model("Ebook",work.ebooks()) if facet else work.ebooks()
|
ebooks = facet.filter_model("Ebook",work.ebooks()) if facet else work.ebooks()
|
||||||
versions = set()
|
versions = set()
|
||||||
content['_links'] = links = {}
|
|
||||||
|
|
||||||
for ebook in ebooks:
|
for ebook in ebooks:
|
||||||
if updated is None:
|
if updated is None:
|
||||||
# most recent ebook, first ebook in loop
|
# most recent ebook, first ebook in loop
|
||||||
updated = ebook.created.isoformat()
|
updated = ebook.created.isoformat()
|
||||||
content.update(text_node('updated', updated))
|
metadata['updated'] = updated
|
||||||
if not ebook.version_label in versions:
|
if not ebook.version_label in versions:
|
||||||
versions.add(ebook.version_label)
|
versions.add(ebook.version_label)
|
||||||
link_node_attrib = {}
|
|
||||||
ebookfiles = links.get("opds:acquisition:open-access",[])
|
|
||||||
ebookfiles.append(link_node_attrib)
|
|
||||||
# ebook.download_url is an absolute URL with the protocol, domain, and path baked in
|
# ebook.download_url is an absolute URL with the protocol, domain, and path baked in
|
||||||
link_node_attrib.update({"href":add_query_component(ebook.download_url, "feed=opds"),
|
acquire = {
|
||||||
"rights": str(ebook.rights)})
|
"rel": "opds:acquisition:open-access",
|
||||||
|
"href": add_query_component(ebook.download_url, "feed=opds"),
|
||||||
|
"rights": str(ebook.rights)
|
||||||
|
}
|
||||||
if ebook.is_direct():
|
if ebook.is_direct():
|
||||||
link_node_attrib["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "")
|
acquire["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "")
|
||||||
else:
|
else:
|
||||||
""" indirect acquisition, i.e. google books """
|
""" indirect acquisition, i.e. google books """
|
||||||
link_node_attrib["type"] = "text/html"
|
acquire["type"] = "text/html"
|
||||||
indirect_attrib = {}
|
acquire["indirectAcquisition"] = {
|
||||||
indirect = {"indirectAcquisition":indirect_attrib}
|
"type": FORMAT_TO_MIMETYPE.get(ebook.format)
|
||||||
indirect_attrib["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "")
|
}
|
||||||
link_node_attrib.update(indirect)
|
|
||||||
if ebook.version_label:
|
if ebook.version_label:
|
||||||
link_node_attrib.update({"version": ebook.version_label})
|
acquire["version"] = ebook.version_label
|
||||||
links["opds:acquisition:open-access"] = ebookfiles
|
|
||||||
|
acquires.append(acquire)
|
||||||
|
|
||||||
# get the cover -- assume jpg?
|
# get the cover -- assume jpg?
|
||||||
if work.cover_image_small():
|
if work.cover_image_small():
|
||||||
cover_node_attrib = {}
|
cover_node = {
|
||||||
cover_node = {"opds:image:thumbnail": cover_node_attrib}
|
"href": work.cover_image_small(),
|
||||||
cover_node_attrib.update({"href":work.cover_image_small(),
|
"type": "image/"+work.cover_filetype(),
|
||||||
"type":"image/"+work.cover_filetype(),
|
}
|
||||||
})
|
images.append(cover_node)
|
||||||
links.update(cover_node)
|
|
||||||
if work.cover_image_thumbnail():
|
if work.cover_image_thumbnail():
|
||||||
cover_node2_attrib = {}
|
cover_node2 = {
|
||||||
cover_node2 = {"opds:image": cover_node2_attrib}
|
"href": work.cover_image_thumbnail(),
|
||||||
cover_node2_attrib.update({"href":work.cover_image_thumbnail(),
|
"type": "image/"+work.cover_filetype(),
|
||||||
"type":"image/"+work.cover_filetype(),
|
}
|
||||||
})
|
images.append(cover_node2)
|
||||||
links.update(cover_node2)
|
|
||||||
|
|
||||||
|
|
||||||
# <dcterms:issued>2012</dcterms:issued>
|
# <dcterms:issued>2012</dcterms:issued>
|
||||||
content.update({"issued": work.publication_date})
|
metadata["issued"] = work.publication_date
|
||||||
|
|
||||||
# author
|
# author
|
||||||
# TO DO: include all authors?
|
# TO DO: include all authors?
|
||||||
content["contributor"] = {"name": work.author()}
|
metadata["author"] = work.author()
|
||||||
|
|
||||||
# publisher
|
# publisher
|
||||||
#<dcterms:publisher>Open Book Publishers</dcterms:publisher>
|
#<dcterms:publisher>Open Book Publishers</dcterms:publisher>
|
||||||
if len(work.publishers()):
|
if len(work.publishers()):
|
||||||
content["publishers"] = [{"publisher": publisher.name.name}
|
metadata["publishers"] = [{"publisher": publisher.name.name}
|
||||||
for publisher in work.publishers()]
|
for publisher in work.publishers()]
|
||||||
|
|
||||||
# language
|
# language
|
||||||
content["language"] = work.language
|
metadata["language"] = work.language
|
||||||
|
|
||||||
# description
|
# description
|
||||||
content["summary"] = work.description
|
metadata["summary"] = work.description
|
||||||
|
|
||||||
# identifiers
|
# identifiers
|
||||||
if work.identifiers.filter(type='isbn'):
|
if work.identifiers.filter(type='identifier'):
|
||||||
content['identifers'] = [isbn_node(isbn.value)
|
metadata['other_identifiers'] = [isbn_node(isbn.value)
|
||||||
for isbn in work.identifiers.filter(type='isbn')[0:9]] #10 should be more than enough
|
for isbn in work.identifiers.filter(type='isbn')[0:9]] #10 should be more than enough
|
||||||
|
|
||||||
|
|
||||||
# subject tags
|
# subject tags
|
||||||
subjects = [subject.name for subject in work.subjects.all()]
|
subjects = [subject.name for subject in work.subjects.all()]
|
||||||
if subjects:
|
if subjects:
|
||||||
content["category"] = subjects
|
metadata["subjects"] = subjects
|
||||||
|
|
||||||
# age level
|
# age level
|
||||||
# <category term="15-18" scheme="http://schema.org/typicalAgeRange" label="Teen - Grade 10-12, Age 15-18"/>
|
# <category term="15-18" scheme="http://schema.org/typicalAgeRange" label="Teen - Grade 10-12, Age 15-18"/>
|
||||||
|
@ -148,11 +163,11 @@ def work_node(work, facet=None):
|
||||||
age_level_node_attrib["scheme"] = 'http://schema.org/typicalAgeRange'
|
age_level_node_attrib["scheme"] = 'http://schema.org/typicalAgeRange'
|
||||||
age_level_node_attrib["term"] = work.age_level
|
age_level_node_attrib["term"] = work.age_level
|
||||||
age_level_node_attrib["label"] = work.get_age_level_display()
|
age_level_node_attrib["label"] = work.get_age_level_display()
|
||||||
content.update(age_level_node)
|
metadata.update(age_level_node)
|
||||||
|
|
||||||
|
|
||||||
# rating
|
# rating
|
||||||
content["Rating"] = {"ratingValue":"{:}".format(work.priority())}
|
metadata["rating"] = {"ratingValue":"{:}".format(work.priority())}
|
||||||
return content
|
return content
|
||||||
|
|
||||||
class Facet:
|
class Facet:
|
||||||
|
@ -188,6 +203,9 @@ def get_facet_facet(facet_path):
|
||||||
def opds_feed_for_work(work_id):
|
def opds_feed_for_work(work_id):
|
||||||
class single_work_facet:
|
class single_work_facet:
|
||||||
def __init__(self, work_id):
|
def __init__(self, work_id):
|
||||||
|
class NullFacet(facets.BaseFacet):
|
||||||
|
def get_other_groups(self):
|
||||||
|
return[]
|
||||||
try:
|
try:
|
||||||
works=models.Work.objects.filter(id=work_id)
|
works=models.Work.objects.filter(id=work_id)
|
||||||
except models.Work.DoesNotExist:
|
except models.Work.DoesNotExist:
|
||||||
|
@ -198,7 +216,7 @@ def opds_feed_for_work(work_id):
|
||||||
self.works=works
|
self.works=works
|
||||||
self.title='Unglue.it work #%s' % work_id
|
self.title='Unglue.it work #%s' % work_id
|
||||||
self.feed_path=''
|
self.feed_path=''
|
||||||
self.facet_object= facets.BaseFacet(None)
|
self.facet_object= NullFacet(None)
|
||||||
return opds_feed_for_works( single_work_facet(work_id) )
|
return opds_feed_for_works( single_work_facet(work_id) )
|
||||||
|
|
||||||
def opds_feed_for_works(the_facet, page=None, order_by='newest'):
|
def opds_feed_for_works(the_facet, page=None, order_by='newest'):
|
||||||
|
@ -206,49 +224,34 @@ def opds_feed_for_works(the_facet, page=None, order_by='newest'):
|
||||||
works = the_facet.works
|
works = the_facet.works
|
||||||
feed_path = the_facet.feed_path
|
feed_path = the_facet.feed_path
|
||||||
title = the_facet.title
|
title = the_facet.title
|
||||||
|
metadata = {"title": title}
|
||||||
feed = {'_type': ACQUISITION}
|
links = []
|
||||||
|
feedlist = []
|
||||||
|
feed = {"@context": JSONCONTEXT, "metadata": metadata, "links": links, "publications": feedlist}
|
||||||
|
|
||||||
# add title
|
# add title
|
||||||
# TO DO: will need to calculate the number items and where in the feed we are
|
# TO DO: will need to calculate the number items and where in the feed we are
|
||||||
|
|
||||||
feed.update(text_node('title', title + ' - sorted by ' + order_by))
|
metadata['title'] = title + ' - sorted by ' + order_by
|
||||||
|
|
||||||
# id
|
|
||||||
|
|
||||||
feed.update(text_node('id', "{url}/api/opdsjson/{feed_path}/?order_by={order_by}".format(url=UNGLUEIT_URL,
|
|
||||||
feed_path=urlquote(feed_path), order_by=order_by)))
|
|
||||||
|
|
||||||
# updated
|
|
||||||
# TO DO: fix time zone?
|
|
||||||
# also use our wrapped datetime code
|
|
||||||
|
|
||||||
feed.update(text_node('updated',
|
|
||||||
pytz.utc.localize(datetime.datetime.utcnow()).isoformat()))
|
|
||||||
|
|
||||||
# author
|
|
||||||
|
|
||||||
author_node = {"author":{'name': 'unglue.it','uri': UNGLUEIT_URL}}
|
|
||||||
feed.update(author_node)
|
|
||||||
|
|
||||||
# links: start, self, next/prev (depending what's necessary -- to start with put all CC books)
|
# links: start, self, next/prev (depending what's necessary -- to start with put all CC books)
|
||||||
feed['_links'] = {}
|
|
||||||
# start link
|
|
||||||
append_navlink(feed, 'start', feed_path, None , order_by, title="First {}".format(books_per_page))
|
|
||||||
|
|
||||||
# next link
|
|
||||||
|
|
||||||
if not page:
|
if not page:
|
||||||
page =0
|
page = 0
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
page=int(page)
|
page = int(page)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
page=0
|
page=0
|
||||||
|
|
||||||
|
# self link
|
||||||
|
append_navlink(feed, 'self', feed_path, page , order_by, title="First {}".format(books_per_page))
|
||||||
|
|
||||||
|
# next link
|
||||||
try:
|
try:
|
||||||
works[books_per_page * page + books_per_page]
|
works[books_per_page * page + books_per_page]
|
||||||
append_navlink(feed, 'next', feed_path, page+1 , order_by, title="Next {}".format(books_per_page))
|
append_navlink(feed, 'next', feed_path, page+1 , order_by,
|
||||||
|
title="Next {}".format(books_per_page))
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -264,24 +267,21 @@ def opds_feed_for_works(the_facet, page=None, order_by='newest'):
|
||||||
works = islice(works, books_per_page * page, books_per_page * page + books_per_page)
|
works = islice(works, books_per_page * page, books_per_page * page + books_per_page)
|
||||||
if page > 0:
|
if page > 0:
|
||||||
append_navlink(feed, 'previous', feed_path, page-1, order_by, title="Previous {}".format(books_per_page))
|
append_navlink(feed, 'previous', feed_path, page-1, order_by, title="Previous {}".format(books_per_page))
|
||||||
feedlist = []
|
|
||||||
feed['publications'] = feedlist
|
|
||||||
for work in works:
|
for work in works:
|
||||||
node = work_node(work, facet=the_facet.facet_object)
|
node = work_node(work, facet=the_facet.facet_object)
|
||||||
feedlist.append(node)
|
feedlist.append(node)
|
||||||
return json.dumps(feed,indent=2, separators=(',', ': '), sort_keys=False)
|
return json.dumps(feed,indent=2, separators=(',', ': '), sort_keys=False)
|
||||||
|
|
||||||
def append_navlink(feed, rel, path, page, order_by, group=None, active=None , title=""):
|
def append_navlink(feed, rel, path, page, order_by, group=None, active=None , title=""):
|
||||||
if page==None:
|
link = {
|
||||||
return
|
"rel": rel,
|
||||||
link = {
|
"href": UNGLUEIT_URL + "/api/opdsjson/" + urlquote(path) + '/?order_by=' + order_by + ('&page=' + unicode(page) ),
|
||||||
"href": UNGLUEIT_URL + "/api/opdsjson/" + urlquote(path) + '/?order_by=' + order_by + ('&page=' + unicode(page) ),
|
"type": ACQUISITION,
|
||||||
"type": ACQUISITION,
|
"title": title,
|
||||||
"title": title,
|
}
|
||||||
}
|
|
||||||
if rel == FACET_RELATION:
|
if rel == FACET_RELATION:
|
||||||
if group:
|
if group:
|
||||||
link['facetGroup'] = group
|
link['facetGroup'] = group
|
||||||
if active:
|
if active:
|
||||||
link['activeFacet'] = 'true'
|
link['activeFacet'] = 'true'
|
||||||
feed['_links'][rel] = link
|
feed['links'].append(link)
|
|
@ -1,22 +1,17 @@
|
||||||
{
|
{
|
||||||
"_type": "application/vnd.opds.navigation+json",
|
"@context": "http://opds-spec.org/opds.jsonld",
|
||||||
"title": "Unglue.it Catalog",
|
"metadata":{"title": "Unglue.it Catalog"},
|
||||||
|
|
||||||
"_links": {
|
"links": [
|
||||||
"start": { "href": "https://unglue.it{% url 'opds' %}", "type": "application/vnd.opds.navigation+json" },
|
{"rel":"self", "href": "https://unglue.it{% url 'opds' %}", "type": "application/opds+json" },
|
||||||
"opds:featured": {"href": "{{ feed.feed_path }}/?order_by=popular", "type": "application/vnd.opds.acquisition+json" }
|
{"rel":"opds:featured", "href": "{{ feed.feed_path|urlencode }}/?order_by=popular", "type": "application/opds+json" }
|
||||||
},
|
],
|
||||||
|
|
||||||
"navigation": [{
|
"navigation": [
|
||||||
"title": {{ feed.title }} - Popular,
|
{"title": "{{ feed.title }} - Popular", "href": "{{ feed.feed_path|urlencode }}/?order_by=popular", "type": "application/opds+json"},
|
||||||
"_links": {"opds:featured": { "href": "{{ feed.feed_path }}/?order_by=popular", "profile": "http://opds-spec.org/navigation", "type": "application/vnd.opds.acquisition+json"}}
|
{"title": "{{ feed.title }} - New", "href": "{{ feed.feed_path|urlencode }}/?order_by=newest", "type": "application/opds+json" },
|
||||||
},{
|
{% for feed in feeds %}
|
||||||
"title": "{{ feed.title }} - New",
|
{"title": "{{ feed.title }}", "href": "{{ feed.feed_path|urlencode }}/", "type": "application/opds+json" },
|
||||||
"_links": {"opds:new": { "href": "{{ feed.feed_path }}/?order_by=newest", "profile": "http://opds-spec.org/navigation", "type": "application/vnd.opds.acquisition+json" }}
|
{% endfor %}
|
||||||
}
|
]
|
||||||
{% for feed in feeds %},{
|
|
||||||
"title": "{{ feed.title }}",
|
|
||||||
"_links": {"opds:new": { "href": "{{ feed.feed_path }}", "profile": "http://opds-spec.org/navigation", "type": "application/vnd.opds.acquisition+json" }}
|
|
||||||
}
|
|
||||||
{% endfor %}]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,7 +194,7 @@ class OPDSAcquisitionView(View):
|
||||||
if work:
|
if work:
|
||||||
if self.json:
|
if self.json:
|
||||||
return HttpResponse(opds_json.opds_feed_for_work(work),
|
return HttpResponse(opds_json.opds_feed_for_work(work),
|
||||||
content_type="application/json;profile=opds-catalog;kind=acquisition")
|
content_type="application/opds-publication+json")
|
||||||
else:
|
else:
|
||||||
return HttpResponse(opds.opds_feed_for_work(work),
|
return HttpResponse(opds.opds_feed_for_work(work),
|
||||||
content_type="application/atom+xml;profile=opds-catalog;kind=acquisition")
|
content_type="application/atom+xml;profile=opds-catalog;kind=acquisition")
|
||||||
|
@ -208,7 +208,7 @@ class OPDSAcquisitionView(View):
|
||||||
if self.json:
|
if self.json:
|
||||||
facet_class = opds_json.get_facet_class(facet)()
|
facet_class = opds_json.get_facet_class(facet)()
|
||||||
return HttpResponse(facet_class.feed(page,order_by),
|
return HttpResponse(facet_class.feed(page,order_by),
|
||||||
content_type="application/vnd.opds.acquisition+json")
|
content_type="application/opds+json")
|
||||||
else:
|
else:
|
||||||
facet_class = opds.get_facet_class(facet)()
|
facet_class = opds.get_facet_class(facet)()
|
||||||
return HttpResponse(facet_class.feed(page,order_by),
|
return HttpResponse(facet_class.feed(page,order_by),
|
||||||
|
|
Loading…
Reference in New Issue