Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opds categories feed #492

Merged
merged 26 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0a3d293
Broke Server.404 with /catalogBLABLABLA/root.xml
veloman-yunkan Apr 15, 2021
5b272ac
Fixed handling of /catalogBLABLA/root.xml & alike
veloman-yunkan Apr 15, 2021
1e0ff1f
Fixed the double colon in OPDS date string
veloman-yunkan Apr 16, 2021
54b78ea
Moved gen_date_str() to tools/otherTools.cpp
veloman-yunkan Apr 15, 2021
3c3cf08
Serving /catalog/v2/root.xml
veloman-yunkan Apr 15, 2021
b259afa
Library::getBooksCategories()
veloman-yunkan Apr 16, 2021
2e53b51
Serving /catalog/v2/categories
veloman-yunkan Apr 16, 2021
a1520ce
Fixing the xenial build
veloman-yunkan Apr 17, 2021
feeb9f2
/catalog/v2/* XMLs are OPDS 1.2
veloman-yunkan Apr 18, 2021
92c2de8
Enter InternalServer::m_library_id
veloman-yunkan Apr 18, 2021
19b59fd
Serving /catalog/v2/entries
veloman-yunkan Apr 18, 2021
208dece
Reordered several statements
veloman-yunkan Apr 25, 2021
4aa3c79
Extracted get_search_filter()
veloman-yunkan Jun 8, 2021
70d42ae
A small simplification
veloman-yunkan Apr 25, 2021
b60e3ff
RequestContext::get_optional_param()
veloman-yunkan Apr 25, 2021
07252a1
/catalog/v2/entries is also a search endpoint
veloman-yunkan Apr 25, 2021
dfad1c3
/catalog/v2/searchdescription.xml
veloman-yunkan Apr 25, 2021
cdacc0c
/catalog/v2/entries going through OPDSDumper
veloman-yunkan May 24, 2021
9ca6bd0
/catalog/v2/categories goes through OPDSDumper too
veloman-yunkan May 24, 2021
f886c8c
Root url is normalized once in the constructor
veloman-yunkan May 24, 2021
f179799
Reused InternalServer::search_catalog()
veloman-yunkan May 24, 2021
fa42cbc
Pagination info in /catalog/v2/entries
veloman-yunkan May 24, 2021
312f2cb
Moved handle_catalog_v2*() methods into a new file
veloman-yunkan May 24, 2021
e799f2f
OPDSDumper::dumpOPDSFeed() works via mustache
veloman-yunkan May 27, 2021
dd60235
Fixed the self link in the output of /catalog/v2/entries
veloman-yunkan May 27, 2021
78083f1
Moved OPDS templates under static/templates
veloman-yunkan Jun 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion include/library.h
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,19 @@ class Library
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;

/**
* Get all langagues of the books in the library.
* Get all languagues of the books in the library.
*
* @return A list of languages.
*/
std::vector<std::string> getBooksLanguages() const;

/**
* Get all categories of the books in the library.
*
* @return A list of categories.
*/
std::vector<std::string> getBooksCategories() const;

/**
* Get all book creators of the books in the library.
*
Expand Down
48 changes: 19 additions & 29 deletions include/opds_dumper.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,38 +51,42 @@ class OPDSDumper
/**
* Dump the OPDS feed.
*
* @param id The id of the library.
* @param bookIds the ids of the books to include in the feed
* @param query the query used to obtain the list of book ids
* @return The OPDS feed.
*/
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds);
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const;

/**
* Set the id of the opds stream.
* Dump the OPDS feed.
*
* @param id the id to use.
* @param bookIds the ids of the books to include in the feed
* @param query the query used to obtain the list of book ids
* @return The OPDS feed.
*/
void setId(const std::string& id) { this->id = id;}
std::string dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const;

/**
* Set the title oft the opds stream.
* Dump the categories OPDS feed.
*
* @param title the title to use.
* @param categories list of category names
* @return The OPDS feed.
*/
void setTitle(const std::string& title) { this->title = title; }
std::string categoriesOPDSFeed(const std::vector<std::string>& categories) const;

/**
* Set the root location used when generating url.
* Set the id of the library.
*
* @param rootLocation the root location to use.
* @param id the id to use.
*/
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
void setLibraryId(const std::string& id) { this->libraryId = id;}

/**
* Set the search url.
* Set the root location used when generating url.
*
* @param searchUrl the search url to use.
* @param rootLocation the root location to use.
*/
void setSearchDescriptionUrl(const std::string& searchDescriptionUrl) { this->searchDescriptionUrl = searchDescriptionUrl; }
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }

/**
* Set some informations about the search results.
Expand All @@ -93,27 +97,13 @@ class OPDSDumper
*/
void setOpenSearchInfo(int totalResult, int startIndex, int count);

/**
* Set the library to dump.
*
* @param library The library to dump.
*/
void setLibrary(Library* library) { this->library = library; }

protected:
kiwix::Library* library;
std::string id;
std::string title;
std::string date;
std::string libraryId;
std::string rootLocation;
std::string searchDescriptionUrl;
int m_totalResults;
int m_startIndex;
int m_count;
bool m_isSearchResult = false;

private:
pugi::xml_node handleBook(Book book, pugi::xml_node root_node);
};
}

Expand Down
6 changes: 6 additions & 0 deletions include/tools/otherTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <vector>
#include <map>
#include <zim/zim.h>
#include <mustache.hpp>

namespace pugi {
class xml_node;
Expand All @@ -45,6 +46,11 @@ namespace kiwix

using MimeCounterType = std::map<const std::string, zim::entry_index_type>;
MimeCounterType parseMimetypeCounter(const std::string& counterData);

std::string gen_date_str();
std::string gen_uuid(const std::string& s);

std::string render_template(const std::string& template_str, kainjow::mustache::data data);
}

#endif
15 changes: 15 additions & 0 deletions src/library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,21 @@ std::vector<std::string> Library::getBooksLanguages() const
return booksLanguages;
}

std::vector<std::string> Library::getBooksCategories() const
{
std::set<std::string> categories;

for (const auto& pair: m_books) {
const auto& book = pair.second;
const auto& c = book.getCategory();
if ( !c.empty() ) {
categories.insert(c);
}
}

return std::vector<std::string>(categories.begin(), categories.end());
}

std::vector<std::string> Library::getBooksCreators() const
{
std::vector<std::string> booksCreators;
Expand Down
3 changes: 2 additions & 1 deletion src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ kiwix_sources = [
'server/etag.cpp',
'server/request_context.cpp',
'server/response.cpp',
'server/internalServer.cpp'
'server/internalServer.cpp',
'server/internalServer_catalog_v2.cpp'
]
kiwix_sources += lib_resources

Expand Down
191 changes: 92 additions & 99 deletions src/opds_dumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
#include "book.h"

#include "tools/otherTools.h"
#include <iomanip>

#include "kiwixlib-resources.h"
#include <mustache.hpp>

namespace kiwix
{

/* Constructor */
OPDSDumper::OPDSDumper(Library* library)
: library(library)
Expand All @@ -35,121 +38,111 @@ OPDSDumper::~OPDSDumper()
{
}

std::string gen_date_str()
{
auto now = time(0);
auto tm = localtime(&now);

std::stringstream is;
is << std::setw(2) << std::setfill('0')
<< 1900+tm->tm_year << "-"
<< std::setw(2) << std::setfill('0') << tm->tm_mon+1 << "-"
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
return is.str();
}

static std::string gen_date_from_yyyy_mm_dd(const std::string& date)
{
std::stringstream is;
is << date << "T00:00::00Z";
return is.str();
}

void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
{
m_totalResults = totalResults;
m_startIndex = startIndex,
m_count = count;
m_isSearchResult = true;
}

#define ADD_TEXT_ENTRY(node, child, value) (node).append_child((child)).append_child(pugi::node_pcdata).set_value((value).c_str())

pugi::xml_node OPDSDumper::handleBook(Book book, pugi::xml_node root_node) {
auto entry_node = root_node.append_child("entry");
ADD_TEXT_ENTRY(entry_node, "id", "urn:uuid:"+book.getId());
ADD_TEXT_ENTRY(entry_node, "title", book.getTitle());
ADD_TEXT_ENTRY(entry_node, "summary", book.getDescription());
ADD_TEXT_ENTRY(entry_node, "language", book.getLanguage());
ADD_TEXT_ENTRY(entry_node, "updated", gen_date_from_yyyy_mm_dd(book.getDate()));
ADD_TEXT_ENTRY(entry_node, "name", book.getName());
ADD_TEXT_ENTRY(entry_node, "flavour", book.getFlavour());
ADD_TEXT_ENTRY(entry_node, "category", book.getCategory());
ADD_TEXT_ENTRY(entry_node, "tags", book.getTags());
ADD_TEXT_ENTRY(entry_node, "articleCount", to_string(book.getArticleCount()));
ADD_TEXT_ENTRY(entry_node, "mediaCount", to_string(book.getMediaCount()));
ADD_TEXT_ENTRY(entry_node, "icon", rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath());

auto content_node = entry_node.append_child("link");
content_node.append_attribute("type") = "text/html";
content_node.append_attribute("href") = (rootLocation + "/" + book.getHumanReadableIdFromPath()).c_str();

auto author_node = entry_node.append_child("author");
ADD_TEXT_ENTRY(author_node, "name", book.getCreator());

auto publisher_node = entry_node.append_child("publisher");
ADD_TEXT_ENTRY(publisher_node, "name", book.getPublisher());

if (! book.getUrl().empty()) {
auto acquisition_link = entry_node.append_child("link");
acquisition_link.append_attribute("rel") = "http://opds-spec.org/acquisition/open-access";
acquisition_link.append_attribute("type") = "application/x-zim";
acquisition_link.append_attribute("href") = book.getUrl().c_str();
acquisition_link.append_attribute("length") = to_string(book.getSize()).c_str();
}

if (! book.getFaviconMimeType().empty() ) {
auto image_link = entry_node.append_child("link");
image_link.append_attribute("rel") = "http://opds-spec.org/image/thumbnail";
image_link.append_attribute("type") = book.getFaviconMimeType().c_str();
image_link.append_attribute("href") = (rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath()).c_str();
}
return entry_node;
}

string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds)
namespace
{
date = gen_date_str();
pugi::xml_document doc;

auto root_node = doc.append_child("feed");
root_node.append_attribute("xmlns") = "http://www.w3.org/2005/Atom";
root_node.append_attribute("xmlns:opds") = "http://opds-spec.org/2010/catalog";

ADD_TEXT_ENTRY(root_node, "id", id);

ADD_TEXT_ENTRY(root_node, "title", title);
ADD_TEXT_ENTRY(root_node, "updated", date);
typedef kainjow::mustache::data MustacheData;
typedef kainjow::mustache::list BookData;

if (m_isSearchResult) {
ADD_TEXT_ENTRY(root_node, "totalResults", to_string(m_totalResults));
ADD_TEXT_ENTRY(root_node, "startIndex", to_string(m_startIndex));
ADD_TEXT_ENTRY(root_node, "itemsPerPage", to_string(m_count));
BookData getBookData(const Library* library, const std::vector<std::string>& bookIds)
{
BookData bookData;
for ( const auto& bookId : bookIds ) {
const Book& book = library->getBookById(bookId);
const MustacheData bookUrl = book.getUrl().empty()
? MustacheData(false)
: MustacheData(book.getUrl());
bookData.push_back(kainjow::mustache::object{
{"id", "urn:uuid:"+book.getId()},
{"name", book.getName()},
{"title", book.getTitle()},
{"description", book.getDescription()},
{"language", book.getLanguage()},
{"content_id", book.getHumanReadableIdFromPath()},
{"updated", book.getDate() + "T00:00:00Z"},
{"category", book.getCategory()},
{"flavour", book.getFlavour()},
{"tags", book.getTags()},
{"article_count", to_string(book.getArticleCount())},
{"media_count", to_string(book.getMediaCount())},
{"author_name", book.getCreator()},
{"publisher_name", book.getPublisher()},
{"url", bookUrl},
{"size", to_string(book.getSize())},
});
}

auto self_link_node = root_node.append_child("link");
self_link_node.append_attribute("rel") = "self";
self_link_node.append_attribute("href") = "";
self_link_node.append_attribute("type") = "application/atom+xml";
return bookData;
}

} // unnamed namespace

if (!searchDescriptionUrl.empty() ) {
auto search_link = root_node.append_child("link");
search_link.append_attribute("rel") = "search";
search_link.append_attribute("type") = "application/opensearchdescription+xml";
search_link.append_attribute("href") = searchDescriptionUrl.c_str();
}
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
{
const auto bookData = getBookData(library, bookIds);
const kainjow::mustache::object template_data{
{"date", gen_date_str()},
{"root", rootLocation},
{"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)},
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
{"totalResults", to_string(m_totalResults)},
{"startIndex", to_string(m_startIndex)},
{"itemsPerPage", to_string(m_count)},
{"books", bookData }
};
Comment on lines +89 to +99
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this data is pretty closed from the ones used in dumpOPDSFeedV2.
Could be have "only one method" using two different templates ?
(Or maybe even better, one template with just the endpoint changing ? If possible)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's consider that after the new API is finalized. I don't want the old API to be a drag on the new API, and a premature attempt to share code between them will do just that.


return render_template(RESOURCE::templates::catalog_entries_xml, template_data);
}

string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const
{
const auto bookData = getBookData(library, bookIds);

const kainjow::mustache::object template_data{
{"date", gen_date_str()},
{"endpoint_root", rootLocation + "/catalog/v2"},
{"feed_id", gen_uuid(libraryId + "/entries?"+query)},
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
{"query", query.empty() ? "" : "?" + urlEncode(query)},
{"totalResults", to_string(m_totalResults)},
{"startIndex", to_string(m_startIndex)},
{"itemsPerPage", to_string(m_count)},
{"books", bookData }
};

return render_template(RESOURCE::templates::catalog_v2_entries_xml, template_data);
}

if (library) {
for (auto& bookId: bookIds) {
handleBook(library->getBookById(bookId), root_node);
}
std::string OPDSDumper::categoriesOPDSFeed(const std::vector<std::string>& categories) const
{
const auto now = gen_date_str();
kainjow::mustache::list categoryData;
for ( const auto& category : categories ) {
const auto urlencodedCategoryName = urlEncode(category);
categoryData.push_back(kainjow::mustache::object{
{"name", category},
{"urlencoded_name", urlencodedCategoryName},
{"updated", now},
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
});
}

return nodeToString(root_node);
return render_template(
RESOURCE::templates::catalog_v2_categories_xml,
kainjow::mustache::object{
{"date", now},
{"endpoint_root", rootLocation + "/catalog/v2"},
{"feed_id", gen_uuid(libraryId + "/categories")},
{"categories", categoryData }
}
);
}

}
Loading