mirror of
https://github.com/kiwix/libkiwix.git
synced 2025-12-30 18:08:01 -05:00
Compare commits
56 Commits
kiwix_serv
...
update_htt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f19327023 | ||
|
|
b2b907f813 | ||
|
|
9479c0685d | ||
|
|
09a55d71d6 | ||
|
|
503eb5c4ce | ||
|
|
f714ff8d3e | ||
|
|
08e3d52957 | ||
|
|
30e4c549e4 | ||
|
|
b7b385d87b | ||
|
|
e46b0c07b5 | ||
|
|
cd9fb541fc | ||
|
|
3b942bb745 | ||
|
|
c0bda426b4 | ||
|
|
b3f7556096 | ||
|
|
4c657c082e | ||
|
|
e773a29f29 | ||
|
|
e15a0f4338 | ||
|
|
12d9b69806 | ||
|
|
027854e4f4 | ||
|
|
417e7471ac | ||
|
|
51ac1240f8 | ||
|
|
ea6413ff88 | ||
|
|
61209ea0d7 | ||
|
|
e9eaadde9e | ||
|
|
8a4080baba | ||
|
|
ba05999cba | ||
|
|
a4c3cad018 | ||
|
|
83e757a530 | ||
|
|
5e8f3a5505 | ||
|
|
fe93035a4c | ||
|
|
6e26c5aa75 | ||
|
|
452283cfe6 | ||
|
|
e5168d8b3d | ||
|
|
b8aee8a42c | ||
|
|
9addd82d2d | ||
|
|
e74e7f5623 | ||
|
|
a032d65eb8 | ||
|
|
19afe9442f | ||
|
|
a3ba7619df | ||
|
|
8b12434ff2 | ||
|
|
b4f7dfa5a2 | ||
|
|
ab3095745e | ||
|
|
45adda44b3 | ||
|
|
96cf7e78a5 | ||
|
|
dd118df612 | ||
|
|
8a4248e48e | ||
|
|
5f90f5ee2a | ||
|
|
64b55dbdc7 | ||
|
|
18871b4b15 | ||
|
|
b2027b397c | ||
|
|
49322f5961 | ||
|
|
0466b9759c | ||
|
|
20cdefcdb8 | ||
|
|
6ea40f57da | ||
|
|
a312d2218d | ||
|
|
15839df594 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
HOME: /home/runner
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-26"
|
||||
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-31"
|
||||
steps:
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
|
||||
9
.github/workflows/package.yml
vendored
9
.github/workflows/package.yml
vendored
@@ -10,7 +10,6 @@ jobs:
|
||||
distro:
|
||||
- ubuntu-impish
|
||||
- ubuntu-hirsute
|
||||
- ubuntu-groovy
|
||||
- ubuntu-focal
|
||||
- ubuntu-bionic
|
||||
steps:
|
||||
@@ -51,14 +50,6 @@ jobs:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-groovy
|
||||
if: matrix.distro == 'ubuntu-groovy'
|
||||
name: Build package for ubuntu-groovy
|
||||
id: build-ubuntu-groovy
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-focal
|
||||
if: matrix.distro == 'ubuntu-focal'
|
||||
name: Build package for ubuntu-focal
|
||||
|
||||
47
README.md
47
README.md
@@ -146,6 +146,53 @@ cp ninja ../bin
|
||||
cd ..
|
||||
```
|
||||
|
||||
Custom Index Page
|
||||
-----------------
|
||||
|
||||
to use custom welcome page mention `customIndexPage` argument in `kiwix::internalServer()` or use `kiwix::server->setCustomIndexTemplate()`.
|
||||
(note - while using custom html file please mention all external links as absolute path.)
|
||||
|
||||
to create a HTML template with custom JS you need to have a look at various OPDS based endpoints as mentioned [here](https://wiki.kiwix.org/wiki/OPDS) to load books.
|
||||
|
||||
To use JS provided by kiwix-serve you can use the following template to start with ->
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title><-- Custom Tittle --></title>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{root}}/skin/jquery-ui/jquery-ui.min.js"
|
||||
></script>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
|
||||
<script src="{{root}}/skin/iso6391To3.js"></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
- To get books listed using `index.js` add - `<div class="book__list"></div>` under body tag.
|
||||
- To get number of books listed add - `<h3 class="kiwixHomeBody__results"></h3>` under body tag.
|
||||
- To add language select box add - `<select id="languageFilter"></select>` under body tag.
|
||||
- To add language select box add - `<select id="categoryFilter"></select>` under body tag.
|
||||
- To add search box for books use following form -
|
||||
```
|
||||
<form id='kiwixSearchForm'>
|
||||
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
|
||||
<input type="submit" class="searchButton" value="Search"/>
|
||||
</form>
|
||||
```
|
||||
|
||||
|
||||
If you compile manually Libmicrohttpd, you might need to compile it
|
||||
without GNU TLS, a bug here will empeach further compilation
|
||||
otherwise.
|
||||
|
||||
@@ -26,6 +26,10 @@ namespace pugi {
|
||||
class xml_node;
|
||||
}
|
||||
|
||||
namespace zim {
|
||||
class Archive;
|
||||
}
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
@@ -43,6 +47,7 @@ class Book
|
||||
|
||||
bool update(const Book& other);
|
||||
void update(const Reader& reader);
|
||||
void update(const zim::Archive& archive);
|
||||
void updateFromXml(const pugi::xml_node& node, const std::string& baseDir);
|
||||
void updateFromOpds(const pugi::xml_node& node, const std::string& urlHost);
|
||||
std::string getHumanReadableIdFromPath() const;
|
||||
|
||||
@@ -154,6 +154,7 @@ class Library
|
||||
|
||||
public:
|
||||
typedef std::vector<std::string> BookIdCollection;
|
||||
typedef std::map<std::string, int> AttributeCounts;
|
||||
|
||||
public:
|
||||
Library();
|
||||
@@ -242,6 +243,13 @@ class Library
|
||||
*/
|
||||
std::vector<std::string> getBooksLanguages() const;
|
||||
|
||||
/**
|
||||
* Get all languagues of the books in the library with counts.
|
||||
*
|
||||
* @return A list of languages with the count of books in each language.
|
||||
*/
|
||||
AttributeCounts getBooksLanguagesWithCounts() const;
|
||||
|
||||
/**
|
||||
* Get all categories of the books in the library.
|
||||
*
|
||||
@@ -341,7 +349,12 @@ class Library
|
||||
friend class OPDSDumper;
|
||||
friend class libXMLDumper;
|
||||
|
||||
private: // types
|
||||
typedef const std::string& (Book::*BookStrPropMemFn)() const;
|
||||
|
||||
private: // functions
|
||||
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
|
||||
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
|
||||
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
||||
void updateBookDB(const Book& book);
|
||||
};
|
||||
|
||||
@@ -59,17 +59,32 @@ 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<std::string>& bookIds, const std::string& query) const;
|
||||
std::string dumpOPDSFeedV2(const std::vector<std::string>& 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.
|
||||
*
|
||||
* @param categories list of category names
|
||||
* @return The OPDS feed.
|
||||
*/
|
||||
std::string categoriesOPDSFeed(const std::vector<std::string>& categories) const;
|
||||
std::string categoriesOPDSFeed() const;
|
||||
|
||||
/**
|
||||
* Dump the languages OPDS feed.
|
||||
*
|
||||
* @return The OPDS feed.
|
||||
*/
|
||||
std::string languagesOPDSFeed() const;
|
||||
|
||||
/**
|
||||
* Set the id of the library.
|
||||
|
||||
@@ -292,16 +292,6 @@ class Reader
|
||||
*/
|
||||
string getScraper() const;
|
||||
|
||||
/**
|
||||
* Get the origId of the zim file.
|
||||
*
|
||||
* The origId is only used in the case of patch zim file and is the Id
|
||||
* of the original zim file.
|
||||
*
|
||||
* @return The origId of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getOrigId() const;
|
||||
|
||||
/**
|
||||
* Get the favicon of the zim file.
|
||||
*
|
||||
|
||||
@@ -52,6 +52,7 @@ class Result
|
||||
};
|
||||
|
||||
struct SearcherInternal;
|
||||
struct SuggestionInternal;
|
||||
/**
|
||||
* The Searcher class is reponsible to do different kind of search using the
|
||||
* fulltext index.
|
||||
@@ -85,12 +86,12 @@ class Searcher
|
||||
*
|
||||
* @param search The search query.
|
||||
* @param resultStart the start offset of the search results (used for pagination).
|
||||
* @param resultEnd the end offset of the search results (used for pagination).
|
||||
* @param maxResultCount Maximum results to get from start (used for pagination).
|
||||
* @param verbose print some info on stdout if true.
|
||||
*/
|
||||
void search(const std::string& search,
|
||||
unsigned int resultStart,
|
||||
unsigned int resultEnd,
|
||||
unsigned int maxResultCount,
|
||||
const bool verbose = false);
|
||||
|
||||
/**
|
||||
@@ -104,12 +105,12 @@ class Searcher
|
||||
* @param longitude The longitude of the center point.
|
||||
* @param distance The radius of the disc.
|
||||
* @param resultStart the start offset of the search results (used for pagination).
|
||||
* @param resultEnd the end offset of the search results (used for pagination).
|
||||
* @param maxResultCount Maximum number of results to get from start (used for pagination).
|
||||
* @param verbose print some info on stdout if true.
|
||||
*/
|
||||
void geo_search(float latitude, float longitude, float distance,
|
||||
unsigned int resultStart,
|
||||
unsigned int resultEnd,
|
||||
unsigned int maxResultCount,
|
||||
const bool verbose = false);
|
||||
|
||||
/**
|
||||
@@ -148,22 +149,23 @@ class Searcher
|
||||
zim::SearchResultSet getSearchResultSet();
|
||||
|
||||
unsigned int getResultStart() { return resultStart; }
|
||||
unsigned int getResultEnd() { return resultEnd; }
|
||||
unsigned int getMaxResultCount() { return maxResultCount; }
|
||||
|
||||
protected:
|
||||
std::string beautifyInteger(const unsigned int number);
|
||||
void closeIndex();
|
||||
void searchInIndex(string& search,
|
||||
const unsigned int resultStart,
|
||||
const unsigned int resultEnd,
|
||||
const unsigned int maxResultCount,
|
||||
const bool verbose = false);
|
||||
|
||||
std::vector<Reader*> readers;
|
||||
std::unique_ptr<SearcherInternal> internal;
|
||||
std::unique_ptr<SuggestionInternal> suggestionInternal;
|
||||
std::string searchPattern;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
unsigned int resultEnd;
|
||||
unsigned int maxResultCount;
|
||||
|
||||
private:
|
||||
void reset();
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace kiwix
|
||||
void setPort(int port) { m_port = port; }
|
||||
void setNbThreads(int threads) { m_nbThreads = threads; }
|
||||
void setVerbose(bool verbose) { m_verbose = verbose; }
|
||||
void setIndexTemplateString(const std::string& indexTemplateString) { m_indexTemplateString = indexTemplateString; }
|
||||
void setTaskbar(bool withTaskbar, bool withLibraryButton)
|
||||
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||
void setBlockExternalLinks(bool blockExternalLinks)
|
||||
@@ -65,6 +66,7 @@ namespace kiwix
|
||||
NameMapper* mp_nameMapper;
|
||||
std::string m_root = "";
|
||||
std::string m_addr = "";
|
||||
std::string m_indexTemplateString = "";
|
||||
int m_port = 80;
|
||||
int m_nbThreads = 1;
|
||||
bool m_verbose = false;
|
||||
|
||||
@@ -167,5 +167,32 @@ std::vector<std::string> split(const std::string& str, const std::string& delims
|
||||
* @throw std::out_of_range if iso2 code is not known.
|
||||
*/
|
||||
std::string converta2toa3(const std::string& a2code);
|
||||
|
||||
/** Extracts content from given file.
|
||||
*
|
||||
* This function provides content of a file provided it's path.
|
||||
*
|
||||
* @param path The absolute path provided in string format.
|
||||
* @return Content of corresponding file in string format.
|
||||
*/
|
||||
std::string getFileContent(const std::string& path);
|
||||
|
||||
/** checks if file exists.
|
||||
*
|
||||
* This function returns boolean stating if file exists or not.
|
||||
*
|
||||
* @param path The absolute path provided in string format.
|
||||
* @return Boolean representing if file exists or not.
|
||||
*/
|
||||
bool fileExists(const std::string& path);
|
||||
|
||||
/** provides mimetype from filename.
|
||||
*
|
||||
* This function provides mimetype from file-name.
|
||||
*
|
||||
* @param filename string containing filename.
|
||||
* @return mimetype from filename in string format.
|
||||
*/
|
||||
std::string getMimeTypeForFile(const std::string& filename);
|
||||
}
|
||||
#endif // KIWIX_TOOLS_H
|
||||
|
||||
43
src/book.cpp
43
src/book.cpp
@@ -27,6 +27,9 @@
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/pathTools.h"
|
||||
#include "tools/archiveTools.h"
|
||||
|
||||
#include <zim/archive.h>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
@@ -80,26 +83,28 @@ bool Book::update(const kiwix::Book& other)
|
||||
|
||||
void Book::update(const kiwix::Reader& reader)
|
||||
{
|
||||
m_path = reader.getZimFilePath();
|
||||
m_pathValid = true;
|
||||
m_id = reader.getId();
|
||||
m_title = reader.getTitle();
|
||||
m_description = reader.getDescription();
|
||||
m_language = reader.getLanguage();
|
||||
m_creator = reader.getCreator();
|
||||
m_publisher = reader.getPublisher();
|
||||
m_date = reader.getDate();
|
||||
m_name = reader.getName();
|
||||
m_flavour = reader.getFlavour();
|
||||
m_tags = reader.getTags();
|
||||
m_category = getCategoryFromTags();
|
||||
m_origId = reader.getOrigId();
|
||||
m_articleCount = reader.getArticleCount();
|
||||
m_mediaCount = reader.getMediaCount();
|
||||
m_size = static_cast<uint64_t>(reader.getFileSize()) << 10;
|
||||
m_pathValid = true;
|
||||
update(*reader.getZimArchive());
|
||||
}
|
||||
|
||||
reader.getFavicon(m_favicon, m_faviconMimeType);
|
||||
void Book::update(const zim::Archive& archive) {
|
||||
m_path = archive.getFilename();
|
||||
m_pathValid = true;
|
||||
m_id = getArchiveId(archive);
|
||||
m_title = getArchiveTitle(archive);
|
||||
m_description = getMetaDescription(archive);
|
||||
m_language = getMetaLanguage(archive);
|
||||
m_creator = getMetaCreator(archive);
|
||||
m_publisher = getMetaPublisher(archive);
|
||||
m_date = getMetaDate(archive);
|
||||
m_name = getMetaName(archive);
|
||||
m_flavour = getMetaFlavour(archive);
|
||||
m_tags = getMetaTags(archive);
|
||||
m_category = getCategoryFromTags();
|
||||
m_articleCount = archive.getArticleCount();
|
||||
m_mediaCount = getArchiveMediaCount(archive);
|
||||
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
|
||||
|
||||
getArchiveFavicon(archive, 48, m_favicon, m_faviconMimeType);
|
||||
}
|
||||
|
||||
#define ATTR(name) node.attribute(name).value()
|
||||
|
||||
@@ -208,23 +208,36 @@ bool Library::writeBookmarksToFile(const std::string& path) const
|
||||
return writeTextFile(path, dumper.dumpLibXMLBookmark());
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksLanguages() const
|
||||
Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) const
|
||||
{
|
||||
std::vector<std::string> booksLanguages;
|
||||
std::map<std::string, bool> booksLanguagesMap;
|
||||
AttributeCounts propValueCounts;
|
||||
|
||||
for (auto& pair: m_books) {
|
||||
auto& book = pair.second;
|
||||
auto& language = book.getLanguage();
|
||||
if (booksLanguagesMap.find(language) == booksLanguagesMap.end()) {
|
||||
if (book.getOrigId().empty()) {
|
||||
booksLanguagesMap[language] = true;
|
||||
booksLanguages.push_back(language);
|
||||
}
|
||||
for (const auto& pair: m_books) {
|
||||
const auto& book = pair.second;
|
||||
if (book.getOrigId().empty()) {
|
||||
propValueCounts[(book.*p)()] += 1;
|
||||
}
|
||||
}
|
||||
return propValueCounts;
|
||||
}
|
||||
|
||||
return booksLanguages;
|
||||
std::vector<std::string> Library::getBookPropValueSet(BookStrPropMemFn p) const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
for ( const auto& kv : getBookAttributeCounts(p) ) {
|
||||
result.push_back(kv.first);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksLanguages() const
|
||||
{
|
||||
return getBookPropValueSet(&Book::getLanguage);
|
||||
}
|
||||
|
||||
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
{
|
||||
return getBookAttributeCounts(&Book::getLanguage);
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksCategories() const
|
||||
@@ -244,40 +257,12 @@ std::vector<std::string> Library::getBooksCategories() const
|
||||
|
||||
std::vector<std::string> Library::getBooksCreators() const
|
||||
{
|
||||
std::vector<std::string> booksCreators;
|
||||
std::map<std::string, bool> booksCreatorsMap;
|
||||
|
||||
for (auto& pair: m_books) {
|
||||
auto& book = pair.second;
|
||||
auto& creator = book.getCreator();
|
||||
if (booksCreatorsMap.find(creator) == booksCreatorsMap.end()) {
|
||||
if (book.getOrigId().empty()) {
|
||||
booksCreatorsMap[creator] = true;
|
||||
booksCreators.push_back(creator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return booksCreators;
|
||||
return getBookPropValueSet(&Book::getCreator);
|
||||
}
|
||||
|
||||
std::vector<std::string> Library::getBooksPublishers() const
|
||||
{
|
||||
std::vector<std::string> booksPublishers;
|
||||
std::map<std::string, bool> booksPublishersMap;
|
||||
|
||||
for (auto& pair:m_books) {
|
||||
auto& book = pair.second;
|
||||
auto& publisher = book.getPublisher();
|
||||
if (booksPublishersMap.find(publisher) == booksPublishersMap.end()) {
|
||||
if (book.getOrigId().empty()) {
|
||||
booksPublishersMap[publisher] = true;
|
||||
booksPublishers.push_back(publisher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return booksPublishers;
|
||||
return getBookPropValueSet(&Book::getPublisher);
|
||||
}
|
||||
|
||||
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
|
||||
|
||||
@@ -215,8 +215,8 @@ bool Manager::readBookFromPath(const std::string& path, kiwix::Book* book)
|
||||
tmp_path = computeAbsolutePath(getCurrentDirectory(), path);
|
||||
}
|
||||
try {
|
||||
kiwix::Reader reader(tmp_path);
|
||||
book->update(reader);
|
||||
zim::Archive archive(tmp_path);
|
||||
book->update(archive);
|
||||
book->setPathValid(true);
|
||||
} catch (const std::exception& e) {
|
||||
book->setPathValid(false);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include "kiwixlib-resources.h"
|
||||
#include <mustache.hpp>
|
||||
#include <unicode/locid.h>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/otherTools.h"
|
||||
@@ -50,18 +51,31 @@ namespace
|
||||
{
|
||||
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
typedef kainjow::mustache::list BookData;
|
||||
typedef kainjow::mustache::list BooksData;
|
||||
typedef kainjow::mustache::list IllustrationInfo;
|
||||
|
||||
BookData getBookData(const Library* library, const std::vector<std::string>& bookIds)
|
||||
IllustrationInfo getBookIllustrationInfo(const Book& book)
|
||||
{
|
||||
kainjow::mustache::list illustrations;
|
||||
if ( book.isPathValid() ) {
|
||||
for ( auto illustration_size : zim::Archive(book.getPath()).getIllustrationSizes() ) {
|
||||
illustrations.push_back(kainjow::mustache::object{
|
||||
{"icon_width", to_string(illustration_size)},
|
||||
{"icon_height", to_string(illustration_size)},
|
||||
{"icon_scale", "1"},
|
||||
});
|
||||
}
|
||||
}
|
||||
return illustrations;
|
||||
}
|
||||
|
||||
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()},
|
||||
@@ -77,17 +91,46 @@ BookData getBookData(const Library* library, const std::vector<std::string>& boo
|
||||
{"publisher_name", book.getPublisher()},
|
||||
{"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<std::string>& 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) {
|
||||
const icu::Locale locale(lang.c_str());
|
||||
icu::UnicodeString ustring;
|
||||
locale.getDisplayLanguage(locale, ustring);
|
||||
std::string result;
|
||||
ustring.toUTF8String(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& 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},
|
||||
@@ -96,36 +139,44 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& 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<std::string>& bookIds, const std::string& query) const
|
||||
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& 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::categoriesOPDSFeed(const std::vector<std::string>& categories) const
|
||||
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();
|
||||
kainjow::mustache::list categoryData;
|
||||
for ( const auto& category : categories ) {
|
||||
for ( const auto& category : library->getBooksCategories() ) {
|
||||
const auto urlencodedCategoryName = urlEncode(category);
|
||||
categoryData.push_back(kainjow::mustache::object{
|
||||
{"name", category},
|
||||
@@ -146,4 +197,32 @@ std::string OPDSDumper::categoriesOPDSFeed(const std::vector<std::string>& categ
|
||||
);
|
||||
}
|
||||
|
||||
std::string OPDSDumper::languagesOPDSFeed() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list languageData;
|
||||
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
|
||||
const std::string languageCode = langAndBookCount.first;
|
||||
const int bookCount = langAndBookCount.second;
|
||||
const auto languageSelfName = getLanguageSelfName(languageCode);
|
||||
languageData.push_back(kainjow::mustache::object{
|
||||
{"lang_code", languageCode},
|
||||
{"lang_self_name", languageSelfName},
|
||||
{"book_count", to_string(bookCount)},
|
||||
{"updated", now},
|
||||
{"id", gen_uuid(libraryId + "/languages/" + languageCode)}
|
||||
});
|
||||
}
|
||||
|
||||
return render_template(
|
||||
RESOURCE::templates::catalog_v2_languages_xml,
|
||||
kainjow::mustache::object{
|
||||
{"date", now},
|
||||
{"endpoint_root", rootLocation + "/catalog/v2"},
|
||||
{"feed_id", gen_uuid(libraryId + "/languages")},
|
||||
{"languages", languageData }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
126
src/reader.cpp
126
src/reader.cpp
@@ -21,6 +21,7 @@
|
||||
#include <time.h>
|
||||
|
||||
#include <zim/search.h>
|
||||
#include <zim/suggestion.h>
|
||||
#include <zim/item.h>
|
||||
#include <zim/error.h>
|
||||
|
||||
@@ -29,44 +30,6 @@
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools/archiveTools.h"
|
||||
|
||||
inline char hi(char v)
|
||||
{
|
||||
char hex[] = "0123456789abcdef";
|
||||
return hex[(v >> 4) & 0xf];
|
||||
}
|
||||
|
||||
inline char lo(char v)
|
||||
{
|
||||
char hex[] = "0123456789abcdef";
|
||||
return hex[v & 0xf];
|
||||
}
|
||||
|
||||
std::string hexUUID(std::string in)
|
||||
{
|
||||
std::ostringstream out;
|
||||
for (unsigned n = 0; n < 4; ++n) {
|
||||
out << hi(in[n]) << lo(in[n]);
|
||||
}
|
||||
out << '-';
|
||||
for (unsigned n = 4; n < 6; ++n) {
|
||||
out << hi(in[n]) << lo(in[n]);
|
||||
}
|
||||
out << '-';
|
||||
for (unsigned n = 6; n < 8; ++n) {
|
||||
out << hi(in[n]) << lo(in[n]);
|
||||
}
|
||||
out << '-';
|
||||
for (unsigned n = 8; n < 10; ++n) {
|
||||
out << hi(in[n]) << lo(in[n]);
|
||||
}
|
||||
out << '-';
|
||||
for (unsigned n = 10; n < 16; ++n) {
|
||||
out << hi(in[n]) << lo(in[n]);
|
||||
}
|
||||
std::string op = out.str();
|
||||
return op;
|
||||
}
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
/* Constructor */
|
||||
@@ -119,12 +82,7 @@ zim::Archive* Reader::getZimArchive() const
|
||||
|
||||
MimeCounterType Reader::parseCounterMetadata() const
|
||||
{
|
||||
try {
|
||||
auto counterContent = zimArchive->getMetadata("Counter");
|
||||
return parseMimetypeCounter(counterContent);
|
||||
} catch (zim::EntryNotFound& e) {
|
||||
return {};
|
||||
}
|
||||
return kiwix::parseArchiveCounter(*zimArchive);
|
||||
}
|
||||
|
||||
/* Get the count of articles which can be indexed/displayed */
|
||||
@@ -146,19 +104,7 @@ unsigned int Reader::getArticleCount() const
|
||||
/* Get the count of medias content in the ZIM file */
|
||||
unsigned int Reader::getMediaCount() const
|
||||
{
|
||||
std::map<const std::string, unsigned int> counterMap
|
||||
= this->parseCounterMetadata();
|
||||
unsigned int counter = 0;
|
||||
|
||||
for (auto &pair:counterMap) {
|
||||
if (startsWith(pair.first, "image/") ||
|
||||
startsWith(pair.first, "video/") ||
|
||||
startsWith(pair.first, "audio/")) {
|
||||
counter += pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
return counter;
|
||||
return kiwix::getArchiveMediaCount(*zimArchive);
|
||||
}
|
||||
|
||||
/* Get the total of all items of a ZIM file, redirects included */
|
||||
@@ -170,9 +116,7 @@ unsigned int Reader::getGlobalCount() const
|
||||
/* Return the UID of the ZIM file */
|
||||
string Reader::getId() const
|
||||
{
|
||||
std::ostringstream s;
|
||||
s << zimArchive->getUuid();
|
||||
return s.str();
|
||||
return kiwix::getArchiveId(*zimArchive);
|
||||
}
|
||||
|
||||
Entry Reader::getRandomPage() const
|
||||
@@ -191,7 +135,7 @@ Entry Reader::getMainPage() const
|
||||
|
||||
bool Reader::getFavicon(string& content, string& mimeType) const
|
||||
{
|
||||
return kiwix::getArchiveFavicon(*zimArchive, content, mimeType);
|
||||
return kiwix::getArchiveFavicon(*zimArchive, 48, content, mimeType);
|
||||
}
|
||||
|
||||
string Reader::getZimFilePath() const
|
||||
@@ -281,7 +225,7 @@ string Reader::getRelation() const
|
||||
|
||||
string Reader::getFlavour() const
|
||||
{
|
||||
METADATA("Flavour")
|
||||
return kiwix::getMetaFlavour(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getSource() const
|
||||
@@ -295,31 +239,6 @@ string Reader::getScraper() const
|
||||
}
|
||||
#undef METADATA
|
||||
|
||||
string Reader::getOrigId() const
|
||||
{
|
||||
string value;
|
||||
this->getMetadata("startfileuid", value);
|
||||
if (value.empty()) {
|
||||
return "";
|
||||
}
|
||||
std::string id = value;
|
||||
std::string origID;
|
||||
std::string temp = "";
|
||||
unsigned int k = 0;
|
||||
char tempArray[16] = "";
|
||||
for (unsigned int i = 0; i < id.size(); i++) {
|
||||
if (id[i] == '\n') {
|
||||
tempArray[k] = atoi(temp.c_str());
|
||||
temp = "";
|
||||
k++;
|
||||
} else {
|
||||
temp += id[i];
|
||||
}
|
||||
}
|
||||
origID = hexUUID(tempArray);
|
||||
return origID;
|
||||
}
|
||||
|
||||
Entry Reader::getEntryFromPath(const std::string& path) const
|
||||
{
|
||||
try {
|
||||
@@ -459,35 +378,36 @@ bool Reader::searchSuggestionsSmart(const string& prefix,
|
||||
SuggestionsList_t& results)
|
||||
{
|
||||
std::vector<std::string> variants = this->getTitleVariants(prefix);
|
||||
bool retVal = false;
|
||||
|
||||
/* Try to search in the title using fulltext search database */
|
||||
auto suggestionSearcher = zim::SuggestionSearcher(*zimArchive);
|
||||
|
||||
auto suggestionSearcher = zim::Searcher(*zimArchive);
|
||||
zim::Query suggestionQuery;
|
||||
suggestionQuery.setQuery(prefix, true);
|
||||
auto suggestionSearch = suggestionSearcher.search(suggestionQuery);
|
||||
|
||||
if (suggestionSearch.getEstimatedMatches()) {
|
||||
if (zimArchive->hasTitleIndex()) {
|
||||
auto suggestionSearch = suggestionSearcher.suggest(prefix);
|
||||
const auto suggestions = suggestionSearch.getResults(0, suggestionsCount);
|
||||
for (auto current = suggestions.begin();
|
||||
current != suggestions.end();
|
||||
current++) {
|
||||
for (auto current : suggestions) {
|
||||
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
|
||||
current.getPath(), current.getSnippet());
|
||||
results.push_back(suggestion);
|
||||
}
|
||||
retVal = true;
|
||||
} else {
|
||||
// Check some of the variants of the prefix
|
||||
for (std::vector<std::string>::iterator variantsItr = variants.begin();
|
||||
variantsItr != variants.end();
|
||||
variantsItr++) {
|
||||
retVal = this->searchSuggestions(*variantsItr, suggestionsCount, results)
|
||||
|| retVal;
|
||||
auto suggestionSearch = suggestionSearcher.suggest(*variantsItr);
|
||||
for (auto current : suggestionSearch.getResults(0, suggestionsCount)) {
|
||||
if (results.size() >= suggestionsCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
|
||||
current.getPath(), current.getSnippet());
|
||||
results.push_back(suggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
return results.size() > 0;
|
||||
}
|
||||
|
||||
/* Get next suggestion */
|
||||
@@ -546,7 +466,7 @@ bool Reader::isCorrupted() const
|
||||
/* Return the file size, works also for splitted files */
|
||||
unsigned int Reader::getFileSize() const
|
||||
{
|
||||
return zimArchive->getFilesize() / 1024;
|
||||
return kiwix::getArchiveFileSize(*zimArchive);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
106
src/searcher.cpp
106
src/searcher.cpp
@@ -23,6 +23,7 @@
|
||||
#include "reader.h"
|
||||
|
||||
#include <zim/search.h>
|
||||
#include <zim/suggestion.h>
|
||||
|
||||
#include <mustache.hpp>
|
||||
#include <cmath>
|
||||
@@ -37,6 +38,7 @@ class _Result : public Result
|
||||
{
|
||||
public:
|
||||
_Result(zim::SearchResultSet::iterator iterator);
|
||||
_Result(SuggestionItem suggestionItem);
|
||||
virtual ~_Result(){};
|
||||
|
||||
virtual std::string get_url();
|
||||
@@ -50,6 +52,8 @@ class _Result : public Result
|
||||
|
||||
private:
|
||||
zim::SearchResultSet::iterator iterator;
|
||||
SuggestionItem suggestionItem;
|
||||
bool isSuggestion;
|
||||
};
|
||||
|
||||
struct SearcherInternal : zim::SearchResultSet {
|
||||
@@ -62,12 +66,20 @@ struct SearcherInternal : zim::SearchResultSet {
|
||||
zim::SearchResultSet::iterator current_iterator;
|
||||
};
|
||||
|
||||
struct SuggestionInternal : zim::SuggestionResultSet {
|
||||
explicit SuggestionInternal(const zim::SuggestionResultSet& srs)
|
||||
: zim::SuggestionResultSet(srs),
|
||||
currentIterator(srs.begin()) {}
|
||||
|
||||
zim::SuggestionResultSet::iterator currentIterator;
|
||||
};
|
||||
|
||||
/* Constructor */
|
||||
Searcher::Searcher()
|
||||
: searchPattern(""),
|
||||
estimatedResultCount(0),
|
||||
resultStart(0),
|
||||
resultEnd(0)
|
||||
maxResultCount(0)
|
||||
{
|
||||
loadICUExternalTables();
|
||||
}
|
||||
@@ -95,7 +107,7 @@ Reader* Searcher::get_reader(int readerIndex)
|
||||
/* Search strings in the database */
|
||||
void Searcher::search(const std::string& search,
|
||||
unsigned int resultStart,
|
||||
unsigned int resultEnd,
|
||||
unsigned int maxResultCount,
|
||||
const bool verbose)
|
||||
{
|
||||
this->reset();
|
||||
@@ -106,9 +118,9 @@ void Searcher::search(const std::string& search,
|
||||
|
||||
this->searchPattern = search;
|
||||
this->resultStart = resultStart;
|
||||
this->resultEnd = resultEnd;
|
||||
this->maxResultCount = maxResultCount;
|
||||
/* Try to find results */
|
||||
if (resultStart != resultEnd) {
|
||||
if (maxResultCount != 0) {
|
||||
/* Perform the search */
|
||||
string unaccentedSearch = removeAccents(search);
|
||||
std::vector<zim::Archive> archives;
|
||||
@@ -119,11 +131,11 @@ void Searcher::search(const std::string& search,
|
||||
}
|
||||
}
|
||||
zim::Searcher searcher(archives);
|
||||
searcher.setVerbose(verbose);
|
||||
zim::Query query;
|
||||
query.setQuery(unaccentedSearch, false);
|
||||
query.setVerbose(verbose);
|
||||
query.setQuery(unaccentedSearch);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, resultEnd)));
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
@@ -133,7 +145,7 @@ void Searcher::search(const std::string& search,
|
||||
|
||||
void Searcher::geo_search(float latitude, float longitude, float distance,
|
||||
unsigned int resultStart,
|
||||
unsigned int resultEnd,
|
||||
unsigned int maxResultCount,
|
||||
const bool verbose)
|
||||
{
|
||||
this->reset();
|
||||
@@ -147,10 +159,10 @@ void Searcher::geo_search(float latitude, float longitude, float distance,
|
||||
oss << "Articles located less than " << distance << " meters of " << latitude << ";" << longitude;
|
||||
this->searchPattern = oss.str();
|
||||
this->resultStart = resultStart;
|
||||
this->resultEnd = resultEnd;
|
||||
this->maxResultCount = maxResultCount;
|
||||
|
||||
/* Try to find results */
|
||||
if (resultStart == resultEnd) {
|
||||
if (maxResultCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,12 +172,12 @@ void Searcher::geo_search(float latitude, float longitude, float distance,
|
||||
archives.push_back(*(*current)->getZimArchive());
|
||||
}
|
||||
zim::Searcher searcher(archives);
|
||||
searcher.setVerbose(verbose);
|
||||
zim::Query query;
|
||||
query.setVerbose(verbose);
|
||||
query.setQuery("", false);
|
||||
query.setQuery("");
|
||||
query.setGeorange(latitude, longitude, distance);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, resultEnd)));
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
@@ -179,11 +191,21 @@ void Searcher::restart_search()
|
||||
|
||||
Result* Searcher::getNextResult()
|
||||
{
|
||||
if (internal.get() &&
|
||||
internal->current_iterator != internal->end()) {
|
||||
if (internal.get() && internal->current_iterator != internal->end()) {
|
||||
Result* result = new _Result(internal->current_iterator);
|
||||
internal->current_iterator++;
|
||||
return result;
|
||||
} else if (suggestionInternal.get() &&
|
||||
suggestionInternal->currentIterator != suggestionInternal->end()) {
|
||||
SuggestionItem item(
|
||||
suggestionInternal->currentIterator->getTitle(),
|
||||
normalize(suggestionInternal->currentIterator->getTitle()),
|
||||
suggestionInternal->currentIterator->getPath(),
|
||||
suggestionInternal->currentIterator->getSnippet()
|
||||
);
|
||||
Result* result = new _Result(item);
|
||||
suggestionInternal->currentIterator++;
|
||||
return result;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -206,20 +228,15 @@ void Searcher::suggestions(std::string& searchPattern, const bool verbose)
|
||||
|
||||
this->searchPattern = searchPattern;
|
||||
this->resultStart = 0;
|
||||
this->resultEnd = 10;
|
||||
this->maxResultCount = 10;
|
||||
string unaccentedSearch = removeAccents(searchPattern);
|
||||
|
||||
std::vector<zim::Archive> archives;
|
||||
for (auto current = this->readers.begin(); current != this->readers.end();
|
||||
current++) {
|
||||
archives.push_back(*(*current)->getZimArchive());
|
||||
}
|
||||
zim::Searcher searcher(archives);
|
||||
zim::Query query;
|
||||
query.setVerbose(verbose);
|
||||
query.setQuery(unaccentedSearch, true);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, resultEnd)));
|
||||
// Multizim suggestion is not supported as of now! taking only one archive
|
||||
zim::Archive archive = *(*this->readers.begin())->getZimArchive();
|
||||
zim::SuggestionSearcher searcher(archive);
|
||||
searcher.setVerbose(verbose);
|
||||
zim::SuggestionSearch search = searcher.suggest(searchPattern);
|
||||
suggestionInternal.reset(new SuggestionInternal(search.getResults(resultStart, maxResultCount)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
@@ -235,40 +252,69 @@ zim::SearchResultSet Searcher::getSearchResultSet()
|
||||
}
|
||||
|
||||
_Result::_Result(zim::SearchResultSet::iterator iterator)
|
||||
: iterator(iterator)
|
||||
{
|
||||
}
|
||||
: iterator(iterator),
|
||||
suggestionItem("", "", ""),
|
||||
isSuggestion(false)
|
||||
{}
|
||||
|
||||
_Result::_Result(SuggestionItem item)
|
||||
: iterator(),
|
||||
suggestionItem(item.getTitle(), item.getNormalizedTitle(), item.getPath(), item.getSnippet()),
|
||||
isSuggestion(true)
|
||||
{}
|
||||
|
||||
std::string _Result::get_url()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return suggestionItem.getPath();
|
||||
}
|
||||
return iterator.getPath();
|
||||
}
|
||||
std::string _Result::get_title()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return suggestionItem.getTitle();
|
||||
}
|
||||
return iterator.getTitle();
|
||||
}
|
||||
int _Result::get_score()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return 0;
|
||||
}
|
||||
return iterator.getScore();
|
||||
}
|
||||
std::string _Result::get_snippet()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return suggestionItem.getSnippet();
|
||||
}
|
||||
return iterator.getSnippet();
|
||||
}
|
||||
std::string _Result::get_content()
|
||||
{
|
||||
if (isSuggestion) return "";
|
||||
return iterator->getItem(true).getData();
|
||||
}
|
||||
int _Result::get_size()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return 0;
|
||||
}
|
||||
return iterator.getSize();
|
||||
}
|
||||
int _Result::get_wordCount()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return 0;
|
||||
}
|
||||
return iterator.getWordCount();
|
||||
}
|
||||
std::string _Result::get_zimId()
|
||||
{
|
||||
if (isSuggestion) {
|
||||
return "";
|
||||
}
|
||||
std::ostringstream s;
|
||||
s << iterator.getZimId();
|
||||
return s.str();
|
||||
|
||||
@@ -48,7 +48,8 @@ bool Server::start() {
|
||||
m_verbose,
|
||||
m_withTaskbar,
|
||||
m_withLibraryButton,
|
||||
m_blockExternalLinks));
|
||||
m_blockExternalLinks,
|
||||
m_indexTemplateString));
|
||||
return mp_server->start();
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ extern "C" {
|
||||
#include <zim/uuid.h>
|
||||
#include <zim/error.h>
|
||||
#include <zim/search.h>
|
||||
#include <zim/suggestion.h>
|
||||
#include <zim/entry.h>
|
||||
#include <zim/item.h>
|
||||
|
||||
@@ -94,6 +95,16 @@ inline std::string normalizeRootUrl(std::string rootUrl)
|
||||
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
|
||||
}
|
||||
|
||||
unsigned parseIllustration(const std::string& s)
|
||||
{
|
||||
int nw(0), nh(0), nEnd(0);
|
||||
long int w(-1), h(-1);
|
||||
if ( sscanf(s.c_str(), "Illustration_%n%ldx%n%ld@1%n)", &nw, &w, &nh, &h, &nEnd) == 2
|
||||
&& nEnd == (int)s.size() && !isspace(s[nw]) && !isspace(s[nh]) && w == h && w >= 0) {
|
||||
return w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // unnamed namespace
|
||||
|
||||
static IdNameMapper defaultNameMapper;
|
||||
@@ -117,7 +128,8 @@ InternalServer::InternalServer(Library* library,
|
||||
bool verbose,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks) :
|
||||
bool blockExternalLinks,
|
||||
std::string indexTemplateString) :
|
||||
m_addr(addr),
|
||||
m_port(port),
|
||||
m_root(normalizeRootUrl(root)),
|
||||
@@ -126,6 +138,7 @@ InternalServer::InternalServer(Library* library,
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString),
|
||||
mp_daemon(nullptr),
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
|
||||
@@ -325,7 +338,7 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
|
||||
|
||||
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8", true);
|
||||
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8", true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,17 +347,15 @@ std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& r
|
||||
|
||||
// TODO: retrieve searcher from caching mechanism
|
||||
SuggestionsList_t getSuggestions(const zim::Archive* const archive,
|
||||
const std::string& queryString, int suggestionCount)
|
||||
const std::string& queryString, int start, int suggestionCount)
|
||||
{
|
||||
SuggestionsList_t suggestions;
|
||||
auto searcher = zim::SuggestionSearcher(*archive);
|
||||
if (archive->hasTitleIndex()) {
|
||||
auto searcher = zim::Searcher(*archive);
|
||||
zim::Query suggestionQuery;
|
||||
suggestionQuery.setQuery(queryString, true);
|
||||
auto suggestionSearch = searcher.search(suggestionQuery);
|
||||
auto suggestionResult = suggestionSearch.getResults(0, suggestionCount);
|
||||
auto search = searcher.suggest(queryString);
|
||||
auto srs = search.getResults(start, suggestionCount);
|
||||
|
||||
for (auto it = suggestionResult.begin(); it != suggestionResult.end(); it++) {
|
||||
for (auto it : srs) {
|
||||
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
|
||||
it.getPath(), it.getSnippet());
|
||||
suggestions.push_back(suggestion);
|
||||
@@ -354,9 +365,11 @@ SuggestionsList_t getSuggestions(const zim::Archive* const archive,
|
||||
std::vector<std::string> variants = getTitleVariants(queryString);
|
||||
int currCount = 0;
|
||||
for (auto it = variants.begin(); it != variants.end() && currCount < suggestionCount; it++) {
|
||||
for (auto& entry: archive->findByTitle(*it)) {
|
||||
SuggestionItem suggestion(entry.getTitle(), kiwix::normalize(entry.getTitle()),
|
||||
entry.getPath());
|
||||
auto search = searcher.suggest(queryString);
|
||||
auto srs = search.getResults(0, suggestionCount);
|
||||
for (auto it : srs) {
|
||||
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
|
||||
it.getPath());
|
||||
suggestions.push_back(suggestion);
|
||||
currCount++;
|
||||
}
|
||||
@@ -408,7 +421,9 @@ std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& requ
|
||||
} else if (meta_name == "publisher") {
|
||||
content = getMetaPublisher(*archive);
|
||||
} else if (meta_name == "favicon") {
|
||||
getArchiveFavicon(*archive, content, mimeType);
|
||||
getArchiveFavicon(*archive, 48, content, mimeType);
|
||||
} else if (const unsigned illustrationSize = parseIllustration(meta_name)) {
|
||||
getArchiveFavicon(*archive, illustrationSize, content, mimeType);
|
||||
} else {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
@@ -426,8 +441,6 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
|
||||
std::string content;
|
||||
std::string mimeType;
|
||||
unsigned int maxSuggestionCount = 10;
|
||||
unsigned int suggestionCount = 0;
|
||||
|
||||
std::string bookName;
|
||||
std::string bookId;
|
||||
@@ -442,6 +455,20 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
|
||||
auto start = 0;
|
||||
try {
|
||||
start = request.get_argument<unsigned int>("start");
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
unsigned int count = 10;
|
||||
try {
|
||||
count = request.get_argument<unsigned int>("count");
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
if (count == 0) {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return Response::build_404(*this, request, bookName, "");
|
||||
}
|
||||
@@ -455,7 +482,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
bool first = true;
|
||||
|
||||
/* Get the suggestions */
|
||||
SuggestionsList_t suggestions = getSuggestions(archive.get(), queryString, maxSuggestionCount);
|
||||
SuggestionsList_t suggestions = getSuggestions(archive.get(), queryString, start, count);
|
||||
for(auto& suggestion:suggestions) {
|
||||
MustacheData result;
|
||||
result.set("label", suggestion.getTitle());
|
||||
@@ -470,7 +497,6 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
result.set("first", first);
|
||||
first = false;
|
||||
results.push_back(result);
|
||||
suggestionCount++;
|
||||
}
|
||||
|
||||
|
||||
@@ -567,7 +593,7 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
if (! searcher) {
|
||||
searcher = std::make_shared<zim::Searcher>(*currentArchive);
|
||||
} else {
|
||||
searcher->add_archive(*currentArchive);
|
||||
searcher->addArchive(*currentArchive);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,8 +615,6 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
pageLength = 25;
|
||||
}
|
||||
|
||||
auto end = start + pageLength;
|
||||
|
||||
/* Get the results */
|
||||
try {
|
||||
zim::Query query;
|
||||
@@ -600,8 +624,7 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
|
||||
}
|
||||
|
||||
query.setVerbose(m_verbose.load());
|
||||
query.setQuery("", false);
|
||||
query.setQuery("");
|
||||
query.setGeorange(latitude, longitude, distance);
|
||||
} else {
|
||||
// Execute Ft search
|
||||
@@ -610,12 +633,11 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
}
|
||||
|
||||
std::string queryString = removeAccents(patternString);
|
||||
query.setQuery(queryString, false);
|
||||
query.setVerbose(m_verbose.load());
|
||||
query.setQuery(queryString);
|
||||
}
|
||||
|
||||
zim::Search search = searcher->search(query);
|
||||
SearchRenderer renderer(search.getResults(start, end), mp_nameMapper, start,
|
||||
SearchRenderer renderer(search.getResults(start, pageLength), mp_nameMapper, start,
|
||||
search.getEstimatedMatches());
|
||||
renderer.setSearchPattern(patternString);
|
||||
renderer.setSearchContent(bookName);
|
||||
|
||||
@@ -54,7 +54,8 @@ class InternalServer {
|
||||
bool verbose,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks);
|
||||
bool blockExternalLinks,
|
||||
std::string indexTemplateString);
|
||||
virtual ~InternalServer() = default;
|
||||
|
||||
MHD_Result handlerCallback(struct MHD_Connection* connection,
|
||||
@@ -75,8 +76,10 @@ class InternalServer {
|
||||
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_root(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request, bool partial);
|
||||
std::unique_ptr<Response> handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId);
|
||||
std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2_languages(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_meta(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_search(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
|
||||
@@ -101,6 +104,7 @@ class InternalServer {
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_blockExternalLinks;
|
||||
std::string m_indexTemplateString;
|
||||
struct MHD_Daemon* mp_daemon;
|
||||
|
||||
Library* mp_library;
|
||||
|
||||
@@ -55,10 +55,17 @@ std::unique_ptr<Response> 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") {
|
||||
return handle_catalog_v2_languages(request);
|
||||
} else {
|
||||
return Response::build_404(*this, request, "", "");
|
||||
}
|
||||
@@ -74,19 +81,21 @@ std::unique_ptr<Response> 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")},
|
||||
{"category_list_feed_id", gen_uuid(m_library_id + "/categories")}
|
||||
{"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")}
|
||||
},
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request)
|
||||
std::unique_ptr<Response> 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,
|
||||
@@ -94,6 +103,25 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const Reques
|
||||
);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> 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<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library);
|
||||
@@ -101,7 +129,19 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(mp_library->getBooksCategories()),
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.languagesOPDSFeed(),
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,18 +69,6 @@ std::string getMetaTags(const zim::Archive& archive, bool original) {
|
||||
return join(tags, ";");
|
||||
}
|
||||
|
||||
bool getArchiveFavicon(const zim::Archive& archive,
|
||||
std::string& content, std::string& mimeType){
|
||||
try {
|
||||
auto item = archive.getIllustrationItem();
|
||||
content = item.getData();
|
||||
mimeType = item.getMimetype();
|
||||
return true;
|
||||
} catch(zim::EntryNotFound& e) {};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string getMetaLanguage(const zim::Archive& archive) {
|
||||
return getMetadata(archive, "Language");
|
||||
}
|
||||
@@ -101,6 +89,46 @@ std::string getMetaPublisher(const zim::Archive& archive) {
|
||||
return getMetadata(archive, "Publisher");
|
||||
}
|
||||
|
||||
std::string getMetaFlavour(const zim::Archive& archive) {
|
||||
return getMetadata(archive, "Flavour");
|
||||
}
|
||||
|
||||
std::string getArchiveId(const zim::Archive& archive) {
|
||||
return (std::string) archive.getUuid();
|
||||
}
|
||||
|
||||
bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
|
||||
std::string& content, std::string& mimeType){
|
||||
try {
|
||||
auto item = archive.getIllustrationItem(size);
|
||||
content = item.getData();
|
||||
mimeType = item.getMimetype();
|
||||
return true;
|
||||
} catch(zim::EntryNotFound& e) {};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// should this be in libzim
|
||||
unsigned int getArchiveMediaCount(const zim::Archive& archive) {
|
||||
std::map<const std::string, unsigned int> counterMap = parseArchiveCounter(archive);
|
||||
unsigned int counter = 0;
|
||||
|
||||
for (auto &pair:counterMap) {
|
||||
if (startsWith(pair.first, "image/") ||
|
||||
startsWith(pair.first, "video/") ||
|
||||
startsWith(pair.first, "audio/")) {
|
||||
counter += pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
unsigned int getArchiveFileSize(const zim::Archive& archive) {
|
||||
return archive.getFilesize() / 1024;
|
||||
}
|
||||
|
||||
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry)
|
||||
{
|
||||
return entry.getItem(true);
|
||||
@@ -118,4 +146,13 @@ zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path
|
||||
throw zim::EntryNotFound("Cannot find entry for non empty path");
|
||||
}
|
||||
|
||||
MimeCounterType parseArchiveCounter(const zim::Archive& archive) {
|
||||
try {
|
||||
auto counterContent = archive.getMetadata("Counter");
|
||||
return parseMimetypeCounter(counterContent);
|
||||
} catch (zim::EntryNotFound& e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // kiwix
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#define KIWIX_ARCHIVETOOLS_H
|
||||
|
||||
#include <zim/archive.h>
|
||||
#include <tools/otherTools.h>
|
||||
|
||||
/**
|
||||
* This file contains all the functions that would make handling data related to
|
||||
@@ -33,15 +34,26 @@ namespace kiwix
|
||||
std::string getArchiveTitle(const zim::Archive& archive);
|
||||
std::string getMetaDescription(const zim::Archive& archive);
|
||||
std::string getMetaTags(const zim::Archive& archive, bool original = false);
|
||||
bool getArchiveFavicon(const zim::Archive& archive,
|
||||
std::string& content, std::string& mimeType);
|
||||
std::string getMetaLanguage(const zim::Archive& archive);
|
||||
std::string getMetaName(const zim::Archive& archive);
|
||||
std::string getMetaDate(const zim::Archive& archive);
|
||||
std::string getMetaCreator(const zim::Archive& archive);
|
||||
std::string getMetaPublisher(const zim::Archive& archive);
|
||||
std::string getMetaFlavour(const zim::Archive& archive);
|
||||
std::string getArchiveId(const zim::Archive& archive);
|
||||
|
||||
bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
|
||||
std::string& content, std::string& mimeType);
|
||||
|
||||
unsigned int getArchiveMediaCount(const zim::Archive& archive);
|
||||
unsigned int getArchiveFileSize(const zim::Archive& archive);
|
||||
|
||||
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry);
|
||||
|
||||
zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path);
|
||||
|
||||
MimeCounterType parseArchiveCounter(const zim::Archive& archive);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -269,7 +269,7 @@ std::string getFileSizeAsString(const std::string& path)
|
||||
return convert.str();
|
||||
}
|
||||
|
||||
std::string getFileContent(const std::string& path)
|
||||
std::string kiwix::getFileContent(const std::string& path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
auto wpath = Utf8ToWide(path);
|
||||
@@ -302,7 +302,7 @@ std::string getFileContent(const std::string& path)
|
||||
return content;
|
||||
}
|
||||
|
||||
bool fileExists(const std::string& path)
|
||||
bool kiwix::fileExists(const std::string& path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return PathFileExistsW(Utf8ToWide(path).c_str());
|
||||
@@ -505,7 +505,7 @@ static std::map<std::string, std::string> extMimeTypes = {
|
||||
};
|
||||
|
||||
/* Try to get the mimeType from the file extension */
|
||||
std::string getMimeTypeForFile(const std::string& filename)
|
||||
std::string kiwix::getMimeTypeForFile(const std::string& filename)
|
||||
{
|
||||
std::string mimeType = "text/plain";
|
||||
auto pos = filename.find_last_of(".");
|
||||
|
||||
@@ -29,13 +29,10 @@ std::wstring Utf8ToWide(const std::string& str);
|
||||
|
||||
unsigned int getFileSize(const std::string& path);
|
||||
std::string getFileSizeAsString(const std::string& path);
|
||||
std::string getFileContent(const std::string& path);
|
||||
bool fileExists(const std::string& path);
|
||||
bool makeDirectory(const std::string& path);
|
||||
std::string makeTmpDirectory();
|
||||
bool copyFile(const std::string& sourcePath, const std::string& destPath);
|
||||
bool writeTextFile(const std::string& path, const std::string& content);
|
||||
std::string getMimeTypeForFile(const std::string& filename);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ skin/download.png
|
||||
skin/hash.png
|
||||
skin/search-icon.svg
|
||||
skin/taskbar.js
|
||||
skin/langList.js
|
||||
skin/categoryList.js
|
||||
skin/iso6391To3.js
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
@@ -47,6 +45,8 @@ 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
|
||||
catalog_v2_searchdescription.xml
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const categoryList = {
|
||||
"other": "Other",
|
||||
"gutenberg": "Gutenberg",
|
||||
"mooc": "Mooc",
|
||||
"phet": "Phet",
|
||||
"psiram": "Psiram",
|
||||
"stack_exchange": "Stack Exchange",
|
||||
"ted": "Ted",
|
||||
"vikidia": "Vikidia",
|
||||
"wikibooks": "Wikibooks",
|
||||
"wikinews": "Wikinews",
|
||||
"wikipedia": "Wikipedia",
|
||||
"wikiquote": "Wikiquote",
|
||||
"wikisource": "Wikisource",
|
||||
"wikiversity": "Wikiversity",
|
||||
"wikivoyage": "Wikivoyage",
|
||||
"wiktionary": "Wiktionary"
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
start: 0,
|
||||
count: viewPortToCount()
|
||||
};
|
||||
const filterTypes = ['lang', 'category', 'q'];
|
||||
const bookOrderMap = new Map();
|
||||
const filterCookieName = 'filters';
|
||||
const oneDayDelta = 86400000;
|
||||
@@ -51,14 +50,20 @@
|
||||
quotient = quotient < units.length ? quotient : units.length - 1;
|
||||
fileSize /= (1000 ** quotient);
|
||||
return `${+fileSize.toFixed(2)} ${units[quotient]}`;
|
||||
};
|
||||
};
|
||||
|
||||
const humanFriendlyTitle = (title) => {
|
||||
title = title.replace(/_/g, ' ');
|
||||
return htmlEncode(title[0].toUpperCase() + title.slice(1));
|
||||
}
|
||||
|
||||
function htmlEncode(str) {
|
||||
return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`);
|
||||
}
|
||||
|
||||
function viewPortToCount(){
|
||||
return Math.floor(window.innerHeight/300 + 1)*(window.innerWidth>1000 ? 4 : 3);
|
||||
const zoom = Math.floor((( window.outerWidth - 10 ) / window.innerWidth) * 100);
|
||||
return Math.floor(window.innerHeight/(3*zoom) + 1)*(window.innerWidth/(2.5*zoom) + 1);
|
||||
}
|
||||
|
||||
function getInnerHtml(node, query) {
|
||||
@@ -66,11 +71,16 @@
|
||||
}
|
||||
|
||||
function generateBookHtml(book, sort = false) {
|
||||
const link = book.querySelector('link').getAttribute('href');
|
||||
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
|
||||
let iconUrl;
|
||||
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
|
||||
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
|
||||
iconUrl = link.getAttribute('href');
|
||||
}
|
||||
});
|
||||
const title = getInnerHtml(book, 'title');
|
||||
const description = getInnerHtml(book, 'summary');
|
||||
const id = getInnerHtml(book, 'id');
|
||||
const iconUrl = getInnerHtml(book, 'icon');
|
||||
const language = getInnerHtml(book, 'language');
|
||||
const tags = getInnerHtml(book, 'tags');
|
||||
let tagHtml = tags.split(';').filter(tag => {return !(tag.split(':')[0].startsWith('_'))})
|
||||
@@ -85,7 +95,7 @@
|
||||
} catch {
|
||||
downloadLink = '';
|
||||
}
|
||||
humanFriendlyZimSize = humanFriendlySize(zimSize);
|
||||
const humanFriendlyZimSize = humanFriendlySize(zimSize);
|
||||
|
||||
const divTag = document.createElement('div');
|
||||
divTag.setAttribute('class', 'book');
|
||||
@@ -93,7 +103,7 @@
|
||||
if (sort) {
|
||||
divTag.setAttribute('data-idx', bookOrderMap.get(id));
|
||||
}
|
||||
divTag.innerHTML = `<div class="book__wrapper"><div class='book__icon' ><img class="book__icon--image" src='${iconUrl}'></div>
|
||||
divTag.innerHTML = `<div class="book__wrapper"><div class='book__icon' ><img class="book__icon--image" src='${root}${iconUrl}'></div>
|
||||
<div class='book__title' title='${title}'>
|
||||
<div id="bookTitle">${title}</div>
|
||||
${humanFriendlyZimSize ? `<div id='bookSize'>${humanFriendlyZimSize}</div>`: ''}
|
||||
@@ -101,7 +111,7 @@
|
||||
<div class='book__description' title='${description}'>${description}</div>
|
||||
<div class='book__languageTag'>${language.substr(0, 2).toUpperCase()}</div>
|
||||
<div class='book__tags'><div class="book__tags--wrapper">${tagHtml}</div></div>
|
||||
<div class='book__links'> <a href="${link}" data-hover="Preview">Preview</a>${downloadLink ? ` | <span class="download" data-link=${downloadLink} class="modal-button">Download</span>` : ''} </div></div>`;
|
||||
<div class='book__links'> <a href="${root}${link}" data-hover="Preview">Preview</a>${downloadLink ? ` | <span class="download" data-link=${downloadLink} class="modal-button">Download</span>` : ''} </div></div>`;
|
||||
return divTag;
|
||||
}
|
||||
|
||||
@@ -170,6 +180,14 @@
|
||||
})
|
||||
}
|
||||
|
||||
async function getBookCount(query) {
|
||||
const url = `${root}/catalog/search?${query}`;
|
||||
return await fetch(url).then(async (resp) => {
|
||||
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
|
||||
return parseInt(data.querySelector('totalResults').innerHTML);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadBooks() {
|
||||
loader.style.display = 'block';
|
||||
return await fetch(queryUrlBuilder()).then(async (resp) => {
|
||||
@@ -201,11 +219,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAndDisplayOptions(nodeQuery, query) {
|
||||
// currently taking an object in place of query, will replace it with query while fetching data from backend later on.
|
||||
document.querySelector(nodeQuery).innerHTML += Object.keys(query)
|
||||
.map((option) => {return `<option value='${option}'>${htmlEncode(query[option])}</option>`})
|
||||
.join('');
|
||||
async function loadAndDisplayOptions(nodeQuery, query, valueEntryNode) {
|
||||
await fetch(query).then(async (resp) => {
|
||||
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
|
||||
let optionStr = '';
|
||||
data.querySelectorAll('entry').forEach(entry => {
|
||||
const title = getInnerHtml(entry, 'title');
|
||||
const value = getInnerHtml(entry, valueEntryNode);
|
||||
optionStr += `<option value='${value}'>${humanFriendlyTitle(title)}</option>`;
|
||||
});
|
||||
document.querySelector(nodeQuery).innerHTML += optionStr;
|
||||
});
|
||||
}
|
||||
|
||||
function checkAndInjectEmptyMessage() {
|
||||
@@ -355,21 +379,29 @@
|
||||
fadeOutDiv = document.getElementById('fadeOut');
|
||||
loader = document.querySelector('.loader');
|
||||
await loadAndDisplayBooks();
|
||||
await loadAndDisplayOptions('#languageFilter', langList);
|
||||
await loadAndDisplayOptions('#categoryFilter', categoryList);
|
||||
await loadAndDisplayOptions('#languageFilter', `${root}/catalog/v2/languages`, 'language');
|
||||
await loadAndDisplayOptions('#categoryFilter', `${root}/catalog/v2/categories`, 'title');
|
||||
document.querySelectorAll('.filter').forEach(filter => {
|
||||
filter.addEventListener('change', () => {resetAndFilter(filter.name, filter.value)});
|
||||
});
|
||||
if (filters) {
|
||||
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
|
||||
}
|
||||
params.forEach((value, key) => {document.getElementsByName(key)[0].value = value});
|
||||
params.forEach((value, key) => {
|
||||
const selectBox = document.getElementsByName(key)[0];
|
||||
if (selectBox) {
|
||||
selectBox.value = value
|
||||
}
|
||||
});
|
||||
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
|
||||
if (!window.location.search) {
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
const langFilter = document.getElementById('languageFilter');
|
||||
langFilter.value = browserLang.length === 3 ? browserLang : iso6391To3[browserLang];
|
||||
langFilter.dispatchEvent(new Event('change'));
|
||||
const lang = browserLang.length === 3 ? browserLang : iso6391To3[browserLang];
|
||||
if (await getBookCount(`lang=${lang}`)) {
|
||||
langFilter.value = lang;
|
||||
langFilter.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.filter').forEach(filter => {
|
||||
if (filter.value) {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const langList = {
|
||||
"aar": "Afaraf",
|
||||
"afr": "Afrikaans",
|
||||
"aka": "Akan",
|
||||
"amh": "አማርኛ",
|
||||
"ara": "اللغة العربية",
|
||||
"asm": "অসমীয়া",
|
||||
"aze": "azərbaycan dili",
|
||||
"bak": "башҡорт теле",
|
||||
"bel": "беларуская мова",
|
||||
"bul": "български език",
|
||||
"bam": "bamanankan",
|
||||
"ben": "বাংলা",
|
||||
"bod": "བོད་ཡིག",
|
||||
"bre": "brezhoneg",
|
||||
"bos": "bosanski jezik",
|
||||
"cat": "Català",
|
||||
"che": "нохчийн мотт",
|
||||
"cos": "corsu",
|
||||
"ces": "čeština",
|
||||
"chv": "чӑваш чӗлхи",
|
||||
"cym": "Cymraeg",
|
||||
"dan": "dansk",
|
||||
"deu": "Deutsch",
|
||||
"dzo": "རྫོང་ཁ",
|
||||
"ewe": "Eʋegbe",
|
||||
"eng": "English",
|
||||
"spa": "Español",
|
||||
"est": "eesti",
|
||||
"eus": "euskara",
|
||||
"fas": "فارسی",
|
||||
"ful": "Fulfulde",
|
||||
"fin": "suomi",
|
||||
"fao": "føroyskt",
|
||||
"fra": "Français",
|
||||
"gle": "Gaeilge",
|
||||
"glg": "galego",
|
||||
"grn": "Avañe'ẽ",
|
||||
"guj": "ગુજરાતી",
|
||||
"glv": "Gaelg",
|
||||
"hau": "هَوُسَ",
|
||||
"heb": "עברית",
|
||||
"hin": "हिन्दी",
|
||||
"hrv": "hrvatski jezik",
|
||||
"hun": "magyar",
|
||||
"hye": "Հայերեն",
|
||||
"ind": "Bahasa Indonesia",
|
||||
"ibo": "Asụsụ Igbo",
|
||||
"isl": "Íslenska",
|
||||
"ita": "Italiano",
|
||||
"iku": "ᐃᓄᒃᑎᑐᑦ",
|
||||
"jpn": "日本語",
|
||||
"jav": "basa Jawa",
|
||||
"kat": "ქართული",
|
||||
"kik": "Gĩkũyũ",
|
||||
"kaz": "қазақ тілі",
|
||||
"khm": "ខេមរភាសា",
|
||||
"kan": "ಕನ್ನಡ",
|
||||
"kor": "한국어",
|
||||
"kas": "कश्मीरी",
|
||||
"kur": "Kurdî",
|
||||
"cor": "Kernewek",
|
||||
"kir": "Кыргызча",
|
||||
"ltz": "Lëtzebuergesch",
|
||||
"lug": "Luganda",
|
||||
"lin": "Lingála",
|
||||
"lao": "ພາສາ",
|
||||
"lit": "lietuvių kalba",
|
||||
"lav": "latviešu valoda",
|
||||
"mlg": "fiteny malagasy",
|
||||
"mri": "te reo Māori",
|
||||
"mkd": "македонски јазик",
|
||||
"mal": "മലയാളം",
|
||||
"mon": "Монгол хэл",
|
||||
"mar": "मराठी",
|
||||
"mlt": "Malti",
|
||||
"mya": "ဗမာစာ",
|
||||
"nld": "Nederlands",
|
||||
"nya": "chiCheŵa",
|
||||
"orm": "Afaan Oromoo",
|
||||
"pol": "język polski",
|
||||
"por": "Português",
|
||||
"que": "Runa Simi",
|
||||
"roh": "rumantsch grischun",
|
||||
"run": "Ikirundi",
|
||||
"ron": "Română",
|
||||
"rus": "Русский",
|
||||
"kin": "Ikinyarwanda",
|
||||
"san": "संस्कृतम्",
|
||||
"snd": "सिन्धी",
|
||||
"sag": "yângâ tî sängö",
|
||||
"sin": "සිංහල",
|
||||
"slk": "slovenčina",
|
||||
"slv": "slovenski jezik",
|
||||
"sna": "chiShona",
|
||||
"som": "Soomaaliga",
|
||||
"sqi": "Shqip",
|
||||
"srp": "српски језик",
|
||||
"ssw": "SiSwati",
|
||||
"swe": "svenska",
|
||||
"tam": "தமிழ்",
|
||||
"tel": "తెలుగు",
|
||||
"tgk": "тоҷикӣ",
|
||||
"tha": "ไทย",
|
||||
"tir": "ትግርኛ",
|
||||
"tuk": "Türkmen",
|
||||
"tsn": "Setswana",
|
||||
"tur": "Türkçe",
|
||||
"tso": "Xitsonga",
|
||||
"tat": "татар теле",
|
||||
"uig": "ئۇيغۇرچە",
|
||||
"ukr": "Українська",
|
||||
"urd": "اردو",
|
||||
"uzb": "Ўзбек",
|
||||
"ven": "Tshivenḓa",
|
||||
"vie": "Tiếng Việt",
|
||||
"wln": "walon",
|
||||
"wol": "Wollof",
|
||||
"xho": "isiXhosa",
|
||||
"yor": "Yorùbá",
|
||||
"zho": "中文",
|
||||
"zul": "isiZulu"
|
||||
}
|
||||
@@ -9,30 +9,4 @@
|
||||
{{/filter}}
|
||||
<link rel="self" href="" type="application/atom+xml" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{{root}}/catalog/searchdescription.xml" />
|
||||
{{#books}}
|
||||
<entry>
|
||||
<id>{{id}}</id>
|
||||
<title>{{title}}</title>
|
||||
<summary>{{description}}</summary>
|
||||
<language>{{language}}</language>
|
||||
<updated>{{updated}}</updated>
|
||||
<name>{{name}}</name>
|
||||
<flavour>{{flavour}}</flavour>
|
||||
<category>{{category}}</category>
|
||||
<tags>{{tags}}</tags>
|
||||
<articleCount>{{article_count}}</articleCount>
|
||||
<mediaCount>{{media_count}}</mediaCount>
|
||||
<icon>/meta?name=favicon&content={{{content_id}}}</icon>
|
||||
<link type="text/html" href="/{{{content_id}}}" />
|
||||
<author>
|
||||
<name>{{author_name}}</name>
|
||||
</author>
|
||||
<publisher>
|
||||
<name>{{publisher_name}}</name>
|
||||
</publisher>
|
||||
{{#url}}
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
|
||||
{{/url}}
|
||||
</entry>
|
||||
{{/books}}
|
||||
</feed>
|
||||
{{#books}}{{{entry}}}{{/books}}</feed>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<id>{{feed_id}}</id>
|
||||
|
||||
<link rel="self"
|
||||
href="{{endpoint_root}}/entries{{{query}}}"
|
||||
href="{{endpoint_root}}/{{#dump_partial_entries}}partial_{{/dump_partial_entries}}entries{{{query}}}"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<link rel="start"
|
||||
href="{{endpoint_root}}/root.xml"
|
||||
@@ -21,30 +21,4 @@
|
||||
<startIndex>{{startIndex}}</startIndex>
|
||||
<itemsPerPage>{{itemsPerPage}}</itemsPerPage>
|
||||
{{/filter}}
|
||||
{{#books}}
|
||||
<entry>
|
||||
<id>{{id}}</id>
|
||||
<title>{{title}}</title>
|
||||
<summary>{{description}}</summary>
|
||||
<language>{{language}}</language>
|
||||
<updated>{{updated}}</updated>
|
||||
<name>{{name}}</name>
|
||||
<flavour>{{flavour}}</flavour>
|
||||
<category>{{category}}</category>
|
||||
<tags>{{tags}}</tags>
|
||||
<articleCount>{{article_count}}</articleCount>
|
||||
<mediaCount>{{media_count}}</mediaCount>
|
||||
<icon>/meta?name=favicon&content={{{content_id}}}</icon>
|
||||
<link type="text/html" href="/{{{content_id}}}" />
|
||||
<author>
|
||||
<name>{{author_name}}</name>
|
||||
</author>
|
||||
<publisher>
|
||||
<name>{{publisher_name}}</name>
|
||||
</publisher>
|
||||
{{#url}}
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
|
||||
{{/url}}
|
||||
</entry>
|
||||
{{/books}}
|
||||
</feed>
|
||||
{{#books}}{{{entry}}}{{/books}}</feed>
|
||||
|
||||
34
static/templates/catalog_v2_entry.xml
Normal file
34
static/templates/catalog_v2_entry.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
{{#with_xml_header}}<?xml version="1.0" encoding="UTF-8"?>
|
||||
{{/with_xml_header}} <entry>
|
||||
<id>urn:uuid:{{id}}</id>
|
||||
<title>{{title}}</title>
|
||||
<updated>{{updated}}</updated>
|
||||
{{#dump_partial_entries}}
|
||||
<link rel="alternate"
|
||||
href="{{endpoint_root}}/entry/{{{id}}}"
|
||||
type="application/atom+xml;type=entry;profile=opds-catalog"/>
|
||||
{{/dump_partial_entries}}{{^dump_partial_entries}} <summary>{{description}}</summary>
|
||||
<language>{{language}}</language>
|
||||
<name>{{name}}</name>
|
||||
<flavour>{{flavour}}</flavour>
|
||||
<category>{{category}}</category>
|
||||
<tags>{{tags}}</tags>
|
||||
<articleCount>{{article_count}}</articleCount>
|
||||
<mediaCount>{{media_count}}</mediaCount>
|
||||
{{#icons}}
|
||||
<link rel="http://opds-spec.org/image/thumbnail"
|
||||
href="/meta?name=Illustration_{{icon_width}}x{{icon_height}}@{{icon_scale}}&content={{{content_id}}}"
|
||||
type="image/png;width={{icon_width}};height={{icon_height}};scale={{icon_scale}}"/>
|
||||
{{/icons}}
|
||||
<link type="text/html" href="/{{{content_id}}}" />
|
||||
<author>
|
||||
<name>{{author_name}}</name>
|
||||
</author>
|
||||
<publisher>
|
||||
<name>{{publisher_name}}</name>
|
||||
</publisher>
|
||||
{{#url}}
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
|
||||
{{/url}}
|
||||
{{/dump_partial_entries}}
|
||||
</entry>
|
||||
28
static/templates/catalog_v2_languages.xml
Normal file
28
static/templates/catalog_v2_languages.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:dc="http://purl.org/dc/terms/"
|
||||
xmlns:opds="https://specs.opds.io/opds-1.2"
|
||||
xmlns:thr="http://purl.org/syndication/thread/1.0">
|
||||
<id>{{feed_id}}</id>
|
||||
<link rel="self"
|
||||
href="{{endpoint_root}}/languages"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="{{endpoint_root}}/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<title>List of languages</title>
|
||||
<updated>{{date}}</updated>
|
||||
|
||||
{{#languages}}
|
||||
<entry>
|
||||
<title>{{lang_self_name}}</title>
|
||||
<dc:language>{{{lang_code}}}</dc:language>
|
||||
<thr:count>{{book_count}}</thr:count>
|
||||
<link rel="subsection"
|
||||
href="{{endpoint_root}}/entries?lang={{{lang_code}}}"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>{{updated}}</updated>
|
||||
<id>{{id}}</id>
|
||||
</entry>
|
||||
{{/languages}}
|
||||
</feed>
|
||||
@@ -23,6 +23,15 @@
|
||||
<id>{{all_entries_feed_id}}</id>
|
||||
<content type="text">All entries from this catalog.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>All entries (partial)</title>
|
||||
<link rel="subsection"
|
||||
href="{{endpoint_root}}/partial_entries"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>{{date}}</updated>
|
||||
<id>{{partial_entries_feed_id}}</id>
|
||||
<content type="text">All entries from this catalog in partial format.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>List of categories</title>
|
||||
<link rel="subsection"
|
||||
@@ -32,4 +41,13 @@
|
||||
<id>{{category_list_feed_id}}</id>
|
||||
<content type="text">List of all categories in this catalog.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>List of languages</title>
|
||||
<link rel="subsection"
|
||||
href="{{endpoint_root}}/languages"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<updated>{{date}}</updated>
|
||||
<id>{{language_list_feed_id}}</id>
|
||||
<content type="text">List of all languages in this catalog.</content>
|
||||
</entry>
|
||||
</feed>
|
||||
|
||||
@@ -39,8 +39,6 @@
|
||||
}
|
||||
</style>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
|
||||
<script src="{{root}}/skin/categoryList.js"></script>
|
||||
<script src="{{root}}/skin/langList.js"></script>
|
||||
<script src="{{root}}/skin/iso6391To3.js"></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
|
||||
</head>
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
|
||||
title="Ray (uncategorized) Charles"
|
||||
description="No category is assigned to this library entry."
|
||||
language="eng"
|
||||
language="rus"
|
||||
creator="Wikipedia"
|
||||
publisher="Kiwix"
|
||||
date="2020-03-31"
|
||||
name="wikipedia_en_ray_charles"
|
||||
name="wikipedia_ru_ray_charles"
|
||||
tags="unittest;wikipedia;_pictures:no;_videos:no;_details:no"
|
||||
articleCount="284"
|
||||
mediaCount="2"
|
||||
@@ -37,11 +37,11 @@
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
|
||||
title="Charles, Ray"
|
||||
description="Wikipedia articles about Ray Charles"
|
||||
language="eng"
|
||||
language="fra"
|
||||
creator="Wikipedia"
|
||||
publisher="Kiwix"
|
||||
date="2020-03-31"
|
||||
name="wikipedia_en_ray_charles"
|
||||
name="wikipedia_fr_ray_charles"
|
||||
tags="unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes"
|
||||
articleCount="284"
|
||||
mediaCount="2"
|
||||
|
||||
6439
test/httplib.h
6439
test/httplib.h
File diff suppressed because it is too large
Load Diff
@@ -275,9 +275,25 @@ TEST_F(LibraryTest, getBookMarksTest)
|
||||
TEST_F(LibraryTest, sanityCheck)
|
||||
{
|
||||
EXPECT_EQ(lib.getBookCount(true, true), 12U);
|
||||
EXPECT_EQ(lib.getBooksLanguages().size(), 3U);
|
||||
EXPECT_EQ(lib.getBooksCreators().size(), 9U);
|
||||
EXPECT_EQ(lib.getBooksPublishers().size(), 3U);
|
||||
EXPECT_EQ(lib.getBooksLanguages(),
|
||||
std::vector<std::string>({"deu", "eng", "fra"})
|
||||
);
|
||||
EXPECT_EQ(lib.getBooksCreators(), std::vector<std::string>({
|
||||
"Islam Stack Exchange",
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"TED",
|
||||
"Tania Louis",
|
||||
"Wiki",
|
||||
"Wikibooks",
|
||||
"Wikipedia",
|
||||
"Wikiquote"
|
||||
}));
|
||||
EXPECT_EQ(lib.getBooksPublishers(), std::vector<std::string>({
|
||||
"",
|
||||
"Kiwix",
|
||||
"Kiwix & Some Enthusiasts"
|
||||
}));
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, categoryHandling)
|
||||
|
||||
@@ -6,20 +6,62 @@ namespace kiwix
|
||||
{
|
||||
|
||||
TEST(Searcher, search) {
|
||||
Reader reader("./test/example.zim");
|
||||
Reader reader("./test/example.zim");
|
||||
|
||||
Searcher searcher;
|
||||
searcher.add_reader(&reader);
|
||||
ASSERT_EQ(searcher.get_reader(0)->getTitle(), reader.getTitle());
|
||||
Searcher searcher;
|
||||
searcher.add_reader(&reader);
|
||||
ASSERT_EQ(searcher.get_reader(0)->getTitle(), reader.getTitle());
|
||||
|
||||
searcher.search("wiki", 0, 2);
|
||||
searcher.restart_search();
|
||||
ASSERT_EQ(searcher.getEstimatedResultCount(), (unsigned int)2);
|
||||
searcher.search("wiki", 0, 2);
|
||||
searcher.restart_search();
|
||||
ASSERT_EQ(searcher.getEstimatedResultCount(), (unsigned int)2);
|
||||
|
||||
auto result = searcher.getNextResult();
|
||||
ASSERT_EQ(result->get_title(), "FreedomBox for Communities/Offline Wikipedia - Wikibooks, open books for an open world");
|
||||
result = searcher.getNextResult();
|
||||
ASSERT_EQ(result->get_title(), "Wikibooks");
|
||||
auto result = searcher.getNextResult();
|
||||
ASSERT_EQ(result->get_title(), "FreedomBox for Communities/Offline Wikipedia - Wikibooks, open books for an open world");
|
||||
result = searcher.getNextResult();
|
||||
ASSERT_EQ(result->get_title(), "Wikibooks");
|
||||
}
|
||||
|
||||
TEST(Searcher, suggestion) {
|
||||
Reader reader("./test/zimfile.zim");
|
||||
|
||||
Searcher searcher;
|
||||
searcher.add_reader(&reader);
|
||||
ASSERT_EQ(searcher.get_reader(0)->getTitle(), reader.getTitle());
|
||||
|
||||
std::string query = "ray";
|
||||
searcher.suggestions(query, true);
|
||||
searcher.restart_search();
|
||||
|
||||
auto result = searcher.getNextResult();
|
||||
ASSERT_EQ(result->get_title(), "Charles, Ray");
|
||||
ASSERT_EQ(result->get_url(), "A/Charles,_Ray");
|
||||
ASSERT_EQ(result->get_snippet(), "Charles, <b>Ray</b>");
|
||||
ASSERT_EQ(result->get_score(), 0);
|
||||
ASSERT_EQ(result->get_content(), "");
|
||||
ASSERT_EQ(result->get_size(), 0);
|
||||
ASSERT_EQ(result->get_wordCount(), 0);
|
||||
ASSERT_EQ(result->get_zimId(), "");
|
||||
|
||||
result = searcher.getNextResult();
|
||||
ASSERT_EQ(result->get_title(), "Ray (film)");
|
||||
}
|
||||
|
||||
TEST(Searcher, incrementalRange) {
|
||||
// Attempt to get 50 results in steps of 5
|
||||
zim::Archive archive("./test/zimfile.zim");
|
||||
zim::Searcher ftsearcher(archive);
|
||||
zim::Query query;
|
||||
query.setQuery("ray");
|
||||
auto search = ftsearcher.search(query);
|
||||
|
||||
int suggCount = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto srs = search.getResults(i*5, 5); // get 5 results
|
||||
ASSERT_EQ(srs.size(), 5);
|
||||
suggCount += srs.size();
|
||||
}
|
||||
ASSERT_EQ(suggCount, 50);
|
||||
}
|
||||
|
||||
}
|
||||
269
test/server.cpp
269
test/server.cpp
@@ -54,21 +54,21 @@ public: // types
|
||||
|
||||
public: // functions
|
||||
ZimFileServer(int serverPort, std::string libraryFilePath);
|
||||
ZimFileServer(int serverPort, const FilePathCollection& zimpaths);
|
||||
ZimFileServer(int serverPort, const FilePathCollection& zimpaths, std::string indexTemplateString = "");
|
||||
~ZimFileServer();
|
||||
|
||||
Response GET(const char* path, const Headers& headers = Headers())
|
||||
httplib::Result GET(const char* path, const Headers& headers = Headers())
|
||||
{
|
||||
return client->Get(path, headers);
|
||||
}
|
||||
|
||||
Response HEAD(const char* path, const Headers& headers = Headers())
|
||||
httplib::Result HEAD(const char* path, const Headers& headers = Headers())
|
||||
{
|
||||
return client->Head(path, headers);
|
||||
}
|
||||
|
||||
private:
|
||||
void run(int serverPort);
|
||||
void run(int serverPort, std::string indexTemplateString = "");
|
||||
|
||||
private: // data
|
||||
kiwix::Library library;
|
||||
@@ -88,7 +88,7 @@ ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
|
||||
run(serverPort);
|
||||
}
|
||||
|
||||
ZimFileServer::ZimFileServer(int serverPort, const FilePathCollection& zimpaths)
|
||||
ZimFileServer::ZimFileServer(int serverPort, const FilePathCollection& zimpaths, std::string indexTemplateString)
|
||||
: manager(&this->library)
|
||||
{
|
||||
for ( const auto& zimpath : zimpaths ) {
|
||||
@@ -96,10 +96,10 @@ ZimFileServer::ZimFileServer(int serverPort, const FilePathCollection& zimpaths)
|
||||
throw std::runtime_error("Unable to add the ZIM file '" + zimpath + "'");
|
||||
}
|
||||
|
||||
run(serverPort);
|
||||
run(serverPort, indexTemplateString);
|
||||
}
|
||||
|
||||
void ZimFileServer::run(int serverPort)
|
||||
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
||||
{
|
||||
const std::string address = "127.0.0.1";
|
||||
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
|
||||
@@ -108,6 +108,9 @@ void ZimFileServer::run(int serverPort)
|
||||
server->setPort(serverPort);
|
||||
server->setNbThreads(2);
|
||||
server->setVerbose(false);
|
||||
if (!indexTemplateString.empty()) {
|
||||
server->setIndexTemplateString(indexTemplateString);
|
||||
}
|
||||
|
||||
if ( !server->start() )
|
||||
throw std::runtime_error("ZimFileServer failed to start");
|
||||
@@ -197,6 +200,7 @@ const ResourceCollection resources200Uncompressible{
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=creator" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=publisher" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=favicon" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=Illustration_48x48@1" },
|
||||
|
||||
{ WITH_ETAG, "/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
|
||||
|
||||
@@ -210,6 +214,35 @@ ResourceCollection all200Resources()
|
||||
return concat(resources200Compressible, resources200Uncompressible);
|
||||
}
|
||||
|
||||
TEST(indexTemplateStringTest, emptyIndexTemplate) {
|
||||
const int PORT = 8001;
|
||||
const ZimFileServer::FilePathCollection ZIMFILES {
|
||||
"./test/zimfile.zim",
|
||||
"./test/corner_cases.zim"
|
||||
};
|
||||
|
||||
ZimFileServer zfs(PORT, ZIMFILES, "");
|
||||
EXPECT_EQ(200, zfs.GET("/")->status);
|
||||
}
|
||||
|
||||
TEST(indexTemplateStringTest, indexTemplateCheck) {
|
||||
const int PORT = 8001;
|
||||
const ZimFileServer::FilePathCollection ZIMFILES {
|
||||
"./test/zimfile.zim",
|
||||
"./test/corner_cases.zim"
|
||||
};
|
||||
|
||||
ZimFileServer zfs(PORT, ZIMFILES, "<!DOCTYPE html><head>"
|
||||
"<title>Welcome to kiwix library</title>"
|
||||
"</head>"
|
||||
"</html>");
|
||||
EXPECT_EQ("<!DOCTYPE html><head>"
|
||||
"<title>Welcome to kiwix library</title>"
|
||||
"<link type=\"root\" href=\"\">"
|
||||
"</head>"
|
||||
"</html>", zfs.GET("/")->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, 200)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() )
|
||||
@@ -617,16 +650,18 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:charlesray</id>\n" \
|
||||
" <title>Charles, Ray</title>\n" \
|
||||
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
||||
" <language>eng</language>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
||||
" <language>fra</language>\n" \
|
||||
" <name>wikipedia_fr_ray_charles</name>\n" \
|
||||
" <flavour></flavour>\n" \
|
||||
" <category>jazz</category>\n" \
|
||||
" <tags>unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
|
||||
" <articleCount>284</articleCount>\n" \
|
||||
" <mediaCount>2</mediaCount>\n" \
|
||||
" <icon>/meta?name=favicon&content=zimfile</icon>\n" \
|
||||
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
|
||||
" href=\"/meta?name=Illustration_48x48@1&content=zimfile\"\n" \
|
||||
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
|
||||
" <link type=\"text/html\" href=\"/zimfile\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
@@ -641,16 +676,18 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:raycharles</id>\n" \
|
||||
" <title>Ray Charles</title>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
||||
" <language>eng</language>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||
" <flavour></flavour>\n" \
|
||||
" <category>wikipedia</category>\n" \
|
||||
" <tags>unittest;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
|
||||
" <articleCount>284</articleCount>\n" \
|
||||
" <mediaCount>2</mediaCount>\n" \
|
||||
" <icon>/meta?name=favicon&content=zimfile</icon>\n" \
|
||||
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
|
||||
" href=\"/meta?name=Illustration_48x48@1&content=zimfile\"\n" \
|
||||
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
|
||||
" <link type=\"text/html\" href=\"/zimfile\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
@@ -665,16 +702,18 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:raycharles_uncategorized</id>\n" \
|
||||
" <title>Ray (uncategorized) Charles</title>\n" \
|
||||
" <summary>No category is assigned to this library entry.</summary>\n" \
|
||||
" <language>eng</language>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||
" <summary>No category is assigned to this library entry.</summary>\n" \
|
||||
" <language>rus</language>\n" \
|
||||
" <name>wikipedia_ru_ray_charles</name>\n" \
|
||||
" <flavour></flavour>\n" \
|
||||
" <category></category>\n" \
|
||||
" <tags>unittest;wikipedia;_pictures:no;_videos:no;_details:no</tags>\n" \
|
||||
" <articleCount>284</articleCount>\n" \
|
||||
" <mediaCount>2</mediaCount>\n" \
|
||||
" <icon>/meta?name=favicon&content=zimfile</icon>\n" \
|
||||
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
|
||||
" href=\"/meta?name=Illustration_48x48@1&content=zimfile\"\n" \
|
||||
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
|
||||
" <link type=\"text/html\" href=\"/zimfile\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
@@ -898,7 +937,6 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
|
||||
" <startIndex>100</startIndex>\n"
|
||||
" <itemsPerPage>0</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
" \n"
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
@@ -933,6 +971,15 @@ TEST_F(LibraryServerTest, catalog_v2_root)
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<content type="text">All entries from this catalog.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>All entries (partial)</title>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/partial_entries"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<content type="text">All entries from this catalog in partial format.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>List of categories</title>
|
||||
<link rel="subsection"
|
||||
@@ -942,6 +989,15 @@ TEST_F(LibraryServerTest, catalog_v2_root)
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<content type="text">List of all categories in this catalog.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>List of languages</title>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/languages"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<content type="text">List of all languages in this catalog.</content>
|
||||
</entry>
|
||||
</feed>
|
||||
)";
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
|
||||
@@ -1005,7 +1061,61 @@ TEST_F(LibraryServerTest, catalog_v2_categories)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
|
||||
}
|
||||
|
||||
#define CATALOG_V2_ENTRIES_PREAMBLE(q) \
|
||||
TEST_F(LibraryServerTest, catalog_v2_languages)
|
||||
{
|
||||
const auto r = zfs1_->GET("/catalog/v2/languages");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:dc="http://purl.org/dc/terms/"
|
||||
xmlns:opds="https://specs.opds.io/opds-1.2"
|
||||
xmlns:thr="http://purl.org/syndication/thread/1.0">
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<link rel="self"
|
||||
href="/catalog/v2/languages"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="/catalog/v2/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<title>List of languages</title>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
|
||||
<entry>
|
||||
<title>English</title>
|
||||
<dc:language>eng</dc:language>
|
||||
<thr:count>1</thr:count>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?lang=eng"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>français</title>
|
||||
<dc:language>fra</dc:language>
|
||||
<thr:count>1</thr:count>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?lang=fra"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>русский</title>
|
||||
<dc:language>rus</dc:language>
|
||||
<thr:count>1</thr:count>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?lang=rus"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
</entry>
|
||||
</feed>
|
||||
)";
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
|
||||
}
|
||||
|
||||
#define CATALOG_V2_ENTRIES_PREAMBLE0(x) \
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
|
||||
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
|
||||
" xmlns:opds=\"https://specs.opds.io/opds-1.2\"\n" \
|
||||
@@ -1013,7 +1123,7 @@ TEST_F(LibraryServerTest, catalog_v2_categories)
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
|
||||
"\n" \
|
||||
" <link rel=\"self\"\n" \
|
||||
" href=\"/catalog/v2/entries" q "\"\n" \
|
||||
" href=\"/catalog/v2/" x "\"\n" \
|
||||
" type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"/>\n" \
|
||||
" <link rel=\"start\"\n" \
|
||||
" href=\"/catalog/v2/root.xml\"\n" \
|
||||
@@ -1023,6 +1133,11 @@ TEST_F(LibraryServerTest, catalog_v2_categories)
|
||||
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\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)
|
||||
{
|
||||
@@ -1106,3 +1221,117 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, suggestions_in_range)
|
||||
{
|
||||
/**
|
||||
* Attempt to get 50 suggestions in steps of 5
|
||||
* The suggestions are returned in the json format
|
||||
* [{sugg1}, {sugg2}, ... , {suggN}, {suggest ft search}]
|
||||
* Assuming the number of suggestions = (occurance of "{" - 1)
|
||||
*/
|
||||
{
|
||||
int suggCount = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::string url = "/suggest?content=zimfile&term=ray&start=" + std::to_string(i*5) + "&count=5";
|
||||
const auto r = zfs1_->GET(url.c_str());
|
||||
std::string body = r->body;
|
||||
int currCount = std::count(body.begin(), body.end(), '{') - 1;
|
||||
ASSERT_EQ(currCount, 5);
|
||||
suggCount += currCount;
|
||||
}
|
||||
ASSERT_EQ(suggCount, 50);
|
||||
}
|
||||
|
||||
// Attempt to get 10 suggestions in steps of 5 even though there are only 8
|
||||
{
|
||||
std::string url = "/suggest?content=zimfile&term=song+for+you&start=0&count=5";
|
||||
const auto r1 = zfs1_->GET(url.c_str());
|
||||
std::string body = r1->body;
|
||||
int currCount = std::count(body.begin(), body.end(), '{') - 1;
|
||||
ASSERT_EQ(currCount, 5);
|
||||
|
||||
url = "/suggest?content=zimfile&term=song+for+you&start=5&count=5";
|
||||
const auto r2 = zfs1_->GET(url.c_str());
|
||||
body = r2->body;
|
||||
currCount = std::count(body.begin(), body.end(), '{') - 1;
|
||||
ASSERT_EQ(currCount, 3);
|
||||
}
|
||||
|
||||
// Attempt to get 10 suggestions even though there is only 1
|
||||
{
|
||||
std::string url = "/suggest?content=zimfile&term=strong&start=0&count=5";
|
||||
const auto r = zfs1_->GET(url.c_str());
|
||||
std::string body = r->body;
|
||||
int currCount = std::count(body.begin(), body.end(), '{') - 1;
|
||||
ASSERT_EQ(currCount, 1);
|
||||
}
|
||||
|
||||
// No Suggestion
|
||||
{
|
||||
std::string url = "/suggest?content=zimfile&term=oops&start=0&count=5";
|
||||
const auto r = zfs1_->GET(url.c_str());
|
||||
std::string body = r->body;
|
||||
int currCount = std::count(body.begin(), body.end(), '{') - 1;
|
||||
ASSERT_EQ(currCount, 0);
|
||||
}
|
||||
|
||||
// Out of bound value
|
||||
{
|
||||
std::string url = "/suggest?content=zimfile&term=ray&start=-2&count=-1";
|
||||
const auto r = zfs1_->GET(url.c_str());
|
||||
std::string body = r->body;
|
||||
int currCount = std::count(body.begin(), body.end(), '{') - 1;
|
||||
ASSERT_EQ(currCount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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("")
|
||||
" <title>All Entries</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
"\n"
|
||||
" <entry>\n"
|
||||
" <id>urn:uuid:charlesray</id>\n"
|
||||
" <title>Charles, Ray</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <link rel=\"alternate\"\n"
|
||||
" href=\"/catalog/v2/entry/charlesray\"\n"
|
||||
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
|
||||
" </entry>\n"
|
||||
" <entry>\n"
|
||||
" <id>urn:uuid:raycharles</id>\n"
|
||||
" <title>Ray Charles</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <link rel=\"alternate\"\n"
|
||||
" href=\"/catalog/v2/entry/raycharles\"\n"
|
||||
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
|
||||
" </entry>\n"
|
||||
" <entry>\n"
|
||||
" <id>urn:uuid:raycharles_uncategorized</id>\n"
|
||||
" <title>Ray (uncategorized) Charles</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <link rel=\"alternate\"\n"
|
||||
" href=\"/catalog/v2/entry/raycharles_uncategorized\"\n"
|
||||
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
|
||||
" </entry>\n"
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user