Compare commits

...

95 Commits

Author SHA1 Message Date
Matthieu Gautier
9f19327023 Fix maybe initialized warning in httplib.
Patch has been send upstream :
https://github.com/yhirose/cpp-httplib/pull/1085
2021-11-05 17:12:33 +01:00
Matthieu Gautier
b2b907f813 Update httplib to last version (0.9.7)
Release url is https://github.com/yhirose/cpp-httplib/releases/tag/v0.9.7
2021-11-05 17:12:33 +01:00
Kelson
9479c0685d Merge pull request #623 from kiwix/update-ci
Re-introduce Ubuntu Impish in CI
2021-10-21 12:41:23 +02:00
Emmanuel Engelhart
09a55d71d6 Re-introduce Ubuntu Impish in CI 2021-10-21 12:36:05 +02:00
Matthieu Gautier
503eb5c4ce Merge pull request #621 from kiwix/fix_ci_docker_version 2021-10-19 11:49:20 +02:00
Matthieu Gautier
f714ff8d3e New docker image version is 31. 2021-10-18 18:09:36 +02:00
Matthieu Gautier
08e3d52957 Merge pull request #607 from kiwix/issue/571 2021-10-12 17:40:27 +02:00
Manan Jethwani
30e4c549e4 exposed fileExist, getMimeTypeForFile and getFileCoontent functions 2021-10-12 19:44:38 +05:30
Manan Jethwani
b7b385d87b added custom index template 2021-10-12 19:44:05 +05:30
Matthieu Gautier
e46b0c07b5 Merge pull request #617 from kiwix/adapt_new_libzim_api 2021-09-30 14:52:17 +02:00
Matthieu Gautier
cd9fb541fc Fix method call for new libzim API.
`add_archive` is now `addArchive`.
2021-09-29 11:55:22 +02:00
Matthieu Gautier
3b942bb745 Merge pull request #602 from kiwix/partial_opds_entries
OPDS feed with partial entries
2021-09-09 12:07:02 +02:00
Veloman Yunkan
c0bda426b4 Removed duplication across two mustache templates
Deduplicated the mustache templates static/templates/catalog_v2_entries.xml
and static/templates/catalog_v2_complete_entry.xml (the latter was
renamed to static/templates/catalog_v2_entry.xml).
2021-09-09 12:19:22 +04:00
Veloman Yunkan
b3f7556096 Added partial entries feed to the OPDS root feed 2021-09-09 12:19:22 +04:00
Veloman Yunkan
4c657c082e /catalog/v2/partial_entries OPDS API endpoint 2021-09-09 12:19:22 +04:00
Veloman Yunkan
e773a29f29 Rearranged elements in OPDS entry XML 2021-09-09 12:19:22 +04:00
Veloman Yunkan
e15a0f4338 /catalog/v2/entry/<entry_id> OPDS API endpoint 2021-09-09 12:19:22 +04:00
Veloman Yunkan
12d9b69806 OPDSDumper::dumpOPDSCompleteEntry() 2021-09-09 12:19:22 +04:00
Veloman Yunkan
027854e4f4 Extracted getSingleBookData() in opds_dumper.cpp 2021-09-09 12:19:22 +04:00
Matthieu Gautier
417e7471ac Merge pull request #614 from kiwix/disable-impish 2021-09-09 10:10:50 +02:00
Matthieu Gautier
51ac1240f8 PPA: Temporarily disable Impish builds 2021-09-09 10:05:18 +02:00
Kelson
ea6413ff88 Merge pull request #591 from kiwix/suggestion_range
Allow kiwix-serve to get suggestions of custom range
2021-08-20 08:09:35 +02:00
Maneesh P M
61209ea0d7 Allow kiwix-serve to get suggestions of custom range
This will allow handle_suggest API to accept two arguments `start` and
`suggestionLength` that will allow handle_suggest to retrieve
suggestions in the given range rather than the default 0-10 range.
2021-08-19 21:05:39 +05:30
Kelson
e9eaadde9e Merge pull request #567 from kiwix/suggestion_api_fix 2021-08-14 19:21:29 +02:00
Maneesh P M
8a4080baba Update libkiwix with new libzim api 2021-08-14 22:26:39 +05:30
Kelson
ba05999cba Merge pull request #604 from kiwix/issue/603 2021-08-11 06:50:30 +02:00
Manan Jethwani
a4c3cad018 fixed books availablity on larger screens and added zoom level support 2021-08-10 21:45:10 +05:30
Kelson
83e757a530 Merge pull request #600 from kiwix/dynamic_select_box_value
Use OPDS API to populate categories/languages select boxes on Kiwix Serve welcome page
2021-08-07 15:33:52 +02:00
Manan Jethwani
5e8f3a5505 added use of lang and category api for select boxes on welcome page 2021-08-07 02:39:50 +05:30
Manan Jethwani
fe93035a4c updated welcome page to support OPDS multiple Icon 2021-08-07 02:35:29 +05:30
Kelson
6e26c5aa75 Merge pull request #577 from kiwix/opds_multiple_icons
Support for multiple illustrations in OPDS entry
2021-08-05 23:22:07 +02:00
Veloman Yunkan
452283cfe6 Handling of /meta?name=Illustration_WxH@1 requests 2021-08-05 22:28:09 +04:00
Veloman Yunkan
e5168d8b3d Support for multiple illustrations in OPDS entry 2021-08-05 22:21:13 +04:00
Matthieu Gautier
b8aee8a42c Merge pull request #597 from kiwix/fix_get_results 2021-08-04 15:57:58 +02:00
Maneesh P M
9addd82d2d Fix usage of zim::Searcher::getResults() in libkiwix
The correct usage does not require the user to calculate an `end` using
the `pageLength`. We can directly use getResults(start, pageLength)
2021-08-04 19:20:50 +05:30
Maneesh P M
e74e7f5623 Add unit test for incremental searching
With this, we eventually want to see the usage of getResults giving
a FAILING TEST. This happens because the second argument to
getResults is NOT `end` of the range, but `maxResultCount` to retrieve.
This will be fixed in the next commit.
2021-08-04 19:20:05 +05:30
Matthieu Gautier
a032d65eb8 Merge pull request #576 from kiwix/extend_libkiwix_structures_to_use_libzim 2021-08-03 11:50:24 +02:00
Maneesh P M
19afe9442f Remove OriginId functions since they are not useful right now 2021-08-03 11:42:58 +02:00
Maneesh P M
a3ba7619df Update Manager to use Archive instead of Reader
kiwix::Manager uses Reader to import a zim file, it should be using
zim::Archive directly.
2021-08-03 11:42:58 +02:00
Maneesh P M
8b12434ff2 Update kiwix::book to use libzim structure
Some methods in kiwix::Book uses wrapper structure reader. This usage should
be extended from the native libzim structure zim::Archive
2021-08-03 11:42:58 +02:00
Matthieu Gautier
b4f7dfa5a2 Merge pull request #553 from kiwix/catalog_languages_endpoint 2021-08-03 11:41:31 +02:00
Veloman Yunkan
ab3095745e Languages OPDS feed includes book counts 2021-08-03 11:32:38 +02:00
Veloman Yunkan
45adda44b3 Got rid of <content> node in languages OPDS entry 2021-08-03 11:32:38 +02:00
Veloman Yunkan
96cf7e78a5 OPDSDumper::categoriesOPDSFeed() with no args 2021-08-03 11:32:38 +02:00
Veloman Yunkan
dd118df612 Got rid of langMap in opds_dumper.cpp
Language code to human friendly name translation is now done with the
help of the ICU library. It works if the line

```
-include $(LANGSRCDIR)/resfiles.mk
```

in the file `source/data/Makefile.in` of the icu4c dependency is not
commented out. Currently, the said line is commented out (along with
some other include's) by the `icu4c_custom_data.patch` patch of the
`kiwix-build` tool.
2021-08-03 11:32:38 +02:00
Veloman Yunkan
8a4248e48e Language code in /catalog/v2/languages entries 2021-08-03 11:32:38 +02:00
Veloman Yunkan
5f90f5ee2a Preliminary version of /catalog/v2/languages 2021-08-03 11:32:38 +02:00
Veloman Yunkan
64b55dbdc7 Made test library.xml a multi-language library 2021-08-03 11:32:38 +02:00
Veloman Yunkan
18871b4b15 Helper function Library::getBookPropValueSet()
Introduced a helper function `Library::getBookPropValueSet()` and
deduplicated Library::getBooks{Languages,Creators,Publishers}() methods.
2021-08-03 11:32:38 +02:00
Veloman Yunkan
b2027b397c List of languages entry in /catalog/v2/root.xml
Added a new entry in /catalog/v2/root.xml that points to a
not-yet-existing list of languages navigation feed.
2021-08-03 11:32:38 +02:00
Kelson
49322f5961 Merge pull request #596 from kiwix/better_filter
improved browser lang filter working
2021-07-31 23:24:14 +02:00
Manan Jethwani
0466b9759c improved browser lang filter working 2021-07-30 12:57:59 +05:30
Kelson
20cdefcdb8 Merge pull request #593 from kiwix/remove_groovy_package
Remove groovy deb package
2021-07-28 21:40:02 +02:00
Emmanuel Engelhart
6ea40f57da Remove groovy deb package 2021-07-28 21:34:24 +02:00
Kelson
a312d2218d Merge pull request #590 from kiwix/root_prefix_addition
corrected relative links in preview and icon url
2021-07-25 08:59:24 +02:00
Manan Jethwani
15839df594 corrected relative links in preview and icon url 2021-07-24 19:26:22 +05:30
Kelson
03a929e88e Merge pull request #583 from kiwix/download-modal
Modal download  box on Kiwix Serve welcome page
2021-07-13 16:47:58 +02:00
Manan Jethwani
646502f9cf changed font style for modal 2021-07-13 20:00:43 +05:30
Manan Jethwani
a8a96a99f4 corrected working of magnet link 2021-07-13 00:23:38 +05:30
Manan Jethwani
a517d3b529 added modal for downloading zim file on welcome page 2021-07-12 17:59:26 +05:30
Kelson
60f0f81286 Merge pull request #559 from kiwix/Css_revamp
Revamped Kiwix Serve Welcome page layout
2021-07-08 12:36:19 +02:00
Manan Jethwani
2ed9a50eca fixed button allignment 2021-07-08 12:33:28 +02:00
Manan Jethwani
bce922ab89 bug fix for loader 2021-07-08 12:33:28 +02:00
Manan Jethwani
ad7a63a471 minor change in UI 2021-07-08 12:33:28 +02:00
Manan Jethwani
6e8200637e corrected search button in mobile view 2021-07-08 12:33:28 +02:00
Manan Jethwani
cc45c840d1 fixed minor codefactor issue 2021-07-08 12:33:28 +02:00
Manan Jethwani
0590f27fa1 corrected select box and search bar design 2021-07-08 12:33:28 +02:00
Manan Jethwani
dd27c3a873 changed tile background color 2021-07-08 12:33:28 +02:00
Manan Jethwani
736841818d fixed font and other minor issues in title cards 2021-07-08 12:33:28 +02:00
Manan Jethwani
c1868e22f4 minor codefactor fix 2021-07-08 12:33:28 +02:00
Manan Jethwani
aabfc1d82e fixed card design 2021-07-08 12:33:28 +02:00
Manan Jethwani
2effb3490e minoor changes in responsive behaviour 2021-07-08 12:33:28 +02:00
Manan Jethwani
55672b0288 revamped basic layout and cards 2021-07-08 12:33:28 +02:00
Kelson
0abbeabfe2 Merge pull request #568 from kiwix/ppa-impish
PPA: Build for Ubuntu Impish
2021-07-08 10:24:52 +02:00
Kunal Mehta
1bf52e8ebe PPA: Build for Ubuntu Impish 2021-07-08 09:50:55 +02:00
Kelson
e2db1b3688 Merge pull request #574 from kiwix/remove_mustache_public_header
Fix public headers inclusion (+ small other fixes)
2021-07-07 18:00:17 +02:00
Matthieu Gautier
0b6b6716de Rename split argument from trimEmpty to dropEmpty. 2021-07-07 14:43:13 +02:00
Matthieu Gautier
18b6433322 Correct method declaration in SuggestionItem 2021-07-07 14:43:13 +02:00
Matthieu Gautier
b70c92cade Move back used helper functions to the public API.
- Add docstring
- Move the declaration in kiwix namespace.
- Adapt our include to include the right headers.
2021-07-07 14:43:13 +02:00
Matthieu Gautier
09d843da3a Add a (empty) include/tools.h header.
This header will contain our public tool functions.
2021-07-07 14:43:13 +02:00
Matthieu Gautier
fa83a61a54 Move all public *Tools.h in src.
This by definition remove all the tool functions from the public API.
2021-07-07 14:43:13 +02:00
Matthieu Gautier
967eb10cbf Merge pull request #578 from kiwix/fix_ci_deps
Use correct deps archive in the CI.
2021-07-07 10:59:53 +02:00
Matthieu Gautier
feeee25eac Use correct deps archive in the CI.
Now that project is named libkiwix, the dependencies archive is also
renamed.
2021-07-07 10:53:07 +02:00
Matthieu Gautier
1c0b4502cd Merge pull request #536 from kiwix/internally_drop_reader_searcher 2021-07-06 16:18:10 +02:00
Maneesh P M
6f639144ab Add unit tests for Searcher and Reader
Even though we will be removing the wrappers soon, the test coverage
should be complete and we could simply remove these files later.
2021-07-03 14:07:14 +05:30
Maneesh P M
a94a03cd22 Remove unwanted reader functions
Removing the functions in InternalServer that are no longer needed.
2021-07-03 14:07:14 +05:30
Maneesh P M
bc821638da Drop wrapper structures from handle_search
Since we now have SearcherRenderer that can work with native libzim
structure, we will drop the wrapper and use them instead.
2021-07-03 14:07:12 +05:30
Maneesh P M
bcece66960 Add SearchRenderer handles for libzim structures
Introduces a new member mp_search that houses the zim::Search object,
adds a new constructor for this purpose. This commit also add an
overload for getHtml that takes start and end integers as arguments
since they are not part of the search object we include.
2021-07-03 14:05:50 +05:30
Maneesh P M
c046f64d83 Drop Reader and Entry wrappers from handle_content 2021-07-03 14:05:50 +05:30
Maneesh P M
75b4d311d7 Drop Reader from InternalServer::handle_random 2021-07-03 14:04:04 +05:30
Maneesh P M
a236751c74 Drop usage of Reader from InternalServer::handle_suggest 2021-07-03 14:04:04 +05:30
Maneesh P M
7d68926539 Drop usage of Reader from InternalServer::handle_meta
This is essentially a code move of meta handlers from using Reader
functions to directly using Archive.
2021-07-03 14:04:02 +05:30
Maneesh P M
940368b8ac Add m_archives and getArchiveById to Library
These members will mirror the functionality offered by equivalent usage
of Reader class.
2021-07-03 14:02:31 +05:30
Kelson
0594e60df3 Merge pull request #527 from kiwix/catalog_search_url_generation
OpdsCatalog::getSearchUrl()
2021-06-30 21:35:43 +02:00
Veloman Yunkan
b5c1b26761 OpdsCatalog::getSearchUrl() 2021-06-30 18:27:00 +02:00
71 changed files with 7357 additions and 2775 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Install deps
shell: bash
run: |
ARCHIVE_NAME=deps2_osx_native_dyn_kiwix-lib.tar.xz
ARCHIVE_NAME=deps2_osx_native_dyn_libkiwix.tar.xz
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C $HOME
- name: Compile
shell: bash
@@ -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
@@ -115,7 +115,7 @@ jobs:
- name: Install deps
shell: bash
run: |
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_kiwix-lib.tar.xz
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_libkiwix.tar.xz
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
- name: Compile
shell: bash

View File

@@ -7,7 +7,11 @@ jobs:
strategy:
fail-fast: false
matrix:
distro: [ubuntu-hirsute, ubuntu-groovy, ubuntu-focal, ubuntu-bionic]
distro:
- ubuntu-impish
- ubuntu-hirsute
- ubuntu-focal
- ubuntu-bionic
steps:
- uses: actions/checkout@v2
@@ -30,18 +34,18 @@ jobs:
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
- uses: legoktm/gh-action-build-deb@ubuntu-hirsute
if: matrix.distro == 'ubuntu-hirsute'
name: Build package for ubuntu-hirsute
id: build-ubuntu-hirsute
- uses: legoktm/gh-action-build-deb@ubuntu-impish
if: matrix.distro == 'ubuntu-impish'
name: Build package for ubuntu-impish
id: build-ubuntu-impish
with:
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
- uses: legoktm/gh-action-build-deb@ubuntu-hirsute
if: matrix.distro == 'ubuntu-hirsute'
name: Build package for ubuntu-hirsute
id: build-ubuntu-hirsute
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}

View File

@@ -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.

View File

@@ -7,6 +7,7 @@ files=(
"include/common/otherTools.h"
"include/common/regexTools.h"
"include/common/networkTools.h"
"include/common/archiveTools.h"
"include/manager.h"
"include/reader.h"
"include/kiwix.h"
@@ -22,6 +23,7 @@ files=(
"src/common/pathTools.cpp"
"src/common/regexTools.cpp"
"src/common/otherTools.cpp"
"src/common/archiveTools.cpp"
"src/common/networkTools.cpp"
"src/common/stringTools.cpp"
"src/xapianSearcher.cpp"

View File

@@ -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;

View File

@@ -24,6 +24,7 @@
#include <vector>
#include <map>
#include <memory>
#include <zim/archive.h>
#include "book.h"
#include "bookmark.h"
@@ -146,12 +147,14 @@ class Library
{
std::map<std::string, kiwix::Book> m_books;
std::map<std::string, std::shared_ptr<Reader>> m_readers;
std::map<std::string, std::shared_ptr<zim::Archive>> m_archives;
std::vector<kiwix::Bookmark> m_bookmarks;
class BookDB;
std::unique_ptr<BookDB> m_bookDB;
public:
typedef std::vector<std::string> BookIdCollection;
typedef std::map<std::string, int> AttributeCounts;
public:
Library();
@@ -198,6 +201,7 @@ class Library
const Book& getBookByPath(const std::string& path) const;
Book& getBookByPath(const std::string& path);
std::shared_ptr<Reader> getReaderById(const std::string& id);
std::shared_ptr<zim::Archive> getArchiveById(const std::string& id);
/**
* Remove a book from the library.
@@ -239,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.
*
@@ -338,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);
};

View File

@@ -13,18 +13,8 @@ headers = [
'search_renderer.h',
'server.h',
'kiwixserve.h',
'name_mapper.h'
'name_mapper.h',
'tools.h'
]
install_headers(headers, subdir:'kiwix')
install_headers(
'tools/base64.h',
'tools/networkTools.h',
'tools/otherTools.h',
'tools/pathTools.h',
'tools/regexTools.h',
'tools/stringTools.h',
subdir:'kiwix/tools'
)

33
include/opds_catalog.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_OPDS_CATALOG_H
#define KIWIX_OPDS_CATALOG_H
#include "library.h"
namespace kiwix
{
std::string getSearchUrl(const Filter& f);
} // namespace kiwix
#endif // KIWIX_OPDS_CATALOG_H

View File

@@ -26,9 +26,6 @@
#include <pugixml.hpp>
#include "tools/base64.h"
#include "tools/pathTools.h"
#include "tools/regexTools.h"
#include "library.h"
#include "reader.h"
@@ -62,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.

View File

@@ -29,8 +29,6 @@
#include <string>
#include "common.h"
#include "entry.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"
using namespace std;
@@ -41,26 +39,26 @@ namespace kiwix
* The SuggestionItem is a helper class that contains the info about a single
* suggestion item.
*/
class SuggestionItem
{
// Functions
private:
// Temporarily making the constructor public until the code move is complete
public:
// Create a sugggestion item.
explicit SuggestionItem(std::string title, std::string normalizedTitle,
std::string path, std::string snippet = "") :
explicit SuggestionItem(const std::string& title, const std::string& normalizedTitle,
const std::string& path, const std::string& snippet = "") :
title(title),
normalizedTitle(normalizedTitle),
path(path),
snippet(snippet) {}
public:
const std::string getTitle() {return title;}
const std::string getNormalizedTitle() {return normalizedTitle;}
const std::string getPath() {return path;}
const std::string getSnippet() {return snippet;}
const std::string& getTitle() const { return title;}
const std::string& getNormalizedTitle() const { return normalizedTitle;}
const std::string& getPath() const { return path;}
const std::string& getSnippet() const { return snippet;}
const bool hasSnippet() {return !snippet.empty();}
bool hasSnippet() const { return !snippet.empty();}
// Data
private:
@@ -91,6 +89,13 @@ class Reader
* (.zim extesion).
*/
explicit Reader(const string zimFilePath);
/**
* Create a Reader to read a zim file given by the Archive.
*
* @param archive The shared pointer to the Archive object.
*/
explicit Reader(const std::shared_ptr<zim::Archive> archive);
#ifndef _WIN32
explicit Reader(int fd);
Reader(int fd, zim::offset_type offset, zim::size_type size);
@@ -287,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.
*
@@ -488,7 +483,7 @@ class Reader
zim::Archive* getZimArchive() const;
protected:
std::unique_ptr<zim::Archive> zimArchive;
std::shared_ptr<zim::Archive> zimArchive;
std::string zimFilePath;
SuggestionsList_t suggestions;

View File

@@ -21,6 +21,7 @@
#define KIWIX_SEARCH_RENDERER_H
#include <string>
#include <zim/search.h>
namespace kiwix
{
@@ -40,6 +41,8 @@ class SearchRenderer
* Used to generate pagination links.
*/
SearchRenderer(Searcher* searcher, NameMapper* mapper);
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount);
~SearchRenderer();
@@ -74,7 +77,7 @@ class SearchRenderer
protected:
std::string beautifyInteger(const unsigned int number);
Searcher* mp_searcher;
zim::SearchResultSet m_srs;
NameMapper* mp_nameMapper;
std::string searchContent;
std::string searchPattern;

View File

@@ -29,8 +29,8 @@
#include <string>
#include <memory>
#include <vector>
#include "tools/pathTools.h"
#include "tools/stringTools.h"
#include <zim/search.h>
using namespace std;
@@ -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);
/**
@@ -142,23 +143,29 @@ class Searcher
*/
unsigned int getEstimatedResultCount();
/**
* Get a SearchResultSet object for current search
*/
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();

View File

@@ -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;

198
include/tools.h Normal file
View File

@@ -0,0 +1,198 @@
/*
* Copyright 2021 Matthieu Gautier <mgautier@kymeria.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_TOOLS_H
#define KIWIX_TOOLS_H
#include <string>
#include <vector>
namespace kiwix {
/**
* Return the current directory.
*
* @return the current directory (utf8 encoded)
*/
std::string getCurrentDirectory();
/**
* Return the data directory.
*
* The data directory is a directory where to put data (zim files, ...)
* It depends of the platform and it may be changed by user using environment variable.
*
* The resolution order is :
* - `KIWIX_DATA_DIR` env variable (if set).
* - On Windows :
* . `$APPDATA/kiwix` if $APPDATA is set
* . `$USERPROFILE/kiwix` if $USERPROFILE is set
* - Else :
* . `$XDG_DATA_HOME/kiwix`if $XDG_DATA_HOME is set
* . `$HOME/.local/share/kiwx` if $HOWE is set
* - current directory
*
* @return the path of the data directory (utf8 encoded)
*/
std::string getDataDirectory();
/** Return the path of the executable
*
* Some application may be packaged in auto extractible archive (Appimage) and the
* real executable is different of the path of the archive.
* If `realPathOnly` is true, return the path of the real executable instead of the
* archive launched by the user.
*
* @param realPathOnly If we must return the real path of the executable.
* @return the path of the executable (utf8 encoded)
*/
std::string getExecutablePath(bool realPathOnly = false);
/** Tell if the path is a relative path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path A utf8 encoded path.
* @return true if the path is relative.
*/
bool isRelativePath(const std::string& path);
/** Append a path to another one.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param basePath the base path.
* @param relativePath a path to add to the base path, must be a relative path.
* @return The concatenation of the paths, using the right separator.
*/
std::string appendToDirectory(const std::string& basePath, const std::string& relativePath);
/** Remove the last element of a path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path a path.
* @return The parent directory (or empty string if none).
*/
std::string removeLastPathElement(const std::string& path);
/** Get the last element of a path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path a path.
* @return The base name of the path or empty string if none (ending with a separator).
*/
std::string getLastPathElement(const std::string& path);
/** Compute the absolute path of a relative path based on another one
*
* Equivalent to appendToDirectory followed by a normalization of the path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path the base path (if empty, current directory is taken).
* @param relativePath the relative path.
* @return a absolute path.
*/
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath);
/** Compute the relative path of a path relative to another one
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path the base path.
* @param absolutePath the absolute path to find the relative path for.
* @return a relative path (pointing to absolutePath, relative to path).
*/
std::string computeRelativePath(const std::string& path, const std::string& absolutePath);
/** Sleep the current thread.
*
* This function is provided as a small helper. It is probably better to use native tools.
*
* @param milliseconds The number of milliseconds to wait for.
*/
void sleep(unsigned int milliseconds);
/** Split a string
*
* This function is provided as a small helper. It is probably better to use native tools.
*
* Assuming text = "foo:;bar;baz,oups;"
*
* split(text, ":;", true, true) => ["foo", ":", ";", "bar", ";", "baz,oups", ";"]
* split(text, ":;", true, false) => ["foo", "bar", "baz,oups"] (default)
* split(text, ":;", false, true) => ["foo", ":", "", ";", "bar", ";", "baz,oups", ";", ""]
* split(text, ":;", false, false) => ["foo", "", "bar", "baz,oups", ""]
*
* @param str The string to split.
* @param delims A string of potential delimiters.
* Each charater in the string can be a individual delimiters.
* @param dropEmpty true if empty part must be dropped from the result.
* @param keepDelim true if delimiter must be included from the result.
* @return a list of part (potentially containing delimiters)
*/
std::vector<std::string> split(const std::string& str, const std::string& delims, bool dropEmpty=true, bool keepDelim = false);
/** Convert language code from iso2 code to iso3
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate locales.
*
* @param a2code a iso2 code string.
* @return the corresponding iso3 code.
* @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

View File

@@ -6,10 +6,10 @@
#include <sstream>
#include <thread>
#include <chrono>
#include <tools/otherTools.h>
#include <tools/pathTools.h>
#include <tools/stringTools.h>
#include <downloader.h> // For AriaError
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"
#include "downloader.h" // For AriaError
#ifdef _WIN32
# define ARIA2_CMD "aria2c.exe"

View File

@@ -20,10 +20,16 @@
#include "book.h"
#include "reader.h"
#include "tools.h"
#include "tools/base64.h"
#include "tools/regexTools.h"
#include "tools/networkTools.h"
#include "tools/otherTools.h"
#include "tools/stringTools.h"
#include "tools/pathTools.h"
#include "tools/archiveTools.h"
#include <zim/archive.h>
#include <pugixml.hpp>
@@ -77,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()

View File

@@ -9,6 +9,7 @@
# include <unistd.h>
#endif
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"

View File

@@ -22,6 +22,7 @@
#include "reader.h"
#include "libxml_dumper.h"
#include "tools.h"
#include "tools/base64.h"
#include "tools/regexTools.h"
#include "tools/pathTools.h"
@@ -108,6 +109,7 @@ bool Library::removeBookById(const std::string& id)
{
m_bookDB->delete_document("Q" + id);
m_readers.erase(id);
m_archives.erase(id);
return m_books.erase(id) == 1;
}
@@ -146,11 +148,35 @@ std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
return m_readers.at(id);
} catch (std::out_of_range& e) {}
try {
auto reader = make_shared<Reader>(m_archives.at(id));
m_readers[id] = reader;
return reader;
} catch (std::out_of_range& e) {}
auto book = getBookById(id);
if (!book.isPathValid())
return nullptr;
auto sptr = make_shared<Reader>(book.getPath());
m_readers[id] = sptr;
auto archive = make_shared<zim::Archive>(book.getPath());
m_archives[id] = archive;
auto reader = make_shared<Reader>(archive);
m_readers[id] = reader;
return reader;
}
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
{
try {
return m_archives.at(id);
} catch (std::out_of_range& e) {}
auto book = getBookById(id);
if (!book.isPathValid())
return nullptr;
auto sptr = make_shared<zim::Archive>(book.getPath());
m_archives[id] = sptr;
return sptr;
}
@@ -182,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
@@ -218,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

View File

@@ -20,10 +20,10 @@
#include "libxml_dumper.h"
#include "book.h"
#include "tools.h"
#include "tools/base64.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools/pathTools.h"
namespace kiwix
{

View File

@@ -19,6 +19,7 @@
#include "manager.h"
#include "tools.h"
#include "tools/pathTools.h"
#include <pugixml.hpp>
@@ -214,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);

View File

@@ -19,6 +19,7 @@ kiwix_sources = [
'tools/stringTools.cpp',
'tools/networkTools.cpp',
'tools/otherTools.cpp',
'tools/archiveTools.cpp',
'kiwixserve.cpp',
'name_mapper.cpp',
'server/byte_range.cpp',
@@ -26,7 +27,8 @@ kiwix_sources = [
'server/request_context.cpp',
'server/response.cpp',
'server/internalServer.cpp',
'server/internalServer_catalog_v2.cpp'
'server/internalServer_catalog_v2.cpp',
'opds_catalog.cpp'
]
kiwix_sources += lib_resources

74
src/opds_catalog.cpp Normal file
View File

@@ -0,0 +1,74 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#include "opds_catalog.h"
#include "tools/stringTools.h"
#include <sstream>
namespace kiwix
{
namespace
{
const char opdsSearchEndpoint[] = "/catalog/v2/entries";
enum Separator { AMP };
std::ostringstream& operator<<(std::ostringstream& oss, Separator sep)
{
if ( oss.tellp() > 0 )
oss << "&";
return oss;
}
std::string buildSearchString(const Filter& f)
{
std::ostringstream oss;
if ( f.hasQuery() )
oss << AMP << "q=" << urlEncode(f.getQuery());
if ( f.hasCategory() )
oss << AMP << "category=" << urlEncode(f.getCategory());
if ( f.hasLang() )
oss << AMP << "lang=" << urlEncode(f.getLang());
if ( f.hasName() )
oss << AMP << "name=" << urlEncode(f.getName());
if ( !f.getAcceptTags().empty() )
oss << AMP << "tag=" << urlEncode(join(f.getAcceptTags(), ";"));
return oss.str();
}
} // unnamed namespace
std::string getSearchUrl(const Filter& f)
{
const std::string searchString = buildSearchString(f);
if ( searchString.empty() )
return opdsSearchEndpoint;
else
return opdsSearchEndpoint + ("?" + searchString);
}
} // namespace kiwix

View File

@@ -20,10 +20,12 @@
#include "opds_dumper.h"
#include "book.h"
#include "tools/otherTools.h"
#include "kiwixlib-resources.h"
#include <mustache.hpp>
#include <unicode/locid.h>
#include "tools/stringTools.h"
#include "tools/otherTools.h"
namespace kiwix
{
@@ -49,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()},
@@ -76,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},
@@ -95,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},
@@ -145,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 }
}
);
}
}

View File

@@ -21,48 +21,14 @@
#include <time.h>
#include <zim/search.h>
#include <zim/suggestion.h>
#include <zim/item.h>
#include <zim/error.h>
#include "tools.h"
#include "tools/stringTools.h"
#include "tools/otherTools.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;
}
#include "tools/archiveTools.h"
namespace kiwix
{
@@ -86,6 +52,11 @@ Reader::Reader(const string zimFilePath)
srand(time(nullptr));
}
Reader::Reader(const std::shared_ptr<zim::Archive> archive)
: zimArchive(archive),
zimFilePath(archive->getFilename())
{}
#ifndef _WIN32
Reader::Reader(int fd)
: zimArchive(new zim::Archive(fd)),
@@ -111,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 */
@@ -138,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 */
@@ -162,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
@@ -183,14 +135,7 @@ Entry Reader::getMainPage() const
bool Reader::getFavicon(string& content, string& mimeType) const
{
try {
auto item = zimArchive->getIllustrationItem();
content = item.getData();
mimeType = item.getMimetype();
return true;
} catch(zim::EntryNotFound& e) {};
return false;
return kiwix::getArchiveFavicon(*zimArchive, 48, content, mimeType);
}
string Reader::getZimFilePath() const
@@ -212,47 +157,32 @@ bool Reader::getMetadata(const string& name, string& value) const
string Reader::getName() const
{
METADATA("Name")
return kiwix::getMetaName(*zimArchive);
}
string Reader::getTitle() const
{
string value = zimArchive->getMetadata("Title");
if (value.empty()) {
value = getLastPathElement(zimFilePath);
std::replace(value.begin(), value.end(), '_', ' ');
size_t pos = value.find(".zim");
value = value.substr(0, pos);
}
return value;
return kiwix::getArchiveTitle(*zimArchive);
}
string Reader::getCreator() const
{
METADATA("Creator")
return kiwix::getMetaCreator(*zimArchive);
}
string Reader::getPublisher() const
{
METADATA("Publisher")
return kiwix::getMetaPublisher(*zimArchive);
}
string Reader::getDate() const
{
METADATA("Date")
return kiwix::getMetaDate(*zimArchive);
}
string Reader::getDescription() const
{
string value;
this->getMetadata("Description", value);
/* Mediawiki Collection tends to use the "Subtitle" name */
if (value.empty()) {
this->getMetadata("Subtitle", value);
}
return value;
return kiwix::getMetaDescription(*zimArchive);
}
string Reader::getLongDescription() const
@@ -262,7 +192,7 @@ string Reader::getLongDescription() const
string Reader::getLanguage() const
{
METADATA("Language")
return kiwix::getMetaLanguage(*zimArchive);
}
string Reader::getLicense() const
@@ -272,13 +202,7 @@ string Reader::getLicense() const
string Reader::getTags(bool original) const
{
string tags_str;
getMetadata("Tags", tags_str);
if (original) {
return tags_str;
}
auto tags = convertTags(tags_str);
return join(tags, ";");
return kiwix::getMetaTags(*zimArchive, original);
}
@@ -301,7 +225,7 @@ string Reader::getRelation() const
string Reader::getFlavour() const
{
METADATA("Flavour")
return kiwix::getMetaFlavour(*zimArchive);
}
string Reader::getSource() const
@@ -315,39 +239,10 @@ 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
{
if (path.empty() || path == "/") {
return getMainPage();
}
try {
return zimArchive->getEntryByPath(path);
return kiwix::getEntryFromPath(*zimArchive, path);
} catch (zim::EntryNotFound& e) {
throw NoEntry();
}
@@ -460,12 +355,7 @@ bool Reader::searchSuggestions(const string& prefix,
std::vector<std::string> Reader::getTitleVariants(
const std::string& title) const
{
std::vector<std::string> variants;
variants.push_back(title);
variants.push_back(kiwix::ucFirst(title));
variants.push_back(kiwix::lcFirst(title));
variants.push_back(kiwix::toTitle(title));
return variants;
return kiwix::getTitleVariants(title);
}
@@ -488,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 */
@@ -575,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);
}
}

View File

@@ -30,17 +30,29 @@
#include <mustache.hpp>
#include "kiwixlib-resources.h"
#include "tools/stringTools.h"
namespace kiwix
{
/* Constructor */
SearchRenderer::SearchRenderer(Searcher* searcher, NameMapper* mapper)
: mp_searcher(searcher),
: m_srs(searcher->getSearchResultSet()),
mp_nameMapper(mapper),
protocolPrefix("zim://"),
searchProtocolPrefix("search://?")
searchProtocolPrefix("search://?"),
estimatedResultCount(searcher->getEstimatedResultCount()),
resultStart(searcher->getResultStart())
{}
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount)
: m_srs(srs),
mp_nameMapper(mapper),
protocolPrefix("zim://"),
searchProtocolPrefix("search://?"),
estimatedResultCount(estimatedResultCount),
resultStart(start)
{}
/* Destructor */
@@ -70,29 +82,26 @@ std::string SearchRenderer::getHtml()
{
kainjow::mustache::data results{kainjow::mustache::data::type::list};
mp_searcher->restart_search();
Result* p_result = NULL;
while ((p_result = mp_searcher->getNextResult())) {
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
kainjow::mustache::data result;
result.set("title", p_result->get_title());
result.set("url", p_result->get_url());
result.set("snippet", p_result->get_snippet());
result.set("resultContentId", mp_nameMapper->getNameForId(p_result->get_zimId()));
result.set("title", it.getTitle());
result.set("url", it.getPath());
result.set("snippet", it.getSnippet());
std::ostringstream s;
s << it.getZimId();
result.set("resultContentId", mp_nameMapper->getNameForId(s.str()));
if (p_result->get_wordCount() >= 0) {
result.set("wordCount", kiwix::beautifyInteger(p_result->get_wordCount()));
if (it.getWordCount() >= 0) {
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));
}
results.push_back(result);
delete p_result;
}
// pages
kainjow::mustache::data pages{kainjow::mustache::data::type::list};
auto resultStart = mp_searcher->getResultStart();
auto resultEnd = 0U;
auto estimatedResultCount = mp_searcher->getEstimatedResultCount();
auto currentPage = 0U;
auto pageStart = 0U;
auto pageEnd = 0U;
@@ -146,4 +155,4 @@ std::string SearchRenderer::getHtml()
return ss.str();
}
}
}

View File

@@ -18,14 +18,16 @@
*/
#include <cmath>
#include "searcher.h"
#include "reader.h"
#include <zim/search.h>
#include <zim/suggestion.h>
#include <mustache.hpp>
#include <cmath>
#include "tools/stringTools.h"
#include "kiwixlib-resources.h"
#define MAX_SEARCH_LEN 140
@@ -36,6 +38,7 @@ class _Result : public Result
{
public:
_Result(zim::SearchResultSet::iterator iterator);
_Result(SuggestionItem suggestionItem);
virtual ~_Result(){};
virtual std::string get_url();
@@ -49,6 +52,8 @@ class _Result : public Result
private:
zim::SearchResultSet::iterator iterator;
SuggestionItem suggestionItem;
bool isSuggestion;
};
struct SearcherInternal : zim::SearchResultSet {
@@ -61,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();
}
@@ -94,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();
@@ -105,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;
@@ -118,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();
}
@@ -132,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();
@@ -146,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;
}
@@ -159,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();
}
@@ -178,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;
}
@@ -205,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();
}
@@ -228,41 +246,75 @@ unsigned int Searcher::getEstimatedResultCount()
return this->estimatedResultCount;
}
_Result::_Result(zim::SearchResultSet::iterator iterator)
: iterator(iterator)
zim::SearchResultSet Searcher::getSearchResultSet()
{
return *(this->internal);
}
_Result::_Result(zim::SearchResultSet::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();

View File

@@ -48,7 +48,8 @@ bool Server::start() {
m_verbose,
m_withTaskbar,
m_withLibraryButton,
m_blockExternalLinks));
m_blockExternalLinks,
m_indexTemplateString));
return mp_server->start();
}

View File

@@ -43,10 +43,11 @@ extern "C" {
#include "microhttpd_wrapper.h"
}
#include "tools/otherTools.h"
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/regexTools.h"
#include "tools/stringTools.h"
#include "tools/archiveTools.h"
#include "library.h"
#include "name_mapper.h"
#include "entry.h"
@@ -55,6 +56,11 @@ extern "C" {
#include "opds_dumper.h"
#include <zim/uuid.h>
#include <zim/error.h>
#include <zim/search.h>
#include <zim/suggestion.h>
#include <zim/entry.h>
#include <zim/item.h>
#include <mustache.hpp>
@@ -89,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;
@@ -112,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)),
@@ -121,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)
@@ -320,25 +338,66 @@ 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);
}
/**
* Archive and Zim handlers begin
**/
// TODO: retrieve searcher from caching mechanism
SuggestionsList_t getSuggestions(const zim::Archive* const archive,
const std::string& queryString, int start, int suggestionCount)
{
SuggestionsList_t suggestions;
auto searcher = zim::SuggestionSearcher(*archive);
if (archive->hasTitleIndex()) {
auto search = searcher.suggest(queryString);
auto srs = search.getResults(start, suggestionCount);
for (auto it : srs) {
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
it.getPath(), it.getSnippet());
suggestions.push_back(suggestion);
}
} else {
// TODO: This case should be handled by libzim
std::vector<std::string> variants = getTitleVariants(queryString);
int currCount = 0;
for (auto it = variants.begin(); it != variants.end() && currCount < suggestionCount; it++) {
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++;
}
}
}
return suggestions;
}
/**
* Archive and Zim handlers end
**/
std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request)
{
std::string bookName;
std::string bookId;
std::string meta_name;
std::shared_ptr<Reader> reader;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
bookId = mp_nameMapper->getIdForName(bookName);
meta_name = request.get_argument("name");
reader = mp_library->getReaderById(bookId);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range& e) {
return Response::build_404(*this, request, bookName, "");
}
if (reader == nullptr) {
if (archive == nullptr) {
return Response::build_404(*this, request, bookName, "");
}
@@ -346,23 +405,25 @@ std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& requ
std::string mimeType = "text";
if (meta_name == "title") {
content = reader->getTitle();
content = getArchiveTitle(*archive);
} else if (meta_name == "description") {
content = reader->getDescription();
content = getMetaDescription(*archive);
} else if (meta_name == "language") {
content = reader->getLanguage();
content = getMetaLanguage(*archive);
} else if (meta_name == "name") {
content = reader->getName();
content = getMetaName(*archive);
} else if (meta_name == "tags") {
content = reader->getTags();
content = getMetaTags(*archive);
} else if (meta_name == "date") {
content = reader->getDate();
content = getMetaDate(*archive);
} else if (meta_name == "creator") {
content = reader->getCreator();
content = getMetaCreator(*archive);
} else if (meta_name == "publisher") {
content = reader->getPublisher();
content = getMetaPublisher(*archive);
} else if (meta_name == "favicon") {
reader->getFavicon(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, "");
}
@@ -380,56 +441,70 @@ 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;
std::string term;
std::shared_ptr<Reader> reader;
std::string queryString;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
bookId = mp_nameMapper->getIdForName(bookName);
term = request.get_argument("term");
reader = mp_library->getReaderById(bookId);
queryString = request.get_argument("term");
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {
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, "");
}
if (m_verbose.load()) {
printf("Searching suggestions for: \"%s\"\n", term.c_str());
printf("Searching suggestions for: \"%s\"\n", queryString.c_str());
}
MustacheData results{MustacheData::type::list};
bool first = true;
if (reader != nullptr) {
/* Get the suggestions */
SuggestionsList_t suggestions;
reader->searchSuggestionsSmart(term, maxSuggestionCount, suggestions);
for(auto& suggestion:suggestions) {
MustacheData result;
result.set("label", suggestion.getTitle());
if (suggestion.hasSnippet()) {
result.set("label", suggestion.getSnippet());
}
/* Get the suggestions */
SuggestionsList_t suggestions = getSuggestions(archive.get(), queryString, start, count);
for(auto& suggestion:suggestions) {
MustacheData result;
result.set("label", suggestion.getTitle());
result.set("value", suggestion.getTitle());
result.set("kind", "path");
result.set("path", suggestion.getPath());
result.set("first", first);
first = false;
results.push_back(result);
suggestionCount++;
if (suggestion.hasSnippet()) {
result.set("label", suggestion.getSnippet());
}
result.set("value", suggestion.getTitle());
result.set("kind", "path");
result.set("path", suggestion.getPath());
result.set("first", first);
first = false;
results.push_back(result);
}
/* Propose the fulltext search if possible */
if (reader->hasFulltextIndex()) {
if (archive->hasFulltextIndex()) {
MustacheData result;
result.set("label", "containing '" + term + "'...");
result.set("value", term + " ");
result.set("label", "containing '" + queryString + "'...");
result.set("value", queryString + " ");
result.set("kind", "pattern");
result.set("first", first);
results.push_back(result);
@@ -492,30 +567,34 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
} catch(const std::out_of_range&) {}
catch(const std::invalid_argument&) {}
std::shared_ptr<Reader> reader(nullptr);
std::shared_ptr<zim::Archive> archive;
try {
reader = mp_library->getReaderById(bookId);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {}
/* Make the search */
if ( (!reader && !bookName.empty())
if ( (!archive && !bookName.empty())
|| (patternString.empty() && ! has_geo_query) ) {
auto data = get_default_data();
data.set("pattern", encodeDiples(patternString));
auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8");
response->set_taskbar(bookName, reader ? reader->getTitle() : "");
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
response->set_code(MHD_HTTP_NOT_FOUND);
return std::move(response);
}
Searcher searcher;
if (reader) {
searcher.add_reader(reader.get());
std::shared_ptr<zim::Searcher> searcher;
if (archive) {
searcher = std::make_shared<zim::Searcher>(*archive);
} else {
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
auto currentReader = mp_library->getReaderById(bookId);
if (currentReader) {
searcher.add_reader(currentReader.get());
auto currentArchive = mp_library->getArchiveById(bookId);
if (currentArchive) {
if (! searcher) {
searcher = std::make_shared<zim::Searcher>(*currentArchive);
} else {
searcher->addArchive(*currentArchive);
}
}
}
}
@@ -536,25 +615,37 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
pageLength = 25;
}
auto end = start + pageLength;
/* Get the results */
try {
zim::Query query;
if (patternString.empty()) {
searcher.geo_search(latitude, longitude, distance,
start, end, m_verbose.load());
// Execute geo-search
if (m_verbose.load()) {
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
}
query.setQuery("");
query.setGeorange(latitude, longitude, distance);
} else {
searcher.search(patternString,
start, end, m_verbose.load());
// Execute Ft search
if (m_verbose.load()) {
cout << "Performing query `" << patternString << "'" << endl;
}
std::string queryString = removeAccents(patternString);
query.setQuery(queryString);
}
SearchRenderer renderer(&searcher, mp_nameMapper);
zim::Search search = searcher->search(query);
SearchRenderer renderer(search.getResults(start, pageLength), mp_nameMapper, start,
search.getEstimatedMatches());
renderer.setSearchPattern(patternString);
renderer.setSearchContent(bookName);
renderer.setProtocolPrefix(m_root + "/");
renderer.setSearchProtocolPrefix(m_root + "/search?");
renderer.setPageLength(pageLength);
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
response->set_taskbar(bookName, reader ? reader->getTitle() : "");
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
return std::move(response);
} catch (const std::exception& e) {
@@ -571,23 +662,23 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
std::string bookName;
std::string bookId;
std::shared_ptr<Reader> reader;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
bookId = mp_nameMapper->getIdForName(bookName);
reader = mp_library->getReaderById(bookId);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request, bookName, "");
}
if (reader == nullptr) {
if (archive == nullptr) {
return Response::build_404(*this, request, bookName, "");
}
try {
auto entry = reader->getRandomPage();
return build_redirect(bookName, entry.getFinalEntry());
} catch(kiwix::NoEntry& e) {
auto entry = archive->getRandomEntry();
return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) {
return Response::build_404(*this, request, bookName, "");
}
}
@@ -734,22 +825,10 @@ std::string searchSuggestionHTML(const std::string& searchURL, const std::string
} // unnamed namespace
std::shared_ptr<Reader>
InternalServer::get_reader(const std::string& bookName) const
{
std::shared_ptr<Reader> reader;
try {
const std::string bookId = mp_nameMapper->getIdForName(bookName);
reader = mp_library->getReaderById(bookId);
} catch (const std::out_of_range& e) {
}
return reader;
}
std::unique_ptr<Response>
InternalServer::build_redirect(const std::string& bookName, const kiwix::Entry& entry) const
InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const
{
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(entry.getPath());
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(item.getPath());
return Response::build_redirect(*this, redirectUrl);
}
@@ -765,8 +844,13 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (bookName.empty())
return build_homepage(request);
const std::shared_ptr<Reader> reader = get_reader(bookName);
if (reader == nullptr) {
std::shared_ptr<zim::Archive> archive;
try {
const std::string bookId = mp_nameMapper->getIdForName(bookName);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
std::string searchURL = m_root+"/search?pattern="+pattern; // Make a full search on the entire library.
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
@@ -779,31 +863,31 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
}
try {
auto entry = reader->getEntryFromPath(urlStr);
auto entry = getEntryFromPath(*archive, urlStr);
if (entry.isRedirect() || urlStr.empty()) {
// If urlStr is empty, we want to mainPage.
// We must do a redirection to the real page.
return build_redirect(bookName, entry.getFinalEntry());
return build_redirect(bookName, getFinalItem(*archive, entry));
}
auto response = ItemResponse::build(*this, request, entry.getZimEntry().getItem());
auto response = ItemResponse::build(*this, request, entry.getItem());
try {
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, reader->getTitle());
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, getArchiveTitle(*archive));
} catch (std::bad_cast& e) {}
if (m_verbose.load()) {
printf("Found %s\n", entry.getPath().c_str());
printf("mimeType: %s\n", entry.getMimetype().c_str());
printf("mimeType: %s\n", entry.getItem(true).getMimetype().c_str());
}
return response;
} catch(kiwix::NoEntry& e) {
} catch(zim::EntryNotFound& e) {
if (m_verbose.load())
printf("Failed to find %s\n", urlStr.c_str());
std::string searchURL = m_root+"/search?content="+bookName+"&pattern="+pattern; // Make a search on this specific book only.
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
return Response::build_404(*this, request, bookName, reader->getTitle(), details);
return Response::build_404(*this, request, bookName, getArchiveTitle(*archive), details);
}
}

View File

@@ -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,
@@ -69,14 +70,16 @@ class InternalServer {
private: // functions
std::unique_ptr<Response> handle_request(const RequestContext& request);
std::unique_ptr<Response> build_redirect(const std::string& bookName, const kiwix::Entry& entry) const;
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
std::unique_ptr<Response> build_homepage(const RequestContext& request);
std::unique_ptr<Response> handle_skin(const RequestContext& request);
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);
@@ -89,7 +92,6 @@ class InternalServer {
MustacheData get_default_data() const;
std::shared_ptr<Reader> get_reader(const std::string& bookName) const;
bool etag_not_needed(const RequestContext& r) const;
ETag get_matching_if_none_match_etag(const RequestContext& request) const;
@@ -102,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;

View File

@@ -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"
);
}

158
src/tools/archiveTools.cpp Normal file
View File

@@ -0,0 +1,158 @@
/*
* Copyright 2021 Maneesh P M <manu.pm55@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#include "archiveTools.h"
#include "tools.h"
#include "pathTools.h"
#include "otherTools.h"
#include "stringTools.h"
#include <zim/error.h>
#include <zim/item.h>
namespace kiwix
{
std::string getMetadata(const zim::Archive& archive, const std::string& name) {
try {
return archive.getMetadata(name);
} catch (zim::EntryNotFound& e) {
return "";
}
}
std::string getArchiveTitle(const zim::Archive& archive) {
std::string value = getMetadata(archive, "Title");
if (value.empty()) {
value = getLastPathElement(archive.getFilename());
std::replace(value.begin(), value.end(), '_', ' ');
size_t pos = value.find(".zim");
value = value.substr(0, pos);
}
return value;
}
std::string getMetaDescription(const zim::Archive& archive) {
std::string value;
value = getMetadata(archive, "Description");
/* Mediawiki Collection tends to use the "Subtitle" name */
if (value.empty()) {
value = getMetadata(archive, "Subtitle");
}
return value;
}
std::string getMetaTags(const zim::Archive& archive, bool original) {
std::string tags_str = getMetadata(archive, "Tags");
if (original) {
return tags_str;
}
auto tags = convertTags(tags_str);
return join(tags, ";");
}
std::string getMetaLanguage(const zim::Archive& archive) {
return getMetadata(archive, "Language");
}
std::string getMetaName(const zim::Archive& archive) {
return getMetadata(archive, "Name");
}
std::string getMetaDate(const zim::Archive& archive) {
return getMetadata(archive, "Date");
}
std::string getMetaCreator(const zim::Archive& archive) {
return getMetadata(archive, "Creator");
}
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);
}
zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path)
{
try {
return archive.getEntryByPath(path);
} catch (zim::EntryNotFound& e) {
if (path.empty() || path == "/") {
return archive.getMainEntry();
}
}
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

59
src/tools/archiveTools.h Normal file
View File

@@ -0,0 +1,59 @@
/*
* Copyright 2021 Maneesh P M <manu.pm55@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_ARCHIVETOOLS_H
#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
* an archive easier.
**/
namespace kiwix
{
std::string getMetadata(const zim::Archive& archive, const std::string& name);
std::string getArchiveTitle(const zim::Archive& archive);
std::string getMetaDescription(const zim::Archive& archive);
std::string getMetaTags(const zim::Archive& archive, bool original = false);
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

View File

@@ -17,7 +17,11 @@
* MA 02110-1301, USA.
*/
// Implement function declared in tools.h and tools/otherTools.h
#include "tools.h"
#include "tools/otherTools.h"
#include <algorithm>
#include <iomanip>

View File

@@ -32,9 +32,7 @@ namespace pugi {
namespace kiwix
{
void sleep(unsigned int milliseconds);
std::string nodeToString(const pugi::xml_node& node);
std::string converta2toa3(const std::string& a2code);
/*
* Convert all format tag string to new format

View File

@@ -17,7 +17,10 @@
* MA 02110-1301, USA.
*/
// Implement method defined in <kiwix/tools.h> and "tools/pathTools.h"
#include "tools.h"
#include "tools/pathTools.h"
#include <stdexcept>
#ifdef __APPLE__
@@ -59,7 +62,6 @@
#define PATH_MAX 1024
#endif
#ifdef _WIN32
std::string WideToUtf8(const std::wstring& wstr)
{
@@ -78,7 +80,7 @@ std::wstring Utf8ToWide(const std::string& str)
}
#endif
bool isRelativePath(const std::string& path)
bool kiwix::isRelativePath(const std::string& path)
{
#ifdef _WIN32
if (path.size() < 3 ) {
@@ -173,7 +175,7 @@ std::vector<std::string> normalizeParts(std::vector<std::string>& parts, bool ab
return ret;
}
std::string computeRelativePath(const std::string& path, const std::string& absolutePath)
std::string kiwix::computeRelativePath(const std::string& path, const std::string& absolutePath)
{
auto parts = kiwix::split(path, SEPARATOR, false);
auto pathParts = normalizeParts(parts, false);
@@ -198,11 +200,11 @@ std::string computeRelativePath(const std::string& path, const std::string& abso
return ret;
}
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath)
std::string kiwix::computeAbsolutePath(const std::string& path, const std::string& relativePath)
{
std::string absolutePath = path;
if (path.empty()) {
absolutePath = getCurrentDirectory();
absolutePath = kiwix::getCurrentDirectory();
}
auto parts = kiwix::split(absolutePath, SEPARATOR, false);
@@ -215,7 +217,7 @@ std::string computeAbsolutePath(const std::string& path, const std::string& rela
return ret;
}
std::string removeLastPathElement(const std::string& path)
std::string kiwix::removeLastPathElement(const std::string& path)
{
auto parts_ = kiwix::split(path, SEPARATOR, false);
auto parts = normalizeParts(parts_, false);
@@ -226,7 +228,7 @@ std::string removeLastPathElement(const std::string& path)
return ret;
}
std::string appendToDirectory(const std::string& directoryPath, const std::string& filename)
std::string kiwix::appendToDirectory(const std::string& directoryPath, const std::string& filename)
{
std::string newPath = directoryPath;
if (!directoryPath.empty() && directoryPath.back() != SEPARATOR[0]) {
@@ -236,7 +238,7 @@ std::string appendToDirectory(const std::string& directoryPath, const std::strin
return newPath;
}
std::string getLastPathElement(const std::string& path)
std::string kiwix::getLastPathElement(const std::string& path)
{
auto parts_ = kiwix::split(path, SEPARATOR);
auto parts = normalizeParts(parts_, false);
@@ -267,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);
@@ -300,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());
@@ -366,7 +368,7 @@ bool copyFile(const std::string& sourcePath, const std::string& destPath)
#endif
}
std::string getExecutablePath(bool realPathOnly)
std::string kiwix::getExecutablePath(bool realPathOnly)
{
if (!realPathOnly) {
char* cAppImage = ::getenv("APPIMAGE");
@@ -420,7 +422,7 @@ bool writeTextFile(const std::string& path, const std::string& content)
return true;
}
std::string getCurrentDirectory()
std::string kiwix::getCurrentDirectory()
{
#ifdef _WIN32
wchar_t* a_cwd = _wgetcwd(NULL, 0);
@@ -434,7 +436,7 @@ std::string getCurrentDirectory()
return ret;
}
std::string getDataDirectory()
std::string kiwix::getDataDirectory()
{
// Try to get the dataDir from the `KIWIX_DATA_DIR` env var
#ifdef _WIN32
@@ -503,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(".");
@@ -524,4 +526,3 @@ std::string getMimeTypeForFile(const std::string& filename)
return mimeType;
}

View File

@@ -26,23 +26,13 @@
std::string WideToUtf8(const std::wstring& wstr);
std::wstring Utf8ToWide(const std::string& str);
#endif
bool isRelativePath(const std::string& path);
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath);
std::string computeRelativePath(const std::string& path, const std::string& absolutePath);
std::string removeLastPathElement(const std::string& path);
std::string appendToDirectory(const std::string& directoryPath, const std::string& filename);
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);
std::string getLastPathElement(const std::string& path);
std::string getExecutablePath(bool realPathOnly = false);
std::string getCurrentDirectory();
std::string getDataDirectory();
bool writeTextFile(const std::string& path, const std::string& content);
std::string getMimeTypeForFile(const std::string& filename);
#endif

View File

@@ -17,9 +17,11 @@
* MA 02110-1301, USA.
*/
#include <tools/stringTools.h>
// Implement function declared in tools.h and tools/stringTools.h
#include "tools.h"
#include "tools/stringTools.h"
#include <tools/pathTools.h>
#include "tools/pathTools.h"
#include <unicode/normlzr.h>
#include <unicode/rep.h>
#include <unicode/translit.h>
@@ -268,7 +270,7 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
/* Split string in a token array */
std::vector<std::string> kiwix::split(const std::string& str,
const std::string& delims,
bool trimEmpty,
bool dropEmpty,
bool keepDelim)
{
std::string::size_type lastPos = 0;
@@ -277,7 +279,7 @@ std::vector<std::string> kiwix::split(const std::string& str,
while( (pos = str.find_first_of(delims, lastPos)) < str.length() )
{
auto token = str.substr(lastPos, pos - lastPos);
if (!trimEmpty || !token.empty()) {
if (!dropEmpty || !token.empty()) {
tokens.push_back(token);
}
if (keepDelim) {
@@ -287,7 +289,7 @@ std::vector<std::string> kiwix::split(const std::string& str,
}
auto token = str.substr(lastPos);
if (!trimEmpty || !token.empty()) {
if (!dropEmpty || !token.empty()) {
tokens.push_back(token);
}
return tokens;
@@ -395,3 +397,11 @@ bool kiwix::startsWith(const std::string& base, const std::string& start)
&& std::equal(start.begin(), start.end(), base.begin());
}
std::vector<std::string> kiwix::getTitleVariants(const std::string& title) {
std::vector<std::string> variants;
variants.push_back(title);
variants.push_back(kiwix::ucFirst(title));
variants.push_back(kiwix::lcFirst(title));
variants.push_back(kiwix::toTitle(title));
return variants;
}

View File

@@ -43,7 +43,6 @@ void loadICUExternalTables();
std::string urlEncode(const std::string& value, bool encodeReserved = false);
std::string urlDecode(const std::string& value, bool component = false);
std::vector<std::string> split(const std::string& str, const std::string& delims, bool trimEmpty = true, bool keepDelim = false);
std::string join(const std::vector<std::string>& list, const std::string& sep);
std::string ucAll(const std::string& word);
@@ -70,5 +69,7 @@ T extractFromString(const std::string& str) {
}
bool startsWith(const std::string& base, const std::string& start);
std::vector<std::string> getTitleVariants(const std::string& title);
} //namespace kiwix
#endif

View File

@@ -18,13 +18,19 @@ skin/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png
skin/jquery-ui/jquery-ui.theme.min.css
skin/jquery-ui/jquery-ui.min.css
skin/caret.png
skin/bittorrent.png
skin/magnet.png
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
skin/taskbar.css
skin/home.css
skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf
skin/block_external.js
templates/search_result.html
templates/no_search_result.html
@@ -39,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

BIN
static/skin/bittorrent.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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"
}

BIN
static/skin/download.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

View File

Binary file not shown.

View File

Binary file not shown.

BIN
static/skin/hash.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

456
static/skin/home.css Normal file
View File

@@ -0,0 +1,456 @@
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
font-size: 62.5%;
}
body {
position: relative;
box-sizing: border-box;
}
::selection {
background-color: #00b4e4;
color: white;
}
.kiwixNav {
background-color: #f4f6f8;
width: 100%;
padding: 20px 11vw 25px;
height: 140px;
}
.kiwixHomeBody__results {
font-size: 1.6rem;
color: #333333;
font-family: poppins;
margin: 10px 0 -5px;
position: relative;
top: -10px;
left: 13px;
}
.kiwixNav__filters {
display: grid;
grid-gap: 20px;
grid-template-columns: 231px 231px;
margin: 10px 0;
}
.kiwixNav__kiwixFilter {
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
outline: 0;
box-shadow: none;
border: 0 !important;
background: white;
background-image: none;
border-radius: 1px;
width: 195px;
height: 35px;
flex: 1;
color: black;
padding: 7px 10px 10px;
cursor: pointer;
}
.kiwixNav__kiwixFilter:-ms-expand {
display: none;
}
.kiwixNav__select {
position: relative;
display: flex;
width: 231px;
height: 35px;
line-height: 3;
background: #909090;
overflow: hidden;
border-radius: 10px;
border: 1px solid #b5b2b2;
}
.kiwixNav__select::after {
content: '\25BC';
color: #fff;
background-color: #909090;
height: 100%;
position: absolute;
top: 0;
right: 0;
padding: 0 1em;
cursor: pointer;
pointer-events: none;
-webkit-transition: .25s all ease;
-o-transition: .25s all ease;
transition: .25s all ease;
}
.kiwixNav__kiwixFilter option {
background-color: #fff;
color: #000;
}
.kiwixSearch {
height: 35px;
width: 229px;
border-radius: 10px;
border: solid 1px #b5b2b2;
padding: 10px;
background-image: url('./search-icon.svg');
background-repeat: no-repeat;
background-position: right center;
background-origin: content-box;
background-size: 15px;
}
.searchButton {
margin: 0 17px;
height: 35px;
width: 100px;
border-radius: 10px;
color: white;
background-color: #909090;
border: solid 1px #b5b2b2;
}
.searchButton:hover {
background-color: #858585;
}
.kiwixHomeBody {
padding: 20px 15vw;
min-height: calc(100vh - 170px);
position: relative;
}
.book__list {
position: relative;
}
.book {
border-color: #acacac;
}
.book__wrapper {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
display: grid;
grid-template-columns: 65px 1fr;
grid-template-rows: 70px 120px 1fr 1fr;
grid-gap: 5px;
transition: transform 0.25s;
}
.book__wrapper:hover {
transform: scale(1.07);
}
.book__icon {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
margin: 10px 0 0 10px;
}
.book__icon--image {
max-width: 100%;
max-height: 100%;
}
.book__title {
display: grid;
font-family: poppins;
color: black;
padding: 12px 10px 0 2px;
width: 100%;
height: 100%;
align-items: center;
align-content: center;
}
#bookSize {
font-size: 1.1rem;
margin: 3px 0;
}
#bookTitle {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.45rem;
}
.book__description {
grid-column: 1 / 3;
margin: 10px 10px 5px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 7;
-webkit-box-orient: vertical;
font-family: roboto;
font-weight: 300;
font-size: 1.2rem;
color: #4d4d4d;
line-height: 1.25;
}
.book__languageTag {
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 2px #acacac;
background-color: #ffffff;
font-weight: bold;
font-family: roboto;
color: black;
height: 25px;
width: 25px;
margin: 10px auto 0 10px;
border-radius: 5px;
font-size: 0.85rem;
}
.book__tags {
display: flex;
text-align: end;
font-size: 1.1rem;
justify-content: flex-end;
color: #909090;
font-family: roboto;
margin-right: 10px;
margin-top: 10px;
overflow: hidden;
}
.book__tags--wrapper {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.book__links {
display: flex;
text-align: end;
justify-content: flex-end;
grid-column: 2 / 3;
font-size: 1.1rem;
margin: 10px 4px;
color: #00b4e4;
}
.book__links > a, .book__links > span {
cursor: pointer;
text-decoration: none;
color: #00b4e4;
position: relative;
padding: 1px 6px 0;
font-family: roboto;
}
.book__links > a:hover, .book__links > span:hover {
background: #00b4e4;
color: white;
}
.modal-wrapper {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
background-color: rgba(0, 0, 0, 30%);
}
.modal {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
}
.modal-heading {
background-color: #f0f0f0;
height: 20%;
width: 100%;
border-bottom: 1px solid #ececec;
display: grid;
grid-template-columns: 3fr 1fr;
}
.modal-title {
display: flex;
font-size: 15px;
align-items: center;
padding-left: 20px;
font-family: poppins;
}
.modal-close-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
padding: 20px;
}
.modal-content div {
width: 100%;
height: 40px;
}
.modal-content a {
text-decoration: none;
}
.modal-content a>div {
position: relative;
top: -28px;
left: 50px;
text-decoration: underline;
font-size: 13px;
font-family: poppins;
color: #444343;
}
.modal-content img {
display: inline-block;
margin: 0 10px;
height: 30px;
}
.fadeOut {
position: fixed;
display: none;
bottom: 0;
left: 0;
z-index: 1;
background: linear-gradient(180deg, rgba(232, 232, 232, 0) 0%, #ffffff 100%);
height: 80px;
width: 100%;
}
.noResults {
font-size: 1.6rem;
text-align: center;
}
.noResults > a {
color: #3498db;
}
.loader-spinner {
position: absolute;
top: -50%;
left: 50%;
border: 5px solid #f3f3f3;
border-radius: 50%;
border-top: 5px solid #3498db;
width: 40px;
height: 40px;
margin: auto;
-webkit-animation: spin 1s linear infinite; /* Safari */
animation: spin 1s linear infinite;
margin-top: 35px;
margin-bottom: -35px;
z-index: 2;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loader {
display: none;
position: relative;
height: 70px;
width: 100%;
}
.kiwixfooter {
display: flex;
width: 100%;
justify-content: center;
text-align: center;
font-size: 1.6rem;
height: 30px;
}
.kiwixfooter > a {
width: auto;
}
@media screen and (max-width: 1100px) {
.kiwixHomeBody {
padding: 20px 10vw;
min-height: calc(100vh - 170px);
}
.kiwixNav__filters {
grid-template-columns: repeat(4, 1fr);
}
}
@media screen and (max-width: 590px) {
.kiwixNav {
height: 257px;
}
.kiwixHomeBody {
min-height: calc(100vh - 287px);
}
.kiwixSearch {
margin-top: 11px;
}
.searchButton {
margin: 19px 0;
width: 229px;
}
.kiwixNav__filters {
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 340px) {
.kiwixHomeBody {
padding: 20px 5vw;
}
}

View File

@@ -4,10 +4,10 @@
start: 0,
count: viewPortToCount()
};
const filterTypes = ['lang', 'category', 'q'];
const bookOrderMap = new Map();
const filterCookieName = 'filters';
const oneDayDelta = 86400000;
let loader;
let footer;
let fadeOutDiv;
let iso;
@@ -41,12 +41,29 @@
return result;
}
const humanFriendlySize = (fileSize) => {
if (fileSize === 0) {
return '';
}
const units = ['bytes', 'kB', 'MB', 'GB', 'TB'];
let quotient = Math.floor(Math.log10(fileSize) / 3);
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/100 + 1)*(window.innerWidth>1000 ? 3 : 2);
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) {
@@ -54,27 +71,48 @@
}
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 articleCount = getInnerHtml(book, 'articleCount');
const mediaCount = getInnerHtml(book, 'mediaCount');
const linkTag = document.createElement('a');
linkTag.setAttribute('class', 'book');
linkTag.setAttribute('data-id', id);
linkTag.setAttribute('href', link);
if (sort) {
linkTag.setAttribute('data-idx', bookOrderMap.get(id));
const language = getInnerHtml(book, 'language');
const tags = getInnerHtml(book, 'tags');
let tagHtml = tags.split(';').filter(tag => {return !(tag.split(':')[0].startsWith('_'))})
.map((tag) => {return tag.charAt(0).toUpperCase() + tag.slice(1)})
.join(' | ').replace(/_/g, ' ');
let downloadLink;
let zimSize = 0;
try {
const downloadBookLink = book.querySelector('link[type="application/x-zim"]')
zimSize = parseInt(downloadBookLink.getAttribute('length'));
downloadLink = downloadBookLink.getAttribute('href').split('.meta4')[0];
} catch {
downloadLink = '';
}
linkTag.innerHTML = `<div class='book__background' style="background-image: url('${iconUrl}');">
<div class='book__title' title='${title}'>${title}</div>
const humanFriendlyZimSize = humanFriendlySize(zimSize);
const divTag = document.createElement('div');
divTag.setAttribute('class', 'book');
divTag.setAttribute('data-id', id);
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='${root}${iconUrl}'></div>
<div class='book__title' title='${title}'>
<div id="bookTitle">${title}</div>
${humanFriendlyZimSize ? `<div id='bookSize'>${humanFriendlyZimSize}</div>`: ''}
</div>
<div class='book__description' title='${description}'>${description}</div>
<div class='book__info'>${articleCount} articles, ${mediaCount} medias</div>
</div>`;
return linkTag;
<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="${root}${link}" data-hover="Preview">Preview</a>${downloadLink ? `&nbsp;|&nbsp;<span class="download" data-link=${downloadLink} class="modal-button">Download</span>` : ''} </div></div>`;
return divTag;
}
function toggleFooter(show=false) {
@@ -86,8 +124,71 @@
}
}
function insertModal(button) {
const downloadLink = button.getAttribute('data-link');
button.addEventListener('click', () => {
document.body.insertAdjacentHTML('beforeend', `<div class="modal-wrapper">
<div class="modal">
<div class="modal-heading">
<div class="modal-title">
<div>
Download
</div>
</div>
<div onclick="closeModal()" class="modal-close-button">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
</svg>
</div>
</div>
</div>
<div class="modal-content">
<div class="modal-regular-download">
<a href="${downloadLink}" download>
<img src="../skin/download.png" alt="direct download" />
<div>Direct</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.sha256" download>
<img src="../skin/hash.png" alt="download hash" />
<div>Sha256 hash</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.magnet" target="_blank">
<img src="../skin/magnet.png" alt="download magnet" />
<div>Magnet link</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.torrent" download>
<img src="../skin/bittorrent.png" alt="download torrent" />
<div>Torrent file</div>
</a>
</div>
</div>
</div>
</div>`);
})
}
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() {
const loader = document.querySelector('.loader');
loader.style.display = 'block';
return await fetch(queryUrlBuilder()).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
@@ -96,47 +197,78 @@
bookOrderMap.set(getInnerHtml(book, 'id'), idx);
});
incrementalLoadingParams.start += books.length;
if (parseInt(data.querySelector('totalResults').innerHTML) === bookOrderMap.size) {
const results = parseInt(data.querySelector('totalResults').innerHTML)
if (results === bookOrderMap.size) {
incrementalLoadingParams.count = 0;
toggleFooter(true);
} else {
toggleFooter();
}
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
loader.style.display = 'none';
return books;
});
}
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() {
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
if (!bookOrderMap.size) {
if (!noResultInjected) {
noResultInjected = true;
iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('a'));
iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('div'));
iso.layout();
const spanTag = document.createElement('span');
spanTag.setAttribute('class', 'noResults');
spanTag.innerHTML = `No result. Would you like to <a href="/?lang=">reset filter?</a>`;
document.querySelector('body').append(spanTag);
spanTag.getElementsByTagName('a')[0].onclick = (event) => {
event.preventDefault();
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?lang=`);
setCookie(filterCookieName, 'lang=');
resetAndFilter();
filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''});
};
setTimeout(() => {
const divTag = document.createElement('div');
divTag.setAttribute('class', 'noResults');
divTag.innerHTML = `No result. Would you like to <a href="/?lang=">reset filter</a>?`;
kiwixHomeBody.append(divTag);
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
divTag.getElementsByTagName('a')[0].onclick = (event) => {
event.preventDefault();
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?lang=`);
setCookie(filterCookieName, 'lang=');
resetAndFilter();
document.querySelectorAll('.filter').forEach(filter => {
filter.value = params.get(filter.name) || '';
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
})
};
loader.setAttribute('style', 'position: absolute; top: 50%');
}, 300);
}
return true;
} else if (noResultInjected) {
noResultInjected = false;
document.getElementsByClassName('noResults')[0].remove();
kiwixHomeBody.removeAttribute('style');
}
loader.removeAttribute('style');
return false;
}
@@ -170,7 +302,13 @@
});
books = [...books].filter((book) => {return !booksToFilter.has(getInnerHtml(book, 'id'))});
booksToDelete.forEach(book => {iso.remove(book);});
books.forEach((book) => {iso.insert(generateBookHtml(book, sort))});
books.forEach((book) => {
iso.insert(generateBookHtml(book, sort))
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .download`);
if (downloadButton) {
insertModal(downloadButton);
}
});
}
async function resetAndFilter(filterType = '', filterValue = '') {
@@ -185,12 +323,19 @@
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
setCookie(filterCookieName, params.toString());
}
document.querySelectorAll('.filter').forEach(filter => {
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
await loadAndDisplayBooks(true);
}
window.addEventListener('popstate', async () => {
await resetAndFilter();
filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''});
document.querySelectorAll('.filter').forEach(filter => {filter.value = params.get(filter.name) || ''});
});
async function loadSubset() {
@@ -223,28 +368,48 @@
return index ? parseInt(index) : Infinity;
}
},
sortBy: 'weight'
sortBy: 'weight',
layoutMode: 'cellsByRow',
cellsByRow: {
columnWidth: '.book',
rowHeight: '.book'
}
});
footer = document.getElementById('kiwixfooter');
fadeOutDiv = document.getElementById('fadeOut');
loader = document.querySelector('.loader');
await loadAndDisplayBooks();
await loadAndDisplayOptions('#languageFilter', langList);
await loadAndDisplayOptions('#categoryFilter', categoryList);
filterTypes.forEach((filter) => {
const filterTag = document.getElementsByName(filter)[0];
filterTag.addEventListener('change', () => {resetAndFilter(filterTag.name, filterTag.value)});
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) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
setCookie(filterCookieName, params.toString());
}
})();

View File

File diff suppressed because one or more lines are too long

View File

@@ -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"
}

BIN
static/skin/magnet.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="512.000000" height="512.000000" preserveAspectRatio="xMidYMid meet" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/>
<g class="currentLayer" style=""><title>Layer 1</title><g transform="translate(0,512) scale(0.10000000149011612,-0.10000000149011612) " fill="#484848" stroke="none" id="svg_1" class="selected" fill-opacity="1">
<path d="M1858 5104 c-341 -50 -678 -192 -963 -408 -104 -78 -304 -278 -384 -384 -124 -164 -256 -409 -315 -583 -311 -929 47 -1926 877 -2442 662 -410 1535 -405 2188 14 l76 49 125 -128 c69 -70 352 -364 630 -653 574 -597 548 -575 688 -567 85 5 123 21 176 75 52 52 77 114 77 193 0 98 -24 142 -151 273 -420 434 -1132 1177 -1132 1182 0 3 22 33 49 68 164 209 291 471 362 746 159 614 28 1255 -363 1774 -78 104 -279 305 -386 386 -288 218 -618 355 -974 406 -130 19 -448 18 -580 -1z m502 -545 c308 -38 607 -184 844 -409 476 -455 606 -1146 327 -1745 -128 -273 -356 -519 -626 -674 -387 -222 -857 -259 -1282 -102 -626 231 -1040 879 -983 1540 13 146 33 241 77 371 214 634 830 1060 1493 1032 36 -1 103 -7 150 -13z" id="svg_2" fill="#484848" fill-opacity="1"/>
</g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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&amp;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>

View File

@@ -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&amp;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>

View 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}}&amp;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>

View 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>

View File

@@ -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>

View File

@@ -2,6 +2,7 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Kiwix Server</title>
<script
type="text/javascript"
@@ -21,186 +22,58 @@
href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css"
rel="Stylesheet"
/>
<link
type="text/css"
href="{{root}}/skin/home.css"
rel="Stylesheet"
/>
<style>
html {
min-height: 100%;
position: relative;
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf") format("truetype");
}
body {
background: radial-gradient(#eeeeee 15%, transparent 16%) 0 0,
radial-gradient(#eeeeee 15%, transparent 16%) 8px 8px,
radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 0 1px,
radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 8px 9px;
background-color: #e8e8e8;
background-size: 16px 16px;
margin-left: auto;
margin-right: auto;
max-width: 1100px;
min-height: 100%;
}
.book__list {
text-align: center;
}
.kiwixHomeBody {
position: relative;
text-align: center;
min-height: 100%;
margin: 0 0 15px;
}
.book {
display: inline-block;
vertical-align: bottom;
margin: 8px;
padding: 12px 15px;
width: 300px;
border: 1px solid #ccc;
border-radius: 8px;
text-align: left;
color: #000;
font-family: sans-serif;
font-size: 13px;
background-color: #f1f1f1;
box-shadow: 2px 2px 5px 0 #ccc;
}
#kiwixfooter {
text-align: center;
margin: 0.5em;
position: absolute;
bottom: 0;
left: 46%;
}
.kiwixHomeNavbar {
display: flex;
justify-content: center;
}
.kiwixFilter {
margin: 8px 10px;
}
.kiwixSearch, .searchButton {
margin: 0 13px 0 0;
}
.kiwixSearchForm {
margin: 8px 10px;
float: right;
}
@media (max-width: 1100px) {
.kiwixHomeBody {
padding: 0 125px;
}
}
.book:hover {
background-color: #f9f9f9;
box-shadow: none;
}
.book__background {
background-repeat: no-repeat;
background-size: 48px 48px;
background-position: top right;
}
.book__title {
padding: 0 55px 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 18px;
color: #0645ad;
line-height: 1em;
}
.book__description {
padding: 5px 55px 5px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 15px;
line-height: 1em;
}
.book__info {
color: #777;
font-weight: bold;
font-size: 13px;
line-height: 1em;
}
a:link {
text-decoration: none;
}
a:visited {
text-decoration: none;
}
.noResults {
position: absolute;
top: 48%;
left: 42%;
}
.loader-spinner {
position: absolute;
top: -50%;
left: 50%;
border: 5px solid #f3f3f3;
border-radius: 50%;
border-top: 5px solid #3498db;
width: 40px;
height: 40px;
margin: auto;
-webkit-animation: spin 1s linear infinite; /* Safari */
animation: spin 1s linear infinite;
margin-top: 35px;
margin-bottom: -35px;
z-index: 2;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loader {
position: relative;
height: 70px;
width: 100%;
}
.fadeOut {
position: fixed;
display: none;
bottom: 0;
left: 0;
z-index: 1;
background: linear-gradient(180deg, rgba(232, 232, 232, 0) 0%, rgb(232, 232, 232) 100%);
height: 80px;
width: 100%;
}
.spacer {
height: 20px;
background: transparent;
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf") format("truetype");
}
</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>
<body class="kiwix">
<div class='kiwixHomeNavbar'>
<select name="lang" id="languageFilter" class='kiwixFilter'>
<option value="" selected>All languages</option>
</select>
<select name="category" id="categoryFilter" class='kiwixFilter'>
<option value="" selected>All categories</option>
</select>
<form id='kiwixSearchForm' class='kiwixSearchForm'>
<input type="text" name="q" id="searchFilter" class='kiwixSearch'>
<body>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
<div class="kiwixNav__select">
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All languages</option>
</select>
</div>
<div class="kiwixNav__select">
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All categories</option>
</select>
</div>
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<input type="submit" class="searchButton" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
<h3 class="kiwixHomeBody__results"></h3>
<div class="book__list"></div>
<div id="fadeOut" class="fadeOut"></div>
</div>
<div class="loader"><div class="loader-spinner"></div></div>
<div class="spacer"></div>
<div id="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div>
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
<div id="kiwixfooter" class="kiwixfooter">Powered by&nbsp;<a href="https://kiwix.org">Kiwix</a></div>
</body>
<script>
function closeModal() {
for(modal of document.getElementsByClassName('modal-wrapper')) {
modal.remove();
}
}
</script>
</html>

View File

@@ -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"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -2,6 +2,7 @@
#include "../include/manager.h"
#include "../include/library.h"
#include "../include/book.h"
#include "../include/tools.h"
#include <iostream>
#include <fstream>
@@ -13,13 +14,13 @@ TEST(ManagerTest, addBookFromPathAndGetIdTest)
auto bookId = manager.addBookFromPathAndGetId("./test/example.zim");
ASSERT_NE(bookId, "");
kiwix::Book book = lib.getBookById(bookId);
EXPECT_EQ(book.getPath(), computeAbsolutePath("", "./test/example.zim"));
EXPECT_EQ(book.getPath(), kiwix::computeAbsolutePath("", "./test/example.zim"));
const std::string pathToSave = "./pathToSave";
const std::string url = "url";
bookId = manager.addBookFromPathAndGetId("./test/example.zim", pathToSave, url, true);
book = lib.getBookById(bookId);
auto savedPath = computeAbsolutePath(removeLastPathElement(manager.writableLibraryPath), pathToSave);
auto savedPath = kiwix::computeAbsolutePath(kiwix::removeLastPathElement(manager.writableLibraryPath), pathToSave);
EXPECT_EQ(book.getPath(), savedPath);
EXPECT_EQ(book.getUrl(), url);
}

View File

@@ -8,6 +8,9 @@ tests = [
'kiwixserve',
'book',
'manager',
'opds_catalog',
'reader',
'searcher'
]
if build_machine.system() != 'windows'

85
test/opds_catalog.cpp Normal file
View File

@@ -0,0 +1,85 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "../include/opds_catalog.h"
#include "gtest/gtest.h"
using namespace kiwix;
TEST(OpdsCatalog, getSearchUrl)
{
#define EXPECT_SEARCH_URL(url) EXPECT_EQ(url, getSearchUrl(f))
{
Filter f;
EXPECT_SEARCH_URL("/catalog/v2/entries");
}
{
Filter f;
f.query("abc");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc");
}
{
Filter f;
f.query("abc def");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def");
}
{
Filter f;
f.category("ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted");
}
{
Filter f;
f.lang("eng");
EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng");
}
{
Filter f;
f.name("second");
EXPECT_SEARCH_URL("/catalog/v2/entries?name=second");
}
{
Filter f;
f.acceptTags({"paper", "plastic"});
EXPECT_SEARCH_URL("/catalog/v2/entries?tag=paper;plastic");
}
{
Filter f;
f.query("abc");
f.category("ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted");
}
{
Filter f;
f.category("ted");
f.query("abc");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted");
}
{
Filter f;
f.query("peru");
f.category("scifi");
f.lang("html");
f.name("edsonarantesdonascimento");
f.acceptTags({"body", "script"});
EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body;script");
}
#undef EXPECT_SEARCH_URL
}

View File

@@ -20,7 +20,8 @@
#include "gtest/gtest.h"
#include <string>
#include <vector>
#include "../include/tools/pathTools.h"
#include "../include/tools.h"
#include "../src/tools/pathTools.h"
#ifdef _WIN32
# define S "\\"
@@ -101,61 +102,61 @@ TEST(pathTools, normalizePartsRelative)
TEST(pathTools, isRelativePath)
{
ASSERT_TRUE(isRelativePath("foo"));
ASSERT_TRUE(isRelativePath(P2("foo","bar")));
ASSERT_TRUE(isRelativePath(P3(".","foo","bar")));
ASSERT_TRUE(isRelativePath(P2("..","foo")));
ASSERT_TRUE(isRelativePath(P4("foo","","bar","")));
ASSERT_FALSE(isRelativePath(A1("foo")));
ASSERT_FALSE(isRelativePath(A2("foo", "bar")));
ASSERT_TRUE(kiwix::isRelativePath("foo"));
ASSERT_TRUE(kiwix::isRelativePath(P2("foo","bar")));
ASSERT_TRUE(kiwix::isRelativePath(P3(".","foo","bar")));
ASSERT_TRUE(kiwix::isRelativePath(P2("..","foo")));
ASSERT_TRUE(kiwix::isRelativePath(P4("foo","","bar","")));
ASSERT_FALSE(kiwix::isRelativePath(A1("foo")));
ASSERT_FALSE(kiwix::isRelativePath(A2("foo", "bar")));
#ifdef _WIN32
ASSERT_FALSE(isRelativePath(P2(A_SAMBA, "foo")));
ASSERT_FALSE(isRelativePath(P3(A_SAMBA, "foo", "bar")));
ASSERT_FALSE(kiwix::isRelativePath(P2(A_SAMBA, "foo")));
ASSERT_FALSE(kiwix::isRelativePath(P3(A_SAMBA, "foo", "bar")));
#endif
}
TEST(pathTools, computeAbsolutePath)
{
ASSERT_EQ(computeAbsolutePath(A2("a","b"), "foo"),
ASSERT_EQ(kiwix::computeAbsolutePath(A2("a","b"), "foo"),
A3("a","b","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b",""), "foo"),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b",""), "foo"),
A3("a","b","foo"));
ASSERT_EQ(computeAbsolutePath(A2("a","b"), P2(".","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A2("a","b"), P2(".","foo")),
A3("a","b","foo"));
ASSERT_EQ(computeAbsolutePath(A2("a","b"), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A2("a","b"), P2("..","foo")),
A2("a","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b",""), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b",""), P2("..","foo")),
A2("a","foo"));
ASSERT_EQ(computeAbsolutePath(A5("a","b","c","d","e"), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A5("a","b","c","d","e"), P2("..","foo")),
A5("a","b","c","d","foo"));
ASSERT_EQ(computeAbsolutePath(A5("a","b","c","d","e"), P5("..","..","..","g","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A5("a","b","c","d","e"), P5("..","..","..","g","foo")),
A4("a","b","g","foo"));
#ifdef _WIN32
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b",""), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b",""), P2("..","foo")),
P3(A_SAMBA,"a","foo"));
ASSERT_EQ(computeAbsolutePath(P6(A_SAMBA,"a","b","c","d","e"), P5("..","..","..","g","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P6(A_SAMBA,"a","b","c","d","e"), P5("..","..","..","g","foo")),
P5(A_SAMBA,"a","b","g","foo"));
#endif
}
TEST(pathTools, computeRelativePath)
{
ASSERT_EQ(computeRelativePath(A2("a","b"), A3("a","b","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A2("a","b"), A3("a","b","foo")),
"foo");
ASSERT_EQ(computeRelativePath(A3("a","b",""), A3("a","b","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A3("a","b",""), A3("a","b","foo")),
"foo");
ASSERT_EQ(computeRelativePath(A2("a","b"), A2("a","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A2("a","b"), A2("a","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(A3("a","b",""), A2("a","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A3("a","b",""), A2("a","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(A5("a","b","c","d","e"), A5("a","b","c","d","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A5("a","b","c","d","e"), A5("a","b","c","d","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(A5("a","b","c","d","e"), A4("a","b","g","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A5("a","b","c","d","e"), A4("a","b","g","foo")),
P5("..","..","..","g","foo"));
#ifdef _WIN32
ASSERT_EQ(computeRelativePath(P3(A_SAMBA,"a","b"), P3(A_SAMBA,"a","foo")),
ASSERT_EQ(kiwix::computeRelativePath(P3(A_SAMBA,"a","b"), P3(A_SAMBA,"a","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(P6(A_SAMBA,"a","b","c","d","e"), P5(A_SAMBA,"a","b","g","foo")),
ASSERT_EQ(kiwix::computeRelativePath(P6(A_SAMBA,"a","b","c","d","e"), P5(A_SAMBA,"a","b","g","foo")),
P5("..","..","..","g","foo"));
#endif
@@ -163,73 +164,73 @@ TEST(pathTools, computeRelativePath)
TEST(pathTools, removeLastPathElement)
{
ASSERT_EQ(removeLastPathElement(P3("a","b","c")),
ASSERT_EQ(kiwix::removeLastPathElement(P3("a","b","c")),
P2("a","b"));
ASSERT_EQ(removeLastPathElement(A3("a","b","c")),
ASSERT_EQ(kiwix::removeLastPathElement(A3("a","b","c")),
A2("a","b"));
ASSERT_EQ(removeLastPathElement(P4("a","b","c","")),
ASSERT_EQ(kiwix::removeLastPathElement(P4("a","b","c","")),
P2("a","b"));
ASSERT_EQ(removeLastPathElement(A4("a","b","c","")),
ASSERT_EQ(kiwix::removeLastPathElement(A4("a","b","c","")),
A2("a","b"));
}
TEST(pathTools, appendToDirectory)
{
ASSERT_EQ(appendToDirectory(P3("a","b","c"), "foo.xml"),
ASSERT_EQ(kiwix::appendToDirectory(P3("a","b","c"), "foo.xml"),
P4("a","b","c","foo.xml"));
ASSERT_EQ(appendToDirectory(P4("a","b","c",""), "foo.xml"),
ASSERT_EQ(kiwix::appendToDirectory(P4("a","b","c",""), "foo.xml"),
P4("a","b","c","foo.xml"));
ASSERT_EQ(appendToDirectory(P3("a","b","c"), P2("d","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P3("a","b","c"), P2("d","foo.xml")),
P5("a","b","c","d","foo.xml"));
ASSERT_EQ(appendToDirectory(P4("a","b","c",""), P2("d","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P4("a","b","c",""), P2("d","foo.xml")),
P5("a","b","c","d","foo.xml"));
ASSERT_EQ(appendToDirectory(P3("a","b","c"), P2(".","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P3("a","b","c"), P2(".","foo.xml")),
P5("a","b","c",".","foo.xml"));
ASSERT_EQ(appendToDirectory(P4("a","b","c",""), P2(".","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P4("a","b","c",""), P2(".","foo.xml")),
P5("a","b","c",".","foo.xml"));
}
TEST(pathTools, goUp)
{
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), ".."),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), ".."),
A2("a", "b"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P2("..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P2("..","..")),
A1("a"));
#ifdef _WIN32
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
"c:");
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
"c:");
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), ".."),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), ".."),
P3(A_SAMBA,"a", "b"));
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P2("..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P2("..","..")),
P2(A_SAMBA,"a"));
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P3("..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P3("..","..","..")),
A_SAMBA);
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","..")),
A_SAMBA);
#else
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
"/");
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
"/");
#endif
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P2("..", "foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P2("..", "foo")),
A3("a", "b","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P3("..","..","foo")),
A2("a","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P4("..","..","..","foo")),
A1("foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P5("..","..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P5("..","..","..","..","foo")),
A1("foo"));
#ifdef _WIN32
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","foo")),
P2(A_SAMBA,"foo"));
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P5("..","..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P5("..","..","..","..","foo")),
P2(A_SAMBA,"foo"));
#endif
}
@@ -241,20 +242,20 @@ TEST(pathTools, dirChange)
std::string p1("c:\\a\\b\\c");
std::string p2("d:\\d\\e\\foo.xml");
{
std::string relative_path = computeRelativePath(p1, p2);
std::string relative_path = kiwix::computeRelativePath(p1, p2);
ASSERT_EQ(relative_path, p2);
std::string abs_path = computeAbsolutePath(p1, relative_path);
std::string abs_path = kiwix::computeAbsolutePath(p1, relative_path);
ASSERT_EQ(abs_path, p2);
ASSERT_EQ(computeAbsolutePath(p1, "..\\..\\..\\..\\..\\d:\\d\\e\\foo.xml"), p2);
ASSERT_EQ(kiwix::computeAbsolutePath(p1, "..\\..\\..\\..\\..\\d:\\d\\e\\foo.xml"), p2);
}
std::string ps("\\\\samba\\d\\e\\foo.xml");
{
std::string relative_path = computeRelativePath(p1, ps);
std::string relative_path = kiwix::computeRelativePath(p1, ps);
ASSERT_EQ(relative_path, ps);
std::string abs_path = computeAbsolutePath(p1, relative_path);
std::string abs_path = kiwix::computeAbsolutePath(p1, relative_path);
ASSERT_EQ(abs_path, ps);
// I'm not sure this test is valid on windows :/
// ASSERT_EQ(computeAbsolutePath(p1, "..\\..\\..\\..\\..\\\\samba\\d\\e\\foo.xml"), ps);
// ASSERT_EQ(kiwix::computeAbsolutePath(p1, "..\\..\\..\\..\\..\\\\samba\\d\\e\\foo.xml"), ps);
}
}

62
test/reader.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "gtest/gtest.h"
#include "../include/reader.h"
#include "zim/archive.h"
namespace kiwix
{
/**
* This test file is written primarily to demonstrate how Reader is simply a
* wrapper over an archive. We will be dropping this wrapper soon.
**/
TEST (Reader, archiveWrapper) {
Reader reader("./test/zimfile.zim");
zim::Archive archive = *reader.getZimArchive();
std::ostringstream s;
s << archive.getUuid();
ASSERT_EQ(reader.getId(), s.str());
ASSERT_EQ(reader.getGlobalCount(), archive.getEntryCount());
ASSERT_EQ(reader.getMainPage().getTitle(), archive.getMainEntry().getTitle());
ASSERT_EQ(reader.hasFulltextIndex(), archive.hasFulltextIndex());
ASSERT_NO_THROW(reader.getRandomPage());
}
TEST (Reader, getFunctions) {
zim::Archive archive("./test/zimfile.zim");
Reader reader("./test/zimfile.zim");
auto archiveEntry = archive.getRandomEntry();
ASSERT_TRUE(reader.pathExists(archiveEntry.getPath()));
auto readerEntry = reader.getEntryFromPath(archiveEntry.getPath());
ASSERT_EQ(readerEntry.getTitle(), archiveEntry.getTitle());
ASSERT_FALSE(reader.pathExists("invalidEntryPath"));
ASSERT_THROW(reader.getEntryFromPath("invalidEntryPath"), NoEntry);
readerEntry = reader.getEntryFromTitle(archiveEntry.getTitle());
ASSERT_EQ(readerEntry.getTitle(), archiveEntry.getTitle());
}
TEST (Reader, suggestions) {
Reader reader("./test/zimfile.zim");
SuggestionsList_t suggestions;
reader.searchSuggestionsSmart("The Genius", 4, suggestions);
std::vector<std::string> suggestionResult, expectedResult;
std::string suggestionTitle;
for (auto it = suggestions.begin(); it != suggestions.end(); it++) {
suggestionResult.push_back(it->getTitle());
}
expectedResult = {
"The Genius After Hours",
"The Genius Hits the Road",
"The Genius Sings the Blues",
"The Genius of Ray Charles"
};
ASSERT_EQ(suggestionResult, expectedResult);
}
}

View File

@@ -21,7 +21,7 @@
#include <string>
#include "../include/tools/regexTools.h"
#include "../src/tools/regexTools.h"
namespace
{

67
test/searcher.cpp Normal file
View File

@@ -0,0 +1,67 @@
#include "gtest/gtest.h"
#include "../include/searcher.h"
#include "../include/reader.h"
namespace kiwix
{
TEST(Searcher, search) {
Reader reader("./test/example.zim");
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);
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);
}
}

View File

@@ -5,7 +5,7 @@
#include "../include/manager.h"
#include "../include/server.h"
#include "../include/name_mapper.h"
#include "../include/tools.h"
using TestContextImpl = std::vector<std::pair<std::string, std::string> >;
struct TestContext : TestContextImpl {
@@ -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;
@@ -81,14 +81,14 @@ private: // data
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
: manager(&this->library)
{
if ( isRelativePath(libraryFilePath) )
libraryFilePath = computeAbsolutePath(getCurrentDirectory(), libraryFilePath);
if ( kiwix::isRelativePath(libraryFilePath) )
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
manager.readFile(libraryFilePath, true, true);
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&amp;content=zimfile</icon>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/meta?name=Illustration_48x48@1&amp;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&amp;content=zimfile</icon>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/meta?name=Illustration_48x48@1&amp;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&amp;content=zimfile</icon>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/meta?name=Illustration_48x48@1&amp;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"
);
}