diff --git a/include/opds_dumper.h b/include/opds_dumper.h index 69c74e753..3c4ce4227 100644 --- a/include/opds_dumper.h +++ b/include/opds_dumper.h @@ -59,9 +59,18 @@ class OPDSDumper * * @param bookIds the ids of the books to include in the feed * @param query the query used to obtain the list of book ids + * @param partial whether the feed should include partial or complete entries * @return The OPDS feed. */ - std::string dumpOPDSFeedV2(const std::vector& bookIds, const std::string& query) const; + std::string dumpOPDSFeedV2(const std::vector& bookIds, const std::string& query, bool partial) const; + + /** + * Dump the OPDS complete entry document. + * + * @param bookId the id of the book + * @return The OPDS complete entry document. + */ + std::string dumpOPDSCompleteEntry(const std::string& bookId) const; /** * Dump the categories OPDS feed. diff --git a/src/opds_dumper.cpp b/src/opds_dumper.cpp index bfbe61c84..6897b3142 100644 --- a/src/opds_dumper.cpp +++ b/src/opds_dumper.cpp @@ -51,7 +51,7 @@ namespace { typedef kainjow::mustache::data MustacheData; -typedef kainjow::mustache::list BookData; +typedef kainjow::mustache::list BooksData; typedef kainjow::mustache::list IllustrationInfo; IllustrationInfo getBookIllustrationInfo(const Book& book) @@ -69,16 +69,13 @@ IllustrationInfo getBookIllustrationInfo(const Book& book) return illustrations; } -BookData getBookData(const Library* library, const std::vector& bookIds) +kainjow::mustache::object getSingleBookData(const Book& book) { - 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()}, + return kainjow::mustache::object{ + {"id", book.getId()}, {"name", book.getName()}, {"title", book.getTitle()}, {"description", book.getDescription()}, @@ -95,10 +92,29 @@ BookData getBookData(const Library* library, const std::vector& boo {"url", bookUrl}, {"size", to_string(book.getSize())}, {"icons", getBookIllustrationInfo(book)}, + }; +} + +std::string getSingleBookEntryXML(const Book& book, bool withXMLHeader, const std::string& endpointRoot, bool partial) +{ + auto data = getSingleBookData(book); + data["with_xml_header"] = MustacheData(withXMLHeader); + data["dump_partial_entries"] = MustacheData(partial); + data["endpoint_root"] = endpointRoot; + return render_template(RESOURCE::templates::catalog_v2_entry_xml, data); +} + +BooksData getBooksData(const Library* library, const std::vector& bookIds, const std::string& endpointRoot, bool partial) +{ + BooksData booksData; + for ( const auto& bookId : bookIds ) { + const Book& book = library->getBookById(bookId); + booksData.push_back(kainjow::mustache::object{ + {"entry", getSingleBookEntryXML(book, false, endpointRoot, partial)} }); } - return bookData; + return booksData; } std::string getLanguageSelfName(const std::string& lang) { @@ -114,7 +130,7 @@ std::string getLanguageSelfName(const std::string& lang) { string OPDSDumper::dumpOPDSFeed(const std::vector& bookIds, const std::string& query) const { - const auto bookData = getBookData(library, bookIds); + const auto booksData = getBooksData(library, bookIds, "", false); const kainjow::mustache::object template_data{ {"date", gen_date_str()}, {"root", rootLocation}, @@ -123,31 +139,39 @@ string OPDSDumper::dumpOPDSFeed(const std::vector& bookIds, const s {"totalResults", to_string(m_totalResults)}, {"startIndex", to_string(m_startIndex)}, {"itemsPerPage", to_string(m_count)}, - {"books", bookData } + {"books", booksData } }; return render_template(RESOURCE::templates::catalog_entries_xml, template_data); } -string OPDSDumper::dumpOPDSFeedV2(const std::vector& bookIds, const std::string& query) const +string OPDSDumper::dumpOPDSFeedV2(const std::vector& bookIds, const std::string& query, bool partial) const { - const auto bookData = getBookData(library, bookIds); + const auto endpointRoot = rootLocation + "/catalog/v2"; + const auto booksData = getBooksData(library, bookIds, endpointRoot, partial); + const char* const endpoint = partial ? "/partial_entries" : "/entries"; const kainjow::mustache::object template_data{ {"date", gen_date_str()}, - {"endpoint_root", rootLocation + "/catalog/v2"}, - {"feed_id", gen_uuid(libraryId + "/entries?"+query)}, + {"endpoint_root", endpointRoot}, + {"feed_id", gen_uuid(libraryId + endpoint + "?" + 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 } + {"books", booksData }, + {"dump_partial_entries", MustacheData(partial)} }; return render_template(RESOURCE::templates::catalog_v2_entries_xml, template_data); } +std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const +{ + return getSingleBookEntryXML(library->getBookById(bookId), true, "", false); +} + std::string OPDSDumper::categoriesOPDSFeed() const { const auto now = gen_date_str(); diff --git a/src/server/internalServer.h b/src/server/internalServer.h index f1c813a81..cfaceb9ec 100644 --- a/src/server/internalServer.h +++ b/src/server/internalServer.h @@ -75,7 +75,8 @@ class InternalServer { std::unique_ptr handle_catalog(const RequestContext& request); std::unique_ptr handle_catalog_v2(const RequestContext& request); std::unique_ptr handle_catalog_v2_root(const RequestContext& request); - std::unique_ptr handle_catalog_v2_entries(const RequestContext& request); + std::unique_ptr handle_catalog_v2_entries(const RequestContext& request, bool partial); + std::unique_ptr handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId); std::unique_ptr handle_catalog_v2_categories(const RequestContext& request); std::unique_ptr handle_catalog_v2_languages(const RequestContext& request); std::unique_ptr handle_meta(const RequestContext& request); diff --git a/src/server/internalServer_catalog_v2.cpp b/src/server/internalServer_catalog_v2.cpp index 9dc88b49c..7e49f76ea 100644 --- a/src/server/internalServer_catalog_v2.cpp +++ b/src/server/internalServer_catalog_v2.cpp @@ -55,8 +55,13 @@ std::unique_ptr InternalServer::handle_catalog_v2(const RequestContext kainjow::mustache::object({{"endpoint_root", endpoint_root}}), "application/opensearchdescription+xml" ); + } else if (url == "entry") { + const std::string entryId = request.get_url_part(3); + return handle_catalog_v2_complete_entry(request, entryId); } else if (url == "entries") { - return handle_catalog_v2_entries(request); + return handle_catalog_v2_entries(request, /*partial=*/false); + } else if (url == "partial_entries") { + return handle_catalog_v2_entries(request, /*partial=*/true); } else if (url == "categories") { return handle_catalog_v2_categories(request); } else if (url == "languages") { @@ -76,6 +81,7 @@ std::unique_ptr InternalServer::handle_catalog_v2_root(const RequestCo {"endpoint_root", m_root + "/catalog/v2"}, {"feed_id", gen_uuid(m_library_id)}, {"all_entries_feed_id", gen_uuid(m_library_id + "/entries")}, + {"partial_entries_feed_id", gen_uuid(m_library_id + "/partial_entries")}, {"category_list_feed_id", gen_uuid(m_library_id + "/categories")}, {"language_list_feed_id", gen_uuid(m_library_id + "/languages")} }, @@ -83,13 +89,13 @@ std::unique_ptr InternalServer::handle_catalog_v2_root(const RequestCo ); } -std::unique_ptr InternalServer::handle_catalog_v2_entries(const RequestContext& request) +std::unique_ptr InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial) { OPDSDumper opdsDumper(mp_library); opdsDumper.setRootLocation(m_root); opdsDumper.setLibraryId(m_library_id); const auto bookIds = search_catalog(request, opdsDumper); - const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query()); + const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial); return ContentResponse::build( *this, opdsFeed, @@ -97,6 +103,25 @@ std::unique_ptr InternalServer::handle_catalog_v2_entries(const Reques ); } +std::unique_ptr InternalServer::handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId) +{ + try { + mp_library->getBookById(entryId); + } catch (const std::out_of_range&) { + return Response::build_404(*this, request, "", ""); + } + + OPDSDumper opdsDumper(mp_library); + opdsDumper.setRootLocation(m_root); + opdsDumper.setLibraryId(m_library_id); + const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId); + return ContentResponse::build( + *this, + opdsFeed, + "application/atom+xml;type=entry;profile=opds-catalog" + ); +} + std::unique_ptr InternalServer::handle_catalog_v2_categories(const RequestContext& request) { OPDSDumper opdsDumper(mp_library); diff --git a/static/resources_list.txt b/static/resources_list.txt index 6e92fbaac..eb65b9dbe 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -45,6 +45,7 @@ templates/captured_external.html templates/catalog_entries.xml templates/catalog_v2_root.xml templates/catalog_v2_entries.xml +templates/catalog_v2_entry.xml templates/catalog_v2_categories.xml templates/catalog_v2_languages.xml opensearchdescription.xml diff --git a/static/templates/catalog_entries.xml b/static/templates/catalog_entries.xml index 0a6a05dde..40cbfccd7 100644 --- a/static/templates/catalog_entries.xml +++ b/static/templates/catalog_entries.xml @@ -9,34 +9,4 @@ {{/filter}} - {{#books}} - - {{id}} - {{title}} - {{description}} - {{language}} - {{updated}} - {{name}} - {{flavour}} - {{category}} - {{tags}} - {{article_count}} - {{media_count}} - {{#icons}} - - {{/icons}} - - - {{author_name}} - - - {{publisher_name}} - - {{#url}} - - {{/url}} - - {{/books}} - +{{#books}}{{{entry}}}{{/books}} diff --git a/static/templates/catalog_v2_entries.xml b/static/templates/catalog_v2_entries.xml index 098744d86..2df9523d8 100644 --- a/static/templates/catalog_v2_entries.xml +++ b/static/templates/catalog_v2_entries.xml @@ -5,7 +5,7 @@ {{feed_id}} {{startIndex}} {{itemsPerPage}} {{/filter}} - {{#books}} - - {{id}} - {{title}} - {{description}} - {{language}} - {{updated}} - {{name}} - {{flavour}} - {{category}} - {{tags}} - {{article_count}} - {{media_count}} - {{#icons}} - - {{/icons}} - - - {{author_name}} - - - {{publisher_name}} - - {{#url}} - - {{/url}} - - {{/books}} - +{{#books}}{{{entry}}}{{/books}} diff --git a/static/templates/catalog_v2_entry.xml b/static/templates/catalog_v2_entry.xml new file mode 100644 index 000000000..33abd56d6 --- /dev/null +++ b/static/templates/catalog_v2_entry.xml @@ -0,0 +1,34 @@ +{{#with_xml_header}} +{{/with_xml_header}} + urn:uuid:{{id}} + {{title}} + {{updated}} +{{#dump_partial_entries}} + +{{/dump_partial_entries}}{{^dump_partial_entries}} {{description}} + {{language}} + {{name}} + {{flavour}} + {{category}} + {{tags}} + {{article_count}} + {{media_count}} + {{#icons}} + + {{/icons}} + + + {{author_name}} + + + {{publisher_name}} + + {{#url}} + + {{/url}} +{{/dump_partial_entries}} + diff --git a/static/templates/catalog_v2_root.xml b/static/templates/catalog_v2_root.xml index 9aec1cbad..b10c8d836 100644 --- a/static/templates/catalog_v2_root.xml +++ b/static/templates/catalog_v2_root.xml @@ -23,6 +23,15 @@ {{all_entries_feed_id}} All entries from this catalog. + + All entries (partial) + + {{date}} + {{partial_entries_feed_id}} + All entries from this catalog in partial format. + List of categories \n" \ " urn:uuid:charlesray\n" \ " Charles, Ray\n" \ + " YYYY-MM-DDThh:mm:ssZ\n" \ " Wikipedia articles about Ray Charles\n" \ " fra\n" \ - " YYYY-MM-DDThh:mm:ssZ\n" \ " wikipedia_fr_ray_charles\n" \ " \n" \ " jazz\n" \ @@ -644,9 +644,9 @@ std::string maskVariableOPDSFeedData(std::string s) " \n" \ " urn:uuid:raycharles\n" \ " Ray Charles\n" \ + " YYYY-MM-DDThh:mm:ssZ\n" \ " Wikipedia articles about Ray Charles\n" \ " eng\n" \ - " YYYY-MM-DDThh:mm:ssZ\n" \ " wikipedia_en_ray_charles\n" \ " \n" \ " wikipedia\n" \ @@ -670,9 +670,9 @@ std::string maskVariableOPDSFeedData(std::string s) " \n" \ " urn:uuid:raycharles_uncategorized\n" \ " Ray (uncategorized) Charles\n" \ + " YYYY-MM-DDThh:mm:ssZ\n" \ " No category is assigned to this library entry.\n" \ " rus\n" \ - " YYYY-MM-DDThh:mm:ssZ\n" \ " wikipedia_ru_ray_charles\n" \ " \n" \ " \n" \ @@ -905,7 +905,6 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination) " 100\n" " 0\n" CATALOG_LINK_TAGS - " \n" "\n" ); } @@ -940,6 +939,15 @@ TEST_F(LibraryServerTest, catalog_v2_root) 12345678-90ab-cdef-1234-567890abcdef All entries from this catalog. + + All entries (partial) + + YYYY-MM-DDThh:mm:ssZ + 12345678-90ab-cdef-1234-567890abcdef + All entries from this catalog in partial format. + List of categories body), expected_output); } -#define CATALOG_V2_ENTRIES_PREAMBLE(q) \ +#define CATALOG_V2_ENTRIES_PREAMBLE0(x) \ "\n" \ "12345678-90ab-cdef-1234-567890abcdef\n" \ "\n" \ " \n" \ " \n" \ "\n" \ +#define CATALOG_V2_ENTRIES_PREAMBLE(q) \ + CATALOG_V2_ENTRIES_PREAMBLE0("entries" q) + +#define CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE(q) \ + CATALOG_V2_ENTRIES_PREAMBLE0("partial_entries" q) TEST_F(LibraryServerTest, catalog_v2_entries) { @@ -1239,4 +1252,54 @@ TEST_F(LibraryServerTest, suggestions_in_range) int currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 0); } -} \ No newline at end of file +} + +TEST_F(LibraryServerTest, catalog_v2_individual_entry_access) +{ + const auto r = zfs1_->GET("/catalog/v2/entry/raycharles"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(maskVariableOPDSFeedData(r->body), + "\n" + RAY_CHARLES_CATALOG_ENTRY + ); + + const auto r1 = zfs1_->GET("/catalog/v2/entry/non-existent-entry"); + EXPECT_EQ(r1->status, 404); +} + +TEST_F(LibraryServerTest, catalog_v2_partial_entries) +{ + const auto r = zfs1_->GET("/catalog/v2/partial_entries"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(maskVariableOPDSFeedData(r->body), + CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE("") + " All Entries\n" + " YYYY-MM-DDThh:mm:ssZ\n" + "\n" + " \n" + " urn:uuid:charlesray\n" + " Charles, Ray\n" + " YYYY-MM-DDThh:mm:ssZ\n" + " \n" + " \n" + " \n" + " urn:uuid:raycharles\n" + " Ray Charles\n" + " YYYY-MM-DDThh:mm:ssZ\n" + " \n" + " \n" + " \n" + " urn:uuid:raycharles_uncategorized\n" + " Ray (uncategorized) Charles\n" + " YYYY-MM-DDThh:mm:ssZ\n" + " \n" + " \n" + "\n" + ); +}