From e799f2ff1edce70f22909ba97d584eefd2501f0d Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Thu, 27 May 2021 13:20:57 +0400 Subject: [PATCH] OPDSDumper::dumpOPDSFeed() works via mustache This changes the output of `/catalog/search` as follows: - Entire search query (rather than only the value of the `q` parameter) is put in the node. - Search performed with an empty query presents itself as "All zims". - The feed id remains stable for identical searches on the same library. --- include/opds_dumper.h | 40 +---------- src/opds_dumper.cpp | 125 +++++++++------------------------- src/server/internalServer.cpp | 10 +-- static/catalog_entries.xml | 38 +++++++++++ static/catalog_v2_entries.xml | 2 +- static/resources_list.txt | 1 + test/server.cpp | 32 +++++---- 7 files changed, 94 insertions(+), 154 deletions(-) create mode 100644 static/catalog_entries.xml diff --git a/include/opds_dumper.h b/include/opds_dumper.h index 8d8da0f6e..230d094a1 100644 --- a/include/opds_dumper.h +++ b/include/opds_dumper.h @@ -52,14 +52,16 @@ class OPDSDumper * Dump the OPDS feed. * * @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; /** * Dump the OPDS feed. * * @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 dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const; @@ -79,20 +81,6 @@ class OPDSDumper */ void setLibraryId(const std::string& id) { this->libraryId = id;} - /** - * Set the id of the opds stream. - * - * @param id the id to use. - */ - void setId(const std::string& id) { this->id = id;} - - /** - * Set the title oft the opds stream. - * - * @param title the title to use. - */ - void setTitle(const std::string& title) { this->title = title; } - /** * Set the root location used when generating url. * @@ -100,13 +88,6 @@ class OPDSDumper */ void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; } - /** - * Set the search url. - * - * @param searchUrl the search url to use. - */ - void setSearchDescriptionUrl(const std::string& searchDescriptionUrl) { this->searchDescriptionUrl = searchDescriptionUrl; } - /** * Set some informations about the search results. * @@ -116,28 +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 libraryId; - std::string title; - std::string date; 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); }; } diff --git a/src/opds_dumper.cpp b/src/opds_dumper.cpp index 8f98074b5..d4be8207c 100644 --- a/src/opds_dumper.cpp +++ b/src/opds_dumper.cpp @@ -27,6 +27,7 @@ namespace kiwix { + /* Constructor */ OPDSDumper::OPDSDumper(Library* library) : library(library) @@ -37,112 +38,22 @@ OPDSDumper::~OPDSDumper() { } -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); - - 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)); - } - - 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"; - - - 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(); - } - - if (library) { - for (auto& bookId: bookIds) { - handleBook(library->getBookById(bookId), root_node); - } - } - - return nodeToString(root_node); -} typedef kainjow::mustache::data MustacheData; +typedef kainjow::mustache::list BookData; -string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const +BookData getBookData(const Library* library, const std::vector<std::string>& bookIds) { - kainjow::mustache::list bookData; + BookData bookData; for ( const auto& bookId : bookIds ) { const Book& book = library->getBookById(bookId); const MustacheData bookUrl = book.getUrl().empty() @@ -168,6 +79,32 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const }); } + return bookData; +} + +} // unnamed namespace + +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 } + }; + + return render_template(RESOURCE::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"}, diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index cab52ebae..e395e0dc4 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -636,13 +636,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r } zim::Uuid uuid; - kiwix::OPDSDumper opdsDumper; + kiwix::OPDSDumper opdsDumper(mp_library); opdsDumper.setRootLocation(m_root); - opdsDumper.setSearchDescriptionUrl("catalog/searchdescription.xml"); - opdsDumper.setLibrary(mp_library); + opdsDumper.setLibraryId(m_library_id); std::vector<std::string> bookIdsToDump; if (url == "root.xml") { - opdsDumper.setTitle("All zims"); uuid = zim::Uuid::generate(host); bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true)); } else if (url == "search") { @@ -650,10 +648,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r uuid = zim::Uuid::generate(); } - opdsDumper.setId(kiwix::to_string(uuid)); auto response = ContentResponse::build( *this, - opdsDumper.dumpOPDSFeed(bookIdsToDump), + opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()), "application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8"); return std::move(response); } @@ -705,7 +702,6 @@ InternalServer::search_catalog(const RequestContext& request, const std::string q = filter.hasQuery() ? filter.getQuery() : "<Empty query>"; - opdsDumper.setTitle("Search result for " + q); std::vector<std::string> bookIdsToDump = mp_library->filter(filter); const auto totalResults = bookIdsToDump.size(); const size_t count = request.get_optional_param("count", 10UL); diff --git a/static/catalog_entries.xml b/static/catalog_entries.xml new file mode 100644 index 000000000..32754d5b0 --- /dev/null +++ b/static/catalog_entries.xml @@ -0,0 +1,38 @@ +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog"> + <id>{{feed_id}}</id> + <title>{{^filter}}All zims{{/filter}}{{#filter}}Filtered zims ({{filter}}){{/filter}} + {{date}} +{{#filter}} + {{totalResults}} + {{startIndex}} + {{itemsPerPage}} +{{/filter}} + + + {{#books}} + + {{id}} + {{title}} + {{description}} + {{language}} + {{updated}} + {{name}} + {{flavour}} + {{category}} + {{tags}} + {{article_count}} + {{media_count}} + /meta?name=favicon&content={{{content_id}}} + + + {{author_name}} + + + {{publisher_name}} + + {{#url}} + + {{/url}} + + {{/books}} + diff --git a/static/catalog_v2_entries.xml b/static/catalog_v2_entries.xml index f227c8f43..edc850f02 100644 --- a/static/catalog_v2_entries.xml +++ b/static/catalog_v2_entries.xml @@ -18,7 +18,7 @@ {{date}} {{#filter}} {{totalResults}} - {{startIndex}} + {{startIndex}} {{itemsPerPage}} {{/filter}} {{#books}} diff --git a/static/resources_list.txt b/static/resources_list.txt index 83dea44aa..dbeb5c5cd 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -37,6 +37,7 @@ templates/taskbar_part.html templates/external_blocker_part.html templates/captured_external.html opensearchdescription.xml +catalog_entries.xml catalog_v2_root.xml catalog_v2_entries.xml catalog_v2_categories.xml diff --git a/test/server.cpp b/test/server.cpp index fa366be95..1f7e4b506 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -611,7 +611,7 @@ std::string maskVariableOPDSFeedData(std::string s) " \n" \ " \n" + " href=\"/catalog/searchdescription.xml\" />\n" #define CHARLES_RAY_CATALOG_ENTRY \ " \n" \ @@ -694,6 +694,7 @@ TEST_F(LibraryServerTest, catalog_root_xml) " 12345678-90ab-cdef-1234-567890abcdef\n" " All zims\n" " YYYY-MM-DDThh:mm:ssZ\n" + "\n" CATALOG_LINK_TAGS CHARLES_RAY_CATALOG_ENTRY RAY_CHARLES_CATALOG_ENTRY @@ -727,7 +728,7 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for \"ray charles\"\n" + " Filtered zims (q="ray charles")\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -746,7 +747,7 @@ TEST_F(LibraryServerTest, catalog_search_by_words) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for ray charles\n" + " Filtered zims (q=ray charles)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" @@ -767,7 +768,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for description:ray description:charles\n" + " Filtered zims (q=description:ray description:charles)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -784,7 +785,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for title:\"ray charles\"\n" + " Filtered zims (q=title:"ray charles")\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" @@ -803,7 +804,7 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for ray -uncategorized\n" + " Filtered zims (q=ray -uncategorized)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -822,7 +823,7 @@ TEST_F(LibraryServerTest, catalog_search_by_tag) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for <Empty query>\n" + " Filtered zims (tag=_category:jazz)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" @@ -840,7 +841,7 @@ TEST_F(LibraryServerTest, catalog_search_by_category) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for <Empty query>\n" + " Filtered zims (category=jazz)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" @@ -859,7 +860,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for <Empty query>\n" + " Filtered zims (count=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" @@ -875,7 +876,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for <Empty query>\n" + " Filtered zims (count=1&start=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 1\n" @@ -891,12 +892,13 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Search result for <Empty query>\n" + " Filtered zims (count=10&start=100)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 100\n" " 0\n" CATALOG_LINK_TAGS + " \n" "\n" ); } @@ -1048,7 +1050,7 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range) " Filtered Entries (start=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" - " 1\n" + " 1\n" " 2\n" RAY_CHARLES_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY @@ -1064,7 +1066,7 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range) " Filtered Entries (count=2)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" - " 0\n" + " 0\n" " 2\n" CHARLES_RAY_CATALOG_ENTRY RAY_CHARLES_CATALOG_ENTRY @@ -1080,7 +1082,7 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range) " Filtered Entries (count=1&start=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" - " 1\n" + " 1\n" " 1\n" RAY_CHARLES_CATALOG_ENTRY "\n" @@ -1097,7 +1099,7 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms) " Filtered Entries (q="ray charles")\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" - " 0\n" + " 0\n" " 2\n" RAY_CHARLES_CATALOG_ENTRY CHARLES_RAY_CATALOG_ENTRY