Compare commits
57 Commits
11.0.0
...
widgetEndp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc87def18b | ||
|
|
09dc6f90fd | ||
|
|
58c04b3f77 | ||
|
|
d27220f65d | ||
|
|
efe42c9bbe | ||
|
|
489dfc1123 | ||
|
|
9f545718c2 | ||
|
|
e323dcf6c9 | ||
|
|
3b98987cb3 | ||
|
|
fd36d11ccf | ||
|
|
dc56f82c29 | ||
|
|
1b1c1e352e | ||
|
|
a4b18893aa | ||
|
|
d737db666a | ||
|
|
cff143b4ec | ||
|
|
8e6d893f7f | ||
|
|
111aab0c23 | ||
|
|
dd90ca1018 | ||
|
|
3facd594f6 | ||
|
|
4cd52b0809 | ||
|
|
baf22c2516 | ||
|
|
f8a530100f | ||
|
|
a0db199388 | ||
|
|
f0f473b829 | ||
|
|
1e247d75bb | ||
|
|
4f3ec817db | ||
|
|
98bcf8acd6 | ||
|
|
b69bf4d062 | ||
|
|
6891ce3b57 | ||
|
|
16197afc95 | ||
|
|
abccd9d706 | ||
|
|
d0adb4e722 | ||
|
|
88c25b3a6c | ||
|
|
5aa74c62d6 | ||
|
|
2b6da38c46 | ||
|
|
dfc6cad9c2 | ||
|
|
28f8dbcf20 | ||
|
|
81865c0f0e | ||
|
|
538a46f262 | ||
|
|
e1d1d202bd | ||
|
|
71e2df7406 | ||
|
|
69931fb347 | ||
|
|
12e0fb6934 | ||
|
|
43ab6dfb6a | ||
|
|
93f2686a94 | ||
|
|
19a9c84e13 | ||
|
|
f034018b5c | ||
|
|
596b223a9d | ||
|
|
0c549af307 | ||
|
|
37b39430d1 | ||
|
|
947744caea | ||
|
|
b9e03d2772 | ||
|
|
e9b7eeb3c9 | ||
|
|
15cb9025bb | ||
|
|
1139f2cb4c | ||
|
|
0086049d4f | ||
|
|
e3e4bfa533 |
14
.github/workflows/ci.yml
vendored
@@ -1,13 +1,17 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
Macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -82,10 +86,6 @@ jobs:
|
||||
container:
|
||||
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-31"
|
||||
steps:
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
||||
id: extract_branch
|
||||
- name: Checkout code
|
||||
shell: python
|
||||
run: |
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
'git', 'clone',
|
||||
'https://github.com/${{github.repository}}',
|
||||
'--depth=1',
|
||||
'--branch', '${{steps.extract_branch.outputs.branch}}'
|
||||
'--branch', '${{ github.head_ref || github.ref_name }}'
|
||||
]
|
||||
check_call(command, cwd=environ['HOME'])
|
||||
- name: Install deps
|
||||
|
||||
10
README.md
@@ -173,14 +173,6 @@ To use JS provided by kiwix-serve you can use the following template to start wi
|
||||
<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>
|
||||
@@ -198,7 +190,7 @@ To use JS provided by kiwix-serve you can use the following template to start wi
|
||||
```
|
||||
<form id='kiwixSearchForm'>
|
||||
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
|
||||
<input type="submit" class="searchButton" value="Search"/>
|
||||
<input type="submit" class="kiwixButton" value="Search"/>
|
||||
</form>
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import os
|
||||
|
||||
project = 'libkiwix'
|
||||
copyright = '2022, libkiwix-team'
|
||||
author = 'libzim-team'
|
||||
author = 'libkiwix-team'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to libzim's documentation!
|
||||
Welcome to libkiwix's documentation!
|
||||
==================================
|
||||
|
||||
.. toctree::
|
||||
@@ -12,3 +12,4 @@ Welcome to libzim's documentation!
|
||||
|
||||
usage
|
||||
api/ref_api
|
||||
widget
|
||||
|
||||
@@ -7,11 +7,9 @@ Introduction
|
||||
libkiwix is written in C++. To use the library, you need the include files of libkiwix have
|
||||
to link against libzim.
|
||||
|
||||
Errors are handled with exceptions. When something goes wrong, libzim throws an error,
|
||||
Errors are handled with exceptions. When something goes wrong, libkiwix throws an error,
|
||||
which is always derived from std::exception.
|
||||
|
||||
All classes are defined in the namespace kiwix.
|
||||
|
||||
libkiwix is a set of tools to manage zim files and provide some common functionnality.
|
||||
While libkiwix has some wrappers around libzim classes, they are deprecated and will be removed
|
||||
in the future.
|
||||
|
||||
82
docs/widget.rst
Normal file
@@ -0,0 +1,82 @@
|
||||
Kiwix serve widget
|
||||
====================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The kiwix-serve widget provides an easy to embed way to show the `kiwix-serve` homepage.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
To use the widget, simply add an iframe with its `src` attribute set to the `widget` endpoint.
|
||||
Example HTML Page ::
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Widget Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="http://192.168.18.8:8080/widget?disabledesc&disablefilter&disabledownload" width=1000 height=1000></iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
This creates an iframe with the kiwix-serve homepage contents.
|
||||
|
||||
Arguments are explained below.
|
||||
|
||||
Possible Arguments
|
||||
-------------------
|
||||
|
||||
Currently, the following arguments are supported.
|
||||
|
||||
disabledesc (value = N/A)
|
||||
Disables the description part of a tile.
|
||||
|
||||
disablefilter (value = N/A)
|
||||
Disables the search filters: language, category, tag and search function.
|
||||
|
||||
disableclick (value = N/A)
|
||||
Disables clicking the book to open it for reading.
|
||||
|
||||
disabledownload (value = N/A)
|
||||
Disables the download button (if avaialable at all) on the tile.
|
||||
|
||||
|
||||
Custom CSS and JS
|
||||
-----------------
|
||||
|
||||
You can add your custom CSS rules and Javascript code to the widget.
|
||||
|
||||
To do that, use the following code as template::
|
||||
|
||||
<iframe id="receiver" src="http://192.168.18.8:8080/widget?disabledesc=&disablefilter=&disabledownload=" width="1000" height="1000">
|
||||
<p>Your browser does not support iframes.</p>
|
||||
</iframe>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
var receiver = document.getElementById('receiver').contentWindow;
|
||||
function sendMessage() {
|
||||
let msg = {
|
||||
css: `
|
||||
.book__header {
|
||||
color:red;
|
||||
}`,
|
||||
js: `
|
||||
function widgetTest() {
|
||||
console.log("Testing widget");
|
||||
}
|
||||
widgetTest();
|
||||
`
|
||||
}
|
||||
receiver.postMessage(msg, 'http://192.168.18.8:8080/widget');
|
||||
}
|
||||
sendMessage();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
The CSS/JS fields are optional, you may send both or only one.
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace kiwix
|
||||
{
|
||||
|
||||
class OPDSDumper;
|
||||
class Reader;
|
||||
|
||||
/**
|
||||
* A class to store information about a book (a zim file)
|
||||
@@ -69,7 +68,6 @@ class Book
|
||||
~Book();
|
||||
|
||||
bool update(const Book& other);
|
||||
DEPRECATED 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);
|
||||
|
||||
193
include/entry.h
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018-2020 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_ENTRY_H
|
||||
#define KIWIX_ENTRY_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <zim/entry.h>
|
||||
#include <zim/item.h>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
|
||||
class NoEntry : public std::exception {};
|
||||
|
||||
/**
|
||||
* A entry represent an.. entry in a zim file.
|
||||
*/
|
||||
class Entry
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Construct an entry making reference to an zim article.
|
||||
*
|
||||
* @param article a zim::Article object
|
||||
*/
|
||||
DEPRECATED Entry(zim::Entry entry) : Entry(entry, true) {};
|
||||
virtual ~Entry() = default;
|
||||
|
||||
/**
|
||||
* Get the path of the entry.
|
||||
*
|
||||
* The path is the "key" of an entry.
|
||||
*
|
||||
* @return the path of the entry.
|
||||
*/
|
||||
std::string getPath() const { return entry.getPath(); }
|
||||
|
||||
/**
|
||||
* Get the title of the entry.
|
||||
*
|
||||
* @return the title of the entry.
|
||||
*/
|
||||
std::string getTitle() const { return entry.getTitle(); }
|
||||
|
||||
/**
|
||||
* Get the content of the entry.
|
||||
*
|
||||
* The string is a copy of the content.
|
||||
* If you don't want to do a copy, use get_blob.
|
||||
*
|
||||
* @return the content of the entry.
|
||||
*/
|
||||
std::string getContent() const { return entry.getItem().getData(); }
|
||||
|
||||
/**
|
||||
* Get the blob of the entry.
|
||||
*
|
||||
* A blob make reference to the content without copying it.
|
||||
*
|
||||
* @param offset The starting offset of the blob.
|
||||
* @return the blob of the entry.
|
||||
*/
|
||||
zim::Blob getBlob(offset_type offset = 0) const { return entry.getItem().getData(offset); }
|
||||
|
||||
/**
|
||||
* Get the blob of the entry.
|
||||
*
|
||||
* A blob make reference to the content without copying it.
|
||||
*
|
||||
* @param offset The starting offset of the blob.
|
||||
* @param size The size of the blob.
|
||||
* @return the blob of the entry.
|
||||
*/
|
||||
zim::Blob getBlob(offset_type offset, size_type size) const { return entry.getItem().getData(offset, size); }
|
||||
|
||||
/**
|
||||
* Get the info for direct access to the content of the entry.
|
||||
*
|
||||
* Some entry (ie binary ones) have their content plain stored
|
||||
* in the zim file. Knowing the offset where the content is stored
|
||||
* an user can directly read the content in the zim file bypassing the
|
||||
* libkiwix/libzim.
|
||||
*
|
||||
* @return A pair specifying where to read the content.
|
||||
* The string is the real file to read (may be different that .zim
|
||||
* file if zim is cut).
|
||||
* The offset is the offset to read in the file.
|
||||
* Return <"",0> if is not possible to read directly.
|
||||
*/
|
||||
zim::Item::DirectAccessInfo getDirectAccessInfo() const { return entry.getItem().getDirectAccessInformation(); }
|
||||
|
||||
/**
|
||||
* Get the size of the entry.
|
||||
*
|
||||
* @return the size of the entry.
|
||||
*/
|
||||
size_type getSize() const;
|
||||
|
||||
/**
|
||||
* Get the mime_type of the entry.
|
||||
*
|
||||
* @return the mime_type of the entry.
|
||||
*/
|
||||
std::string getMimetype() const;
|
||||
|
||||
|
||||
/**
|
||||
* Get if the entry is a redirect entry.
|
||||
*
|
||||
* @return True if the entry is a redirect.
|
||||
*/
|
||||
bool isRedirect() const;
|
||||
|
||||
/**
|
||||
* Get if the entry is a link target entry.
|
||||
*
|
||||
* @return True if the entry is a link target.
|
||||
*/
|
||||
bool isLinkTarget() const;
|
||||
|
||||
/**
|
||||
* Get if the entry is a deleted entry.
|
||||
*
|
||||
* @return True if the entry is a deleted entry.
|
||||
*/
|
||||
bool isDeleted() const;
|
||||
|
||||
/**
|
||||
* Get the entry pointed by this entry.
|
||||
*
|
||||
* @return the entry pointed.
|
||||
* @throw NoEntry if the entry is not a redirected entry.
|
||||
*/
|
||||
Entry getRedirectEntry() const;
|
||||
|
||||
/**
|
||||
* Get the final entry pointed by this entry.
|
||||
*
|
||||
* Follow the redirection until a "not redirecting" entry is found.
|
||||
* If the entry is not a redirected entry, return the entry itself.
|
||||
*
|
||||
* @return the final entry.
|
||||
*/
|
||||
Entry getFinalEntry() const;
|
||||
|
||||
/**
|
||||
* Get the zim entry wrapped by this (kiwix) entry
|
||||
*
|
||||
* @return the zim entry
|
||||
*/
|
||||
const zim::Entry& getZimEntry() const { return entry; }
|
||||
|
||||
private:
|
||||
zim::Entry entry;
|
||||
|
||||
private:
|
||||
// Entry is deprecated, so we've marked the constructor as deprecated.
|
||||
// But we still need to construct the entry (in our deprecated code)
|
||||
// To avoid warning because we use deprecated function, we create a second
|
||||
// constructor not deprecated. The `bool marker` is unused, it sole purpose
|
||||
// is to change the signature to have two different constructor.
|
||||
// This one is not deprecated and we must use it in our private code.
|
||||
Entry(zim::Entry entry, bool marker);
|
||||
friend class Reader;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // KIWIX_ENTRY_H
|
||||
@@ -223,7 +223,6 @@ class Library
|
||||
|
||||
Book getBookByIdThreadSafe(const std::string& id) const;
|
||||
|
||||
DEPRECATED std::shared_ptr<Reader> getReaderById(const std::string& id);
|
||||
std::shared_ptr<zim::Archive> getArchiveById(const std::string& id);
|
||||
std::shared_ptr<ZimSearcher> getSearcherById(const std::string& id) {
|
||||
return getSearcherByIds(BookIdSet{id});
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include "book.h"
|
||||
#include "library.h"
|
||||
#include "reader.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -7,9 +7,6 @@ headers = [
|
||||
'libxml_dumper.h',
|
||||
'opds_dumper.h',
|
||||
'downloader.h',
|
||||
'reader.h',
|
||||
'entry.h',
|
||||
'searcher.h',
|
||||
'search_renderer.h',
|
||||
'server.h',
|
||||
'kiwixserve.h',
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include "library.h"
|
||||
#include "reader.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
506
include/reader.h
@@ -1,506 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
*
|
||||
* 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_READER_H
|
||||
#define KIWIX_READER_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <zim/zim.h>
|
||||
#include <zim/archive.h>
|
||||
#include <exception>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include "common.h"
|
||||
#include "entry.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/**
|
||||
* The SuggestionItem is a helper class that contains the info about a single
|
||||
* suggestion item.
|
||||
*/
|
||||
class SuggestionItem
|
||||
{
|
||||
// Functions
|
||||
public:
|
||||
// Create a sugggestion item.
|
||||
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() const { return title;}
|
||||
const std::string& getNormalizedTitle() const { return normalizedTitle;}
|
||||
const std::string& getPath() const { return path;}
|
||||
const std::string& getSnippet() const { return snippet;}
|
||||
|
||||
bool hasSnippet() const { return !snippet.empty();}
|
||||
|
||||
// Data
|
||||
private:
|
||||
std::string title;
|
||||
std::string normalizedTitle;
|
||||
std::string path;
|
||||
std::string snippet;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Reader class is the class who allow to get an entry content from a zim
|
||||
* file.
|
||||
*
|
||||
* Reader is now deprecated. Directly use `zim::Archive`.
|
||||
*/
|
||||
|
||||
using SuggestionsList_t = std::vector<SuggestionItem>;
|
||||
class Reader
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Create a Reader to read a zim file specified by zimFilePath.
|
||||
*
|
||||
* @param zimFilePath The path to the zim file to read.
|
||||
* The zim file can be splitted (.zimaa, .zimab, ...).
|
||||
* In this case, the file path must still point to the
|
||||
* unsplitted path as if the file were not splitted
|
||||
* (.zim extesion).
|
||||
*/
|
||||
explicit DEPRECATED 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 DEPRECATED Reader(const std::shared_ptr<zim::Archive> archive)
|
||||
: Reader(archive, true) {};
|
||||
#ifndef _WIN32
|
||||
explicit DEPRECATED Reader(int fd);
|
||||
DEPRECATED Reader(int fd, zim::offset_type offset, zim::size_type size);
|
||||
#endif
|
||||
~Reader() = default;
|
||||
|
||||
/**
|
||||
* Get the number of "displayable" entries in the zim file.
|
||||
*
|
||||
* @return If the zim file has a /M/Counter metadata, return the number of
|
||||
* entries with the 'text/html' MIMEtype specified in the metadata.
|
||||
* Else return the number of entries in the 'A' namespace.
|
||||
*/
|
||||
unsigned int getArticleCount() const;
|
||||
|
||||
/**
|
||||
* Get the number of media in the zim file.
|
||||
*
|
||||
* @return If the zim file has a /M/Counter metadata, return the number of
|
||||
* entries with the 'image/jpeg', 'image/gif' and 'image/png' in
|
||||
* the metadata.
|
||||
* Else return the number of entries in the 'I' namespace.
|
||||
*/
|
||||
unsigned int getMediaCount() const;
|
||||
|
||||
/**
|
||||
* Get the number of all entries in the zim file.
|
||||
*
|
||||
* @return Return the number of all the entries, whatever their MIMEtype or
|
||||
* their namespace.
|
||||
*/
|
||||
unsigned int getGlobalCount() const;
|
||||
|
||||
/**
|
||||
* Get the path of the zim file.
|
||||
*
|
||||
* @return the path of the zim file as given in the constructor.
|
||||
*/
|
||||
string getZimFilePath() const;
|
||||
|
||||
/**
|
||||
* Get the Id of the zim file.
|
||||
*
|
||||
* @return The uuid stored in the zim file.
|
||||
*/
|
||||
string getId() const;
|
||||
|
||||
/**
|
||||
* Get a random page.
|
||||
*
|
||||
* @return A random Entry. The entry is picked from all entries in
|
||||
* the 'A' namespace.
|
||||
* The main entry is excluded from the potential results.
|
||||
*/
|
||||
Entry getRandomPage() const;
|
||||
|
||||
/**
|
||||
* Get the entry of the main page.
|
||||
*
|
||||
* @return Entry of the main page as specified in the zim file.
|
||||
*/
|
||||
Entry getMainPage() const;
|
||||
|
||||
/**
|
||||
* Get the content of a metadata.
|
||||
*
|
||||
* @param[in] name The name of the metadata.
|
||||
* @param[out] value The value will be set to the content of the metadata.
|
||||
* @return True if it was possible to get the content of the metadata.
|
||||
*/
|
||||
bool getMetadata(const string& name, string& value) const;
|
||||
|
||||
/**
|
||||
* Get the name of the zim file.
|
||||
*
|
||||
* @return The name of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getName() const;
|
||||
|
||||
/**
|
||||
* Get the title of the zim file.
|
||||
*
|
||||
* @return The title of zim file as specified in the zim metadata.
|
||||
* If no title has been set, return a title computed from the
|
||||
* file path.
|
||||
*/
|
||||
string getTitle() const;
|
||||
|
||||
/**
|
||||
* Get the creator of the zim file.
|
||||
*
|
||||
* @return The creator of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getCreator() const;
|
||||
|
||||
/**
|
||||
* Get the publisher of the zim file.
|
||||
*
|
||||
* @return The publisher of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getPublisher() const;
|
||||
|
||||
/**
|
||||
* Get the date of the zim file.
|
||||
*
|
||||
* @return The date of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getDate() const;
|
||||
|
||||
/**
|
||||
* Get the description of the zim file.
|
||||
*
|
||||
* @return The description of the zim file as specified in the zim metadata.
|
||||
* If no description has been set, return the subtitle.
|
||||
*/
|
||||
string getDescription() const;
|
||||
|
||||
/**
|
||||
* Get the long description of the zim file.
|
||||
*
|
||||
* @return The long description of the zim file as specifed in the zim metadata.
|
||||
*/
|
||||
string getLongDescription() const;
|
||||
|
||||
/**
|
||||
* Get the language of the zim file.
|
||||
*
|
||||
* @return The language of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getLanguage() const;
|
||||
|
||||
/**
|
||||
* Get the license of the zim file.
|
||||
*
|
||||
* @return The license of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getLicense() const;
|
||||
|
||||
/**
|
||||
* Get the tags of the zim file.
|
||||
*
|
||||
* @param original If true, return the original tags as specified in the zim metadata.
|
||||
* Else, try to convert it to the new 'normalized' format.
|
||||
* @return The tags of the zim file.
|
||||
*/
|
||||
string getTags(bool original=false) const;
|
||||
|
||||
/**
|
||||
* Get the value (as a string) of a specific tag.
|
||||
*
|
||||
* According to https://wiki.openzim.org/wiki/Tags
|
||||
*
|
||||
* @return The value of the specified tag.
|
||||
* @throw std::out_of_range if the specified tag is not found.
|
||||
*/
|
||||
string getTagStr(const std::string& tagName) const;
|
||||
|
||||
/**
|
||||
* Get the boolean value of a specific tag.
|
||||
*
|
||||
* According to https://wiki.openzim.org/wiki/Tags
|
||||
*
|
||||
* @return The boolean value of the specified tag.
|
||||
* @throw std::out_of_range if the specified tag is not found.
|
||||
* std::domain_error if the value of the tag cannot be convert to bool.
|
||||
*/
|
||||
bool getTagBool(const std::string& tagName) const;
|
||||
|
||||
/**
|
||||
* Get the relations of the zim file.
|
||||
*
|
||||
* @return The relation of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getRelation() const;
|
||||
|
||||
/**
|
||||
* Get the flavour of the zim file.
|
||||
*
|
||||
* @return The flavour of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getFlavour() const;
|
||||
|
||||
/**
|
||||
* Get the source of the zim file.
|
||||
*
|
||||
* @return The source of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getSource() const;
|
||||
|
||||
/**
|
||||
* Get the scraper of the zim file.
|
||||
*
|
||||
* @return The scraper of the zim file as specified in the zim metadata.
|
||||
*/
|
||||
string getScraper() const;
|
||||
|
||||
/**
|
||||
* Get the favicon of the zim file.
|
||||
*
|
||||
* @param[out] content The content of the favicon.
|
||||
* @param[out] mimeType The mimeType of the favicon.
|
||||
* @return True if a favicon has been found.
|
||||
*/
|
||||
bool getFavicon(string& content, string& mimeType) const;
|
||||
|
||||
/**
|
||||
* Get an entry associated to an path.
|
||||
*
|
||||
* @param path The path of the entry.
|
||||
* @return The entry.
|
||||
* @throw NoEntry If no entry correspond to the path.
|
||||
*/
|
||||
Entry getEntryFromPath(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Get an entry associated to an url encoded path.
|
||||
*
|
||||
* Equivalent to `getEntryFromPath(urlDecode(path));`
|
||||
*
|
||||
* @param path The url encoded path.
|
||||
* @return The entry.
|
||||
* @throw NoEntry If no entry correspond to the path.
|
||||
*/
|
||||
Entry getEntryFromEncodedPath(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Get un entry associated to a title.
|
||||
*
|
||||
* @param title The title.
|
||||
* @return The entry
|
||||
* throw NoEntry If no entry correspond to the url.
|
||||
*/
|
||||
Entry getEntryFromTitle(const std::string& title) const;
|
||||
|
||||
/**
|
||||
* Search for entries with title starting with prefix (case sensitive).
|
||||
*
|
||||
* Suggestions are stored in an internal vector and can be retrieved using
|
||||
* `getNextSuggestion` method.
|
||||
* This method is not thread safe and is deprecated. Use :
|
||||
* bool searchSuggestions(const string& prefix,
|
||||
* unsigned int suggestionsCount,
|
||||
* SuggestionsList_t& results);
|
||||
*
|
||||
* @param prefix The prefix to search.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
* @param reset If true, remove previous suggestions in the internal vector.
|
||||
* If false, add suggestions to the internal vector
|
||||
* (until internal vector size is suggestionCount (or no more
|
||||
* suggestion))
|
||||
* @return True if some suggestions have been added to the internal vector.
|
||||
*/
|
||||
DEPRECATED bool searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
const bool reset = true);
|
||||
|
||||
/**
|
||||
* Search for entries with title starting with prefix (case sensitive).
|
||||
*
|
||||
* Suggestions are added to the `result` vector.
|
||||
*
|
||||
* @param prefix The prefix to search.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
* @param result The vector where to store the suggestions.
|
||||
* @return True if some suggestions have been added to the vector.
|
||||
*/
|
||||
|
||||
bool searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& resuls);
|
||||
|
||||
/**
|
||||
* Search for entries for the given prefix.
|
||||
*
|
||||
* If the zim file has a internal fulltext index, the suggestions will be
|
||||
* searched using it.
|
||||
* Else the suggestions will be search using `searchSuggestions` while trying
|
||||
* to be smart about case sensitivity (using `getTitleVariants`).
|
||||
*
|
||||
* In any case, suggestions are stored in an internal vector and can be
|
||||
* retrieved using `getNextSuggestion` method.
|
||||
* The internal vector will be reset.
|
||||
* This method is not thread safe and is deprecated. Use :
|
||||
* bool searchSuggestionsSmart(const string& prefix,
|
||||
* unsigned int suggestionsCount,
|
||||
* SuggestionsList_t& results);
|
||||
*
|
||||
* @param prefix The prefix to search for.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
*/
|
||||
DEPRECATED bool searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount);
|
||||
|
||||
/**
|
||||
* Search for entries for the given prefix.
|
||||
*
|
||||
* If the zim file has a internal fulltext index, the suggestions will be
|
||||
* searched using it.
|
||||
* Else the suggestions will be search using `searchSuggestions` while trying
|
||||
* to be smart about case sensitivity (using `getTitleVariants`).
|
||||
*
|
||||
* In any case, suggestions are stored in an internal vector and can be
|
||||
* retrieved using `getNextSuggestion` method.
|
||||
* The internal vector will be reset.
|
||||
*
|
||||
* @param prefix The prefix to search for.
|
||||
* @param suggestionsCount How many suggestions to search for.
|
||||
* @param results The vector where to store the suggestions
|
||||
* @return True if some suggestions have been added to the results.
|
||||
*/
|
||||
bool searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& results);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the path exists in the zim file.
|
||||
*
|
||||
* @param path the path to check.
|
||||
* @return True if the path exists in the zim file.
|
||||
*/
|
||||
bool pathExists(const string& path) const;
|
||||
|
||||
/**
|
||||
* Check if the zim file has a embedded fulltext index.
|
||||
*
|
||||
* @return True if the zim file has a embedded fulltext index
|
||||
* and is not split (else the fulltext is not accessible).
|
||||
*/
|
||||
bool hasFulltextIndex() const;
|
||||
|
||||
/**
|
||||
* Get potential case title variations for a title.
|
||||
*
|
||||
* @param title a title.
|
||||
* @return the list of variantions.
|
||||
*/
|
||||
std::vector<std::string> getTitleVariants(const std::string& title) const;
|
||||
|
||||
/**
|
||||
* Get the next suggestion title.
|
||||
*
|
||||
* @param[out] title the title of the suggestion.
|
||||
* @return True if title has been set.
|
||||
*/
|
||||
DEPRECATED bool getNextSuggestion(string& title);
|
||||
|
||||
/**
|
||||
* Get the next suggestion title and url.
|
||||
*
|
||||
* @param[out] title the title of the suggestion.
|
||||
* @param[out] url the url of the suggestion.
|
||||
* @return True if title and url have been set.
|
||||
*/
|
||||
DEPRECATED bool getNextSuggestion(string& title, string& url);
|
||||
|
||||
/**
|
||||
* Get if we can check zim file integrity (has a checksum).
|
||||
*
|
||||
* @return True if zim file have a checksum.
|
||||
*/
|
||||
bool canCheckIntegrity() const;
|
||||
|
||||
/**
|
||||
* Check is zim file is corrupted.
|
||||
*
|
||||
* @return True if zim file is corrupted.
|
||||
*/
|
||||
bool isCorrupted() const;
|
||||
|
||||
/**
|
||||
* Return the total size of the zim file.
|
||||
*
|
||||
* If zim file is split, return the sum of all parts' size.
|
||||
*
|
||||
* @return Size of the size file is KiB.
|
||||
*/
|
||||
unsigned int getFileSize() const;
|
||||
|
||||
/**
|
||||
* Get the zim file handler.
|
||||
*
|
||||
* @return The libzim file handler.
|
||||
*/
|
||||
zim::Archive* getZimArchive() const;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<zim::Archive> zimArchive;
|
||||
std::string zimFilePath;
|
||||
|
||||
SuggestionsList_t suggestions;
|
||||
SuggestionsList_t::iterator suggestionsOffset;
|
||||
|
||||
private:
|
||||
std::map<const std::string, unsigned int> parseCounterMetadata() const;
|
||||
|
||||
// Reader is deprecated, so we've marked the constructor as deprecated.
|
||||
// But we still need to construct the reader (in our deprecated code)
|
||||
// To avoid warning because we use deprecated function, we create a
|
||||
// constructor not deprecated. The `bool marker` is unused, it sole purpose
|
||||
// is to change the signature to have a different constructor.
|
||||
// This one is not deprecated and we must use it in our private code.
|
||||
Reader(const std::shared_ptr<zim::Archive> archive, bool marker);
|
||||
friend class Library;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -27,7 +27,6 @@
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
class Searcher;
|
||||
class NameMapper;
|
||||
/**
|
||||
* The SearcherRenderer class is used to render a search result to a html page.
|
||||
@@ -35,17 +34,6 @@ class NameMapper;
|
||||
class SearchRenderer
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Construct a SearchRenderer from a Searcher.
|
||||
*
|
||||
* This method is now deprecated. Construct the renderer from a
|
||||
* `zim::SearchResultSet`
|
||||
*
|
||||
* @param searcher The `Searcher` to render.
|
||||
* @param mapper The `NameMapper` to use to do the rendering.
|
||||
*/
|
||||
DEPRECATED SearchRenderer(Searcher* searcher, NameMapper* mapper);
|
||||
|
||||
/**
|
||||
* Construct a SearchRenderer from a SearchResultSet.
|
||||
*
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
*
|
||||
* 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_SEARCHER_H
|
||||
#define KIWIX_SEARCHER_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unicode/putil.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <locale>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <zim/search.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
class Reader;
|
||||
class Result
|
||||
{
|
||||
public:
|
||||
virtual ~Result(){};
|
||||
virtual std::string get_url() = 0;
|
||||
virtual std::string get_title() = 0;
|
||||
virtual int get_score() = 0;
|
||||
virtual std::string get_snippet() = 0;
|
||||
virtual std::string get_content() = 0;
|
||||
virtual int get_wordCount() = 0;
|
||||
virtual int get_size() = 0;
|
||||
virtual std::string get_zimId() = 0;
|
||||
};
|
||||
|
||||
struct SearcherInternal;
|
||||
struct SuggestionInternal;
|
||||
/**
|
||||
* The Searcher class is reponsible to do different kind of search using the
|
||||
* fulltext index.
|
||||
*
|
||||
* The Searcher is now deprecated. Use libzim search feature.
|
||||
*/
|
||||
class Searcher
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* The default constructor.
|
||||
*/
|
||||
DEPRECATED Searcher();
|
||||
|
||||
~Searcher();
|
||||
|
||||
/**
|
||||
* Add a reader (containing embedded fulltext index) to the search.
|
||||
*
|
||||
* @param reader The Reader for the zim containing the fulltext index.
|
||||
* @return true if the reader has been added.
|
||||
* false if the reader cannot be added (no embedded fulltext index present)
|
||||
*/
|
||||
bool add_reader(std::shared_ptr<Reader> reader);
|
||||
|
||||
|
||||
std::shared_ptr<Reader> get_reader(int index);
|
||||
|
||||
/**
|
||||
* Start a search on the zim associated to the Searcher.
|
||||
*
|
||||
* Search results should be retrived using the getNextResult method.
|
||||
*
|
||||
* @param search The search query.
|
||||
* @param resultStart the start 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 maxResultCount,
|
||||
const bool verbose = false);
|
||||
|
||||
/**
|
||||
* Start a geographique search.
|
||||
* The search return result for entry in a disc of center latitude/longitude
|
||||
* and radius distance.
|
||||
*
|
||||
* Search results should be retrived using the getNextResult method.
|
||||
*
|
||||
* @param latitude The latitude of the center point.
|
||||
* @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 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 maxResultCount,
|
||||
const bool verbose = false);
|
||||
|
||||
/**
|
||||
* Start a suggestion search.
|
||||
* The search made depend of the "version" of the embedded index.
|
||||
* - If the index is newer enough and have a title namespace, the search is
|
||||
* made in the titles only.
|
||||
* - Else the search is made on the whole article content.
|
||||
* In any case, the search is made "partial" (as adding '*' at the end of the query)
|
||||
*
|
||||
* @param search The search query.
|
||||
* @param verbose print some info on stdout if true.
|
||||
*/
|
||||
void suggestions(std::string& search, const bool verbose = false);
|
||||
|
||||
/**
|
||||
* Get the next result of a started search.
|
||||
* This is the method to use to loop hover the search results.
|
||||
*/
|
||||
Result* getNextResult();
|
||||
|
||||
/**
|
||||
* Restart the previous search.
|
||||
* Next call to getNextResult will return the first result.
|
||||
*/
|
||||
void restart_search();
|
||||
|
||||
/**
|
||||
* Get a estimation of the result count.
|
||||
*/
|
||||
unsigned int getEstimatedResultCount();
|
||||
|
||||
/**
|
||||
* Get a SearchResultSet object for current search
|
||||
*/
|
||||
zim::SearchResultSet getSearchResultSet();
|
||||
|
||||
unsigned int getResultStart() { return resultStart; }
|
||||
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 maxResultCount,
|
||||
const bool verbose = false);
|
||||
|
||||
std::vector<std::shared_ptr<Reader>> readers;
|
||||
std::unique_ptr<SearcherInternal> internal;
|
||||
std::unique_ptr<SuggestionInternal> suggestionInternal;
|
||||
std::string searchPattern;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
unsigned int maxResultCount;
|
||||
|
||||
private:
|
||||
void reset();
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
#include "book.h"
|
||||
#include "reader.h"
|
||||
|
||||
#include "tools.h"
|
||||
#include "tools/base64.h"
|
||||
@@ -30,7 +29,7 @@
|
||||
#include "tools/archiveTools.h"
|
||||
|
||||
#include <zim/archive.h>
|
||||
|
||||
#include <zim/item.h>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace kiwix
|
||||
@@ -64,11 +63,6 @@ bool Book::update(const kiwix::Book& other)
|
||||
return true;
|
||||
}
|
||||
|
||||
void Book::update(const kiwix::Reader& reader)
|
||||
{
|
||||
update(*reader.getZimArchive());
|
||||
}
|
||||
|
||||
void Book::update(const zim::Archive& archive) {
|
||||
m_path = archive.getFilename();
|
||||
m_pathValid = true;
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018-2020 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.
|
||||
*/
|
||||
|
||||
#include "reader.h"
|
||||
#include <time.h>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
Entry::Entry(zim::Entry entry, bool _marker)
|
||||
: entry(entry)
|
||||
{
|
||||
}
|
||||
|
||||
size_type Entry::getSize() const
|
||||
{
|
||||
if (entry.isRedirect()) {
|
||||
return 0;
|
||||
} else {
|
||||
return entry.getItem().getSize();
|
||||
}
|
||||
}
|
||||
|
||||
std::string Entry::getMimetype() const
|
||||
{
|
||||
return entry.getItem(true).getMimetype();
|
||||
}
|
||||
|
||||
bool Entry::isRedirect() const
|
||||
{
|
||||
return entry.isRedirect();
|
||||
}
|
||||
|
||||
Entry Entry::getRedirectEntry() const
|
||||
{
|
||||
if ( !entry.isRedirect() ) {
|
||||
throw NoEntry();
|
||||
}
|
||||
|
||||
return Entry(entry.getRedirectEntry(), true);
|
||||
}
|
||||
|
||||
Entry Entry::getFinalEntry() const
|
||||
{
|
||||
int loopCounter = 42;
|
||||
auto final_entry = entry;
|
||||
while (final_entry.isRedirect() && loopCounter--) {
|
||||
final_entry = final_entry.getRedirectEntry();
|
||||
}
|
||||
// Prevent infinite loops.
|
||||
if (final_entry.isRedirect()) {
|
||||
throw NoEntry();
|
||||
}
|
||||
return Entry(final_entry, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#include "library.h"
|
||||
#include "book.h"
|
||||
#include "reader.h"
|
||||
#include "libxml_dumper.h"
|
||||
|
||||
#include "tools.h"
|
||||
@@ -278,16 +277,6 @@ const Book& Library::getBookByPath(const std::string& path) const
|
||||
throw std::out_of_range(ss.str());
|
||||
}
|
||||
|
||||
std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
|
||||
{
|
||||
auto archive = getArchiveById(id);
|
||||
if(archive) {
|
||||
return std::shared_ptr<Reader>(new Reader(archive, true));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
|
||||
{
|
||||
try {
|
||||
@@ -473,8 +462,13 @@ void Library::updateBookDB(const Book& book)
|
||||
indexer.index_text(normalizeText(book.getName()), 1, "XN");
|
||||
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
|
||||
|
||||
for ( const auto& tag : split(normalizeText(book.getTags()), ";") )
|
||||
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
|
||||
doc.add_boolean_term("XT" + tag);
|
||||
if ( tag[0] != '_' ) {
|
||||
indexer.increase_termpos();
|
||||
indexer.index_text(tag);
|
||||
}
|
||||
}
|
||||
|
||||
const std::string idterm = "Q" + book.getId();
|
||||
doc.add_boolean_term(idterm);
|
||||
|
||||
@@ -6,10 +6,7 @@ kiwix_sources = [
|
||||
'libxml_dumper.cpp',
|
||||
'opds_dumper.cpp',
|
||||
'downloader.cpp',
|
||||
'reader.cpp',
|
||||
'entry.cpp',
|
||||
'server.cpp',
|
||||
'searcher.cpp',
|
||||
'search_renderer.cpp',
|
||||
'subprocess.cpp',
|
||||
'aria2.cpp',
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include "kiwixlib-resources.h"
|
||||
#include <mustache.hpp>
|
||||
#include <unicode/locid.h>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/otherTools.h"
|
||||
@@ -163,14 +162,8 @@ std::once_flag fillLanguagesFlag;
|
||||
void fillLanguagesMap()
|
||||
{
|
||||
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
|
||||
auto lang = *icuLangPtr;
|
||||
const icu::Locale locale(lang);
|
||||
icu::UnicodeString ustring;
|
||||
locale.getDisplayLanguage(locale, ustring);
|
||||
std::string displayLanguage;
|
||||
ustring.toUTF8String(displayLanguage);
|
||||
std::string iso3LangCode = locale.getISO3Language();
|
||||
iso639_3.insert({iso3LangCode, displayLanguage});
|
||||
const ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
472
src/reader.cpp
@@ -1,472 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
*
|
||||
* 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 "reader.h"
|
||||
#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"
|
||||
#include "tools/archiveTools.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
/* Constructor */
|
||||
Reader::Reader(const string zimFilePath)
|
||||
: zimArchive(nullptr),
|
||||
zimFilePath(zimFilePath)
|
||||
{
|
||||
string tmpZimFilePath = zimFilePath;
|
||||
|
||||
/* Remove potential trailing zimaa */
|
||||
size_t found = tmpZimFilePath.rfind("zimaa");
|
||||
if (found != string::npos && tmpZimFilePath.size() > 5
|
||||
&& found == tmpZimFilePath.size() - 5) {
|
||||
tmpZimFilePath.resize(tmpZimFilePath.size() - 2);
|
||||
}
|
||||
|
||||
zimArchive.reset(new zim::Archive(tmpZimFilePath));
|
||||
|
||||
/* initialize random seed: */
|
||||
srand(time(nullptr));
|
||||
}
|
||||
|
||||
Reader::Reader(const std::shared_ptr<zim::Archive> archive, bool _marker)
|
||||
: zimArchive(archive),
|
||||
zimFilePath(archive->getFilename())
|
||||
{}
|
||||
|
||||
#ifndef _WIN32
|
||||
Reader::Reader(int fd)
|
||||
: zimArchive(new zim::Archive(fd)),
|
||||
zimFilePath("")
|
||||
{
|
||||
/* initialize random seed: */
|
||||
srand(time(nullptr));
|
||||
}
|
||||
|
||||
Reader::Reader(int fd, zim::offset_type offset, zim::size_type size)
|
||||
: zimArchive(new zim::Archive(fd, offset, size)),
|
||||
zimFilePath("")
|
||||
{
|
||||
/* initialize random seed: */
|
||||
srand(time(nullptr));
|
||||
}
|
||||
#endif // #ifndef _WIN32
|
||||
|
||||
zim::Archive* Reader::getZimArchive() const
|
||||
{
|
||||
return zimArchive.get();
|
||||
}
|
||||
|
||||
MimeCounterType Reader::parseCounterMetadata() const
|
||||
{
|
||||
return kiwix::parseArchiveCounter(*zimArchive);
|
||||
}
|
||||
|
||||
/* Get the count of articles which can be indexed/displayed */
|
||||
unsigned int Reader::getArticleCount() const
|
||||
{
|
||||
std::map<const std::string, unsigned int> counterMap
|
||||
= this->parseCounterMetadata();
|
||||
unsigned int counter = 0;
|
||||
|
||||
for(auto &pair:counterMap) {
|
||||
if (startsWith(pair.first, "text/html")) {
|
||||
counter += pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
/* Get the count of medias content in the ZIM file */
|
||||
unsigned int Reader::getMediaCount() const
|
||||
{
|
||||
return kiwix::getArchiveMediaCount(*zimArchive);
|
||||
}
|
||||
|
||||
/* Get the total of all items of a ZIM file, redirects included */
|
||||
unsigned int Reader::getGlobalCount() const
|
||||
{
|
||||
return zimArchive->getEntryCount();
|
||||
}
|
||||
|
||||
/* Return the UID of the ZIM file */
|
||||
string Reader::getId() const
|
||||
{
|
||||
return kiwix::getArchiveId(*zimArchive);
|
||||
}
|
||||
|
||||
Entry Reader::getRandomPage() const
|
||||
{
|
||||
try {
|
||||
return Entry(zimArchive->getRandomEntry(), true);
|
||||
} catch(...) {
|
||||
throw NoEntry();
|
||||
}
|
||||
}
|
||||
|
||||
Entry Reader::getMainPage() const
|
||||
{
|
||||
return Entry(zimArchive->getMainEntry(), true);
|
||||
}
|
||||
|
||||
bool Reader::getFavicon(string& content, string& mimeType) const
|
||||
{
|
||||
return kiwix::getArchiveFavicon(*zimArchive, 48, content, mimeType);
|
||||
}
|
||||
|
||||
string Reader::getZimFilePath() const
|
||||
{
|
||||
return zimFilePath;
|
||||
}
|
||||
/* Return a metatag value */
|
||||
bool Reader::getMetadata(const string& name, string& value) const
|
||||
{
|
||||
try {
|
||||
value = zimArchive->getMetadata(name);
|
||||
return true;
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#define METADATA(NAME) std::string v; getMetadata(NAME, v); return v;
|
||||
|
||||
string Reader::getName() const
|
||||
{
|
||||
return kiwix::getMetaName(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getTitle() const
|
||||
{
|
||||
return kiwix::getArchiveTitle(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getCreator() const
|
||||
{
|
||||
return kiwix::getMetaCreator(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getPublisher() const
|
||||
{
|
||||
return kiwix::getMetaPublisher(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getDate() const
|
||||
{
|
||||
return kiwix::getMetaDate(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getDescription() const
|
||||
{
|
||||
return kiwix::getMetaDescription(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getLongDescription() const
|
||||
{
|
||||
METADATA("LongDescription")
|
||||
}
|
||||
|
||||
string Reader::getLanguage() const
|
||||
{
|
||||
return kiwix::getMetaLanguage(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getLicense() const
|
||||
{
|
||||
METADATA("License")
|
||||
}
|
||||
|
||||
string Reader::getTags(bool original) const
|
||||
{
|
||||
return kiwix::getMetaTags(*zimArchive, original);
|
||||
}
|
||||
|
||||
|
||||
string Reader::getTagStr(const std::string& tagName) const
|
||||
{
|
||||
string tags_str;
|
||||
getMetadata("Tags", tags_str);
|
||||
return getTagValueFromTagList(convertTags(tags_str), tagName);
|
||||
}
|
||||
|
||||
bool Reader::getTagBool(const std::string& tagName) const
|
||||
{
|
||||
return convertStrToBool(getTagStr(tagName));
|
||||
}
|
||||
|
||||
string Reader::getRelation() const
|
||||
{
|
||||
METADATA("Relation")
|
||||
}
|
||||
|
||||
string Reader::getFlavour() const
|
||||
{
|
||||
return kiwix::getMetaFlavour(*zimArchive);
|
||||
}
|
||||
|
||||
string Reader::getSource() const
|
||||
{
|
||||
METADATA("Source")
|
||||
}
|
||||
|
||||
string Reader::getScraper() const
|
||||
{
|
||||
METADATA("Scraper")
|
||||
}
|
||||
#undef METADATA
|
||||
|
||||
Entry Reader::getEntryFromPath(const std::string& path) const
|
||||
{
|
||||
try {
|
||||
return Entry(kiwix::getEntryFromPath(*zimArchive, path), true);
|
||||
} catch (zim::EntryNotFound& e) {
|
||||
throw NoEntry();
|
||||
}
|
||||
}
|
||||
|
||||
Entry Reader::getEntryFromEncodedPath(const std::string& path) const
|
||||
{
|
||||
return getEntryFromPath(urlDecode(path, true));
|
||||
}
|
||||
|
||||
Entry Reader::getEntryFromTitle(const std::string& title) const
|
||||
{
|
||||
try {
|
||||
return Entry(zimArchive->getEntryByTitle(title), true);
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
throw NoEntry();
|
||||
}
|
||||
}
|
||||
|
||||
bool Reader::pathExists(const string& path) const
|
||||
{
|
||||
return zimArchive->hasEntryByPath(path);
|
||||
}
|
||||
|
||||
/* Does the ZIM file has a fulltext index */
|
||||
bool Reader::hasFulltextIndex() const
|
||||
{
|
||||
return zimArchive->hasFulltextIndex();
|
||||
}
|
||||
|
||||
/* Search titles by prefix */
|
||||
|
||||
bool Reader::searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
const bool reset)
|
||||
{
|
||||
/* Reset the suggestions otherwise check if the suggestions number is less
|
||||
* than the suggestionsCount */
|
||||
if (reset) {
|
||||
this->suggestions.clear();
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
} else {
|
||||
if (this->suggestions.size() > suggestionsCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto ret = searchSuggestions(prefix, suggestionsCount, this->suggestions);
|
||||
|
||||
/* Set the cursor to the begining */
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
bool Reader::searchSuggestions(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& results)
|
||||
{
|
||||
bool retVal = false;
|
||||
|
||||
/* Return if no prefix */
|
||||
if (prefix.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto& entry: zimArchive->findByTitle(prefix)) {
|
||||
if (results.size() >= suggestionsCount) {
|
||||
break;
|
||||
}
|
||||
/* Extract the interesting part of article title & url */
|
||||
std::string normalizedArticleTitle
|
||||
= kiwix::normalize(entry.getTitle());
|
||||
|
||||
// Get the final path.
|
||||
auto item = entry.getItem(true);
|
||||
std::string articleFinalUrl = item.getPath();
|
||||
|
||||
/* Go through all already found suggestions and skip if this
|
||||
article is already in the suggestions list (with an other
|
||||
title) */
|
||||
bool insert = true;
|
||||
std::vector<SuggestionItem>::iterator suggestionItr;
|
||||
for (suggestionItr = results.begin();
|
||||
suggestionItr != results.end();
|
||||
suggestionItr++) {
|
||||
int result = normalizedArticleTitle.compare((*suggestionItr).getNormalizedTitle());
|
||||
if (result == 0 && articleFinalUrl.compare((*suggestionItr).getPath()) == 0) {
|
||||
insert = false;
|
||||
break;
|
||||
} else if (result < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert if possible */
|
||||
if (insert) {
|
||||
SuggestionItem suggestion(entry.getTitle(), normalizedArticleTitle, articleFinalUrl);
|
||||
results.insert(suggestionItr, suggestion);
|
||||
}
|
||||
|
||||
/* Suggestions where found */
|
||||
retVal = true;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
std::vector<std::string> Reader::getTitleVariants(
|
||||
const std::string& title) const
|
||||
{
|
||||
return kiwix::getTitleVariants(title);
|
||||
}
|
||||
|
||||
|
||||
bool Reader::searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount)
|
||||
{
|
||||
this->suggestions.clear();
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
auto ret = searchSuggestionsSmart(prefix, suggestionsCount, this->suggestions);
|
||||
|
||||
this->suggestionsOffset = this->suggestions.begin();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Try also a few variations of the prefix to have better results */
|
||||
bool Reader::searchSuggestionsSmart(const string& prefix,
|
||||
unsigned int suggestionsCount,
|
||||
SuggestionsList_t& results)
|
||||
{
|
||||
std::vector<std::string> variants = this->getTitleVariants(prefix);
|
||||
|
||||
auto suggestionSearcher = zim::SuggestionSearcher(*zimArchive);
|
||||
|
||||
if (zimArchive->hasTitleIndex()) {
|
||||
auto suggestionSearch = suggestionSearcher.suggest(prefix);
|
||||
const auto suggestions = suggestionSearch.getResults(0, suggestionsCount);
|
||||
for (auto current : suggestions) {
|
||||
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
|
||||
current.getPath(), current.getSnippet());
|
||||
results.push_back(suggestion);
|
||||
}
|
||||
} else {
|
||||
// Check some of the variants of the prefix
|
||||
for (std::vector<std::string>::iterator variantsItr = variants.begin();
|
||||
variantsItr != variants.end();
|
||||
variantsItr++) {
|
||||
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 results.size() > 0;
|
||||
}
|
||||
|
||||
/* Get next suggestion */
|
||||
bool Reader::getNextSuggestion(string& title)
|
||||
{
|
||||
if (this->suggestionsOffset != this->suggestions.end()) {
|
||||
/* title */
|
||||
title = (*(this->suggestionsOffset)).getTitle();
|
||||
|
||||
/* increment the cursor for the next call */
|
||||
this->suggestionsOffset++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Reader::getNextSuggestion(string& title, string& url)
|
||||
{
|
||||
if (this->suggestionsOffset != this->suggestions.end()) {
|
||||
/* title */
|
||||
title = (*(this->suggestionsOffset)).getTitle();
|
||||
url = (*(this->suggestionsOffset)).getPath();
|
||||
|
||||
/* increment the cursor for the next call */
|
||||
this->suggestionsOffset++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check if the file has as checksum */
|
||||
bool Reader::canCheckIntegrity() const
|
||||
{
|
||||
return zimArchive->hasChecksum();
|
||||
}
|
||||
|
||||
/* Return true if corrupted, false otherwise */
|
||||
bool Reader::isCorrupted() const
|
||||
{
|
||||
try {
|
||||
if (zimArchive->check() == true) {
|
||||
return false;
|
||||
}
|
||||
} catch (exception& e) {
|
||||
cerr << e.what() << endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Return the file size, works also for splitted files */
|
||||
unsigned int Reader::getFileSize() const
|
||||
{
|
||||
return kiwix::getArchiveFileSize(*zimArchive);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,8 +21,6 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "search_renderer.h"
|
||||
#include "searcher.h"
|
||||
#include "reader.h"
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
|
||||
@@ -38,16 +36,6 @@ namespace kiwix
|
||||
{
|
||||
|
||||
/* Constructor */
|
||||
SearchRenderer::SearchRenderer(Searcher* searcher, NameMapper* mapper)
|
||||
: SearchRenderer(
|
||||
/* srs */ searcher->getSearchResultSet(),
|
||||
/* mapper */ mapper,
|
||||
/* library */ nullptr,
|
||||
/* start */ searcher->getResultStart(),
|
||||
/* estimatedResultCount */ searcher->getEstimatedResultCount()
|
||||
)
|
||||
{}
|
||||
|
||||
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
|
||||
unsigned int start, unsigned int estimatedResultCount)
|
||||
: SearchRenderer(srs, mapper, nullptr, start, estimatedResultCount)
|
||||
@@ -142,7 +130,7 @@ kainjow::mustache::data buildPagination(
|
||||
auto nbPages = lastPage + 1;
|
||||
|
||||
auto firstPageGenerated = currentPage > 4 ? currentPage-4 : 0;
|
||||
auto lastPageGenerated = min(currentPage+4, lastPage);
|
||||
auto lastPageGenerated = std::min(currentPage+4, lastPage);
|
||||
|
||||
if (nbPages != 1) {
|
||||
if (firstPageGenerated!=0) {
|
||||
@@ -178,13 +166,14 @@ kainjow::mustache::data buildPagination(
|
||||
|
||||
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
{
|
||||
const std::string absPathPrefix = protocolPrefix + "content/";
|
||||
// Build the results list
|
||||
kainjow::mustache::data items{kainjow::mustache::data::type::list};
|
||||
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
|
||||
kainjow::mustache::data result;
|
||||
std::string zim_id(it.getZimId());
|
||||
result.set("title", it.getTitle());
|
||||
result.set("absolutePath", protocolPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
|
||||
result.set("snippet", it.getSnippet());
|
||||
if (mp_library) {
|
||||
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
|
||||
@@ -200,7 +189,7 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
|
||||
results.set("hasResults", estimatedResultCount != 0);
|
||||
results.set("start", kiwix::beautifyInteger(resultStart));
|
||||
results.set("end", kiwix::beautifyInteger(min(resultStart+pageLength-1, estimatedResultCount)));
|
||||
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount)));
|
||||
|
||||
// pagination
|
||||
auto pagination = buildPagination(
|
||||
|
||||
330
src/searcher.cpp
@@ -1,330 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Emmanuel Engelhart <kelson@kiwix.org>
|
||||
*
|
||||
* 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 "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
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
class _Result : public Result
|
||||
{
|
||||
public:
|
||||
_Result(zim::SearchResultSet::iterator iterator);
|
||||
_Result(SuggestionItem suggestionItem);
|
||||
virtual ~_Result(){};
|
||||
|
||||
virtual std::string get_url();
|
||||
virtual std::string get_title();
|
||||
virtual int get_score();
|
||||
virtual std::string get_snippet();
|
||||
virtual std::string get_content();
|
||||
virtual int get_wordCount();
|
||||
virtual int get_size();
|
||||
virtual std::string get_zimId();
|
||||
|
||||
private:
|
||||
zim::SearchResultSet::iterator iterator;
|
||||
SuggestionItem suggestionItem;
|
||||
bool isSuggestion;
|
||||
};
|
||||
|
||||
struct SearcherInternal : zim::SearchResultSet {
|
||||
explicit SearcherInternal(const zim::SearchResultSet& srs)
|
||||
: zim::SearchResultSet(srs)
|
||||
, current_iterator(srs.begin())
|
||||
{
|
||||
}
|
||||
|
||||
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),
|
||||
maxResultCount(0)
|
||||
{
|
||||
loadICUExternalTables();
|
||||
}
|
||||
|
||||
/* Destructor */
|
||||
Searcher::~Searcher()
|
||||
{
|
||||
}
|
||||
|
||||
bool Searcher::add_reader(std::shared_ptr<Reader> reader)
|
||||
{
|
||||
if (!reader->hasFulltextIndex()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ( auto existing_reader : readers ) {
|
||||
if ( existing_reader->getZimArchive()->getUuid() == reader->getZimArchive()->getUuid() )
|
||||
return false;
|
||||
}
|
||||
|
||||
this->readers.push_back(reader);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
std::shared_ptr<Reader> Searcher::get_reader(int readerIndex)
|
||||
{
|
||||
return readers.at(readerIndex);
|
||||
}
|
||||
|
||||
/* Search strings in the database */
|
||||
void Searcher::search(const std::string& search,
|
||||
unsigned int resultStart,
|
||||
unsigned int maxResultCount,
|
||||
const bool verbose)
|
||||
{
|
||||
this->reset();
|
||||
|
||||
if (verbose == true) {
|
||||
cout << "Performing query `" << search << "'" << endl;
|
||||
}
|
||||
|
||||
this->searchPattern = search;
|
||||
this->resultStart = resultStart;
|
||||
this->maxResultCount = maxResultCount;
|
||||
/* Try to find results */
|
||||
if (maxResultCount != 0) {
|
||||
/* Perform the search */
|
||||
string unaccentedSearch = removeAccents(search);
|
||||
std::vector<zim::Archive> archives;
|
||||
for (auto current = this->readers.begin(); current != this->readers.end();
|
||||
current++) {
|
||||
if ( (*current)->hasFulltextIndex() ) {
|
||||
archives.push_back(*(*current)->getZimArchive());
|
||||
}
|
||||
}
|
||||
zim::Searcher searcher(archives);
|
||||
searcher.setVerbose(verbose);
|
||||
zim::Query query;
|
||||
query.setQuery(unaccentedSearch);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
void Searcher::geo_search(float latitude, float longitude, float distance,
|
||||
unsigned int resultStart,
|
||||
unsigned int maxResultCount,
|
||||
const bool verbose)
|
||||
{
|
||||
this->reset();
|
||||
|
||||
if (verbose == true) {
|
||||
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
|
||||
}
|
||||
|
||||
/* Perform the search */
|
||||
std::ostringstream oss;
|
||||
oss << "Articles located less than " << distance << " meters of " << latitude << ";" << longitude;
|
||||
this->searchPattern = oss.str();
|
||||
this->resultStart = resultStart;
|
||||
this->maxResultCount = maxResultCount;
|
||||
|
||||
/* Try to find results */
|
||||
if (maxResultCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
searcher.setVerbose(verbose);
|
||||
zim::Query query;
|
||||
query.setQuery("");
|
||||
query.setGeorange(latitude, longitude, distance);
|
||||
zim::Search search = searcher.search(query);
|
||||
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
|
||||
this->estimatedResultCount = search.getEstimatedMatches();
|
||||
}
|
||||
|
||||
|
||||
void Searcher::restart_search()
|
||||
{
|
||||
if (internal.get()) {
|
||||
internal->current_iterator = internal->begin();
|
||||
}
|
||||
}
|
||||
|
||||
Result* Searcher::getNextResult()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/* Reset the results */
|
||||
void Searcher::reset()
|
||||
{
|
||||
this->estimatedResultCount = 0;
|
||||
this->searchPattern = "";
|
||||
return;
|
||||
}
|
||||
|
||||
void Searcher::suggestions(std::string& searchPattern, const bool verbose)
|
||||
{
|
||||
this->reset();
|
||||
|
||||
if (verbose == true) {
|
||||
cout << "Performing suggestion query `" << searchPattern << "`" << endl;
|
||||
}
|
||||
|
||||
this->searchPattern = searchPattern;
|
||||
this->resultStart = 0;
|
||||
this->maxResultCount = 10;
|
||||
string unaccentedSearch = removeAccents(searchPattern);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/* Return the result count estimation */
|
||||
unsigned int Searcher::getEstimatedResultCount()
|
||||
{
|
||||
return this->estimatedResultCount;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <zim/item.h>
|
||||
#include "server/internalServer.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
@@ -51,8 +51,6 @@ extern "C" {
|
||||
#include "tools/networkTools.h"
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include "entry.h"
|
||||
#include "searcher.h"
|
||||
#include "search_renderer.h"
|
||||
#include "opds_dumper.h"
|
||||
#include "i18n.h"
|
||||
@@ -61,6 +59,7 @@ extern "C" {
|
||||
#include <zim/error.h>
|
||||
#include <zim/entry.h>
|
||||
#include <zim/item.h>
|
||||
#include <zim/suggestion.h>
|
||||
|
||||
#include <mustache.hpp>
|
||||
|
||||
@@ -68,6 +67,7 @@ extern "C" {
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include "kiwixlib-resources.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
@@ -212,6 +212,12 @@ void checkBookNumber(const Library::BookIdSet& bookIds, size_t limit) {
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomizedResourceData
|
||||
{
|
||||
std::string mimeType;
|
||||
std::string resourceFilePath;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const RequestContext& request) const
|
||||
@@ -327,7 +333,6 @@ zim::Query SearchInfo::getZimQuery(bool verbose) const {
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
static IdNameMapper defaultNameMapper;
|
||||
|
||||
static MHD_Result staticHandlerCallback(void* cls,
|
||||
@@ -339,6 +344,27 @@ static MHD_Result staticHandlerCallback(void* cls,
|
||||
size_t* upload_data_size,
|
||||
void** cont_cls);
|
||||
|
||||
class InternalServer::CustomizedResources : public std::map<std::string, CustomizedResourceData>
|
||||
{
|
||||
public:
|
||||
CustomizedResources()
|
||||
{
|
||||
const char* fname = ::getenv("KIWIX_SERVE_CUSTOMIZED_RESOURCES");
|
||||
if ( fname )
|
||||
{
|
||||
std::cout << "Populating customized resources" << std::endl;
|
||||
std::ifstream file(fname);
|
||||
std::string url, mimeType, resourceFilePath;
|
||||
while ( file >> url >> mimeType >> resourceFilePath )
|
||||
{
|
||||
std::cout << "Got " << url << " " << mimeType << " " << resourceFilePath << std::endl;
|
||||
(*this)[url] = CustomizedResourceData{mimeType, resourceFilePath};
|
||||
}
|
||||
std::cout << "Done populating customized resources" << std::endl;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
InternalServer::InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
@@ -368,9 +394,12 @@ InternalServer::InternalServer(Library* library,
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper),
|
||||
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
|
||||
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U)))
|
||||
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
|
||||
m_customizedResources(new CustomizedResources)
|
||||
{}
|
||||
|
||||
InternalServer::~InternalServer() = default;
|
||||
|
||||
bool InternalServer::start() {
|
||||
#ifdef _WIN32
|
||||
int flags = MHD_USE_SELECT_INTERNALLY;
|
||||
@@ -495,6 +524,16 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
return ret;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool isEndpointUrl(const std::string& url, const std::string& endpoint)
|
||||
{
|
||||
return startsWith(url, "/" + endpoint + "/") || url == "/" + endpoint;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
@@ -507,36 +546,45 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
|
||||
if (startsWith(request.get_url(), "/skin/"))
|
||||
const auto url = request.get_url();
|
||||
if ( isLocallyCustomizedResource(url) )
|
||||
return handle_locally_customized_resource(request);
|
||||
|
||||
if (url == "/" )
|
||||
return build_homepage(request);
|
||||
|
||||
if (isEndpointUrl(url, "skin"))
|
||||
return handle_skin(request);
|
||||
|
||||
if (startsWith(request.get_url(), "/catalog/"))
|
||||
if (isEndpointUrl(url, "content"))
|
||||
return handle_content(request);
|
||||
|
||||
if (isEndpointUrl(url, "catalog"))
|
||||
return handle_catalog(request);
|
||||
|
||||
if (startsWith(request.get_url(), "/raw/"))
|
||||
if (isEndpointUrl(url, "raw"))
|
||||
return handle_raw(request);
|
||||
|
||||
if (request.get_url() == "/search")
|
||||
if (isEndpointUrl(url, "search"))
|
||||
return handle_search(request);
|
||||
|
||||
if (request.get_url() == "/search/searchdescription.xml") {
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::ft_opensearchdescription_xml,
|
||||
get_default_data(),
|
||||
"application/opensearchdescription+xml");
|
||||
}
|
||||
|
||||
if (request.get_url() == "/suggest")
|
||||
if (isEndpointUrl(url, "suggest"))
|
||||
return handle_suggest(request);
|
||||
|
||||
if (request.get_url() == "/random")
|
||||
if (isEndpointUrl(url, "random"))
|
||||
return handle_random(request);
|
||||
|
||||
if (request.get_url() == "/catch/external")
|
||||
return handle_captured_external(request);
|
||||
if (isEndpointUrl(url, "catch"))
|
||||
return handle_catch(request);
|
||||
|
||||
return handle_content(request);
|
||||
if (isEndpointUrl(url, "widget"))
|
||||
return handle_widget(request);
|
||||
|
||||
std::string contentUrl = m_root + "/content" + url;
|
||||
const std::string query = request.get_query();
|
||||
if ( ! query.empty() )
|
||||
contentUrl += "?" + query;
|
||||
return Response::build_redirect(*this, contentUrl);
|
||||
} catch (std::exception& e) {
|
||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||
return HTTP500Response(*this, request)
|
||||
@@ -585,46 +633,17 @@ std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& r
|
||||
* Archive and Zim handlers begin
|
||||
**/
|
||||
|
||||
SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Archive* const archive,
|
||||
const std::string& bookId, const std::string& queryString, int start, int suggestionCount)
|
||||
{
|
||||
SuggestionsList_t suggestions;
|
||||
std::shared_ptr<zim::SuggestionSearcher> searcher;
|
||||
searcher = cache.getOrPut(bookId, [=](){ return make_shared<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;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_suggest\n");
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/suggest/") ) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
std::string bookName, bookId;
|
||||
std::shared_ptr<zim::Archive> archive;
|
||||
try {
|
||||
@@ -657,9 +676,13 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
bool first = true;
|
||||
|
||||
/* Get the suggestions */
|
||||
SuggestionsList_t suggestions = getSuggestions(suggestionSearcherCache, archive.get(),
|
||||
bookId, queryString, start, count);
|
||||
for(auto& suggestion:suggestions) {
|
||||
auto searcher = suggestionSearcherCache.getOrPut(bookId,
|
||||
[=](){ return make_shared<zim::SuggestionSearcher>(*archive); }
|
||||
);
|
||||
auto search = searcher->suggest(queryString);
|
||||
auto srs = search.getResults(start, count);
|
||||
|
||||
for(auto& suggestion: srs) {
|
||||
MustacheData result;
|
||||
result.set("label", suggestion.getTitle());
|
||||
|
||||
@@ -720,6 +743,18 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
printf("** running handle_search\n");
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/search/") ) {
|
||||
if (request.get_url() == "/search/searchdescription.xml") {
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::ft_opensearchdescription_xml,
|
||||
get_default_data(),
|
||||
"application/opensearchdescription+xml");
|
||||
}
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
try {
|
||||
auto searchInfo = getSearchInfo(request);
|
||||
auto bookIds = searchInfo.getBookIds();
|
||||
@@ -803,6 +838,11 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
printf("** running handle_random\n");
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/random/") ) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
std::shared_ptr<zim::Archive> archive;
|
||||
try {
|
||||
@@ -829,6 +869,11 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_widget(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(*this, RESOURCE::templates::widget_html, get_default_data(), "text/html; charset=utf-8", true);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
|
||||
{
|
||||
std::string source = "";
|
||||
@@ -846,6 +891,20 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
|
||||
return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_catch\n");
|
||||
}
|
||||
|
||||
if ( request.get_url() == "/catch/external" ) {
|
||||
return handle_captured_external(request);
|
||||
}
|
||||
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
@@ -917,15 +976,6 @@ InternalServer::search_catalog(const RequestContext& request,
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string get_book_name(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
return request.get_url_part(0);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::string& pattern)
|
||||
{
|
||||
return ParameterizedMessage("suggest-search",
|
||||
@@ -940,7 +990,8 @@ ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::s
|
||||
std::unique_ptr<Response>
|
||||
InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const
|
||||
{
|
||||
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(item.getPath());
|
||||
const auto path = kiwix::urlEncode(item.getPath());
|
||||
const auto redirectUrl = m_root + "/content/" + bookName + "/" + path;
|
||||
return Response::build_redirect(*this, redirectUrl);
|
||||
}
|
||||
|
||||
@@ -952,9 +1003,10 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
printf("** running handle_content\n");
|
||||
}
|
||||
|
||||
const std::string bookName = get_book_name(request);
|
||||
if (bookName.empty())
|
||||
return build_homepage(request);
|
||||
const std::string contentPrefix = "/content/";
|
||||
const bool isContentPrefixedUrl = startsWith(url, contentPrefix);
|
||||
const size_t prefixLength = isContentPrefixedUrl ? contentPrefix.size() : 1;
|
||||
const std::string bookName = request.get_url_part(isContentPrefixedUrl);
|
||||
|
||||
std::shared_ptr<zim::Archive> archive;
|
||||
try {
|
||||
@@ -970,7 +1022,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
+ TaskbarInfo(bookName);
|
||||
}
|
||||
|
||||
auto urlStr = request.get_url().substr(bookName.size()+1);
|
||||
auto urlStr = url.substr(prefixLength + bookName.size());
|
||||
if (urlStr[0] == '/') {
|
||||
urlStr = urlStr.substr(1);
|
||||
}
|
||||
@@ -1067,4 +1119,34 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
}
|
||||
}
|
||||
|
||||
bool InternalServer::isLocallyCustomizedResource(const std::string& url) const
|
||||
{
|
||||
return m_customizedResources->find(url) != m_customizedResources->end();
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_locally_customized_resource\n");
|
||||
}
|
||||
|
||||
const CustomizedResourceData& crd = m_customizedResources->at(request.get_url());
|
||||
|
||||
if (m_verbose.load()) {
|
||||
std::cout << "Reading " << crd.resourceFilePath << std::endl;
|
||||
}
|
||||
const auto resourceData = getFileContent(crd.resourceFilePath);
|
||||
|
||||
auto byteRange = request.get_range().resolve(resourceData.size());
|
||||
if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) {
|
||||
return Response::build_416(*this, resourceData.size());
|
||||
}
|
||||
|
||||
return ContentResponse::build(*this,
|
||||
resourceData,
|
||||
crd.mimeType,
|
||||
/*isHomePage=*/false,
|
||||
/*raw=*/true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,9 +89,8 @@ class SearchInfo {
|
||||
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
|
||||
typedef ConcurrentCache<string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
|
||||
typedef ConcurrentCache<std::string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
|
||||
|
||||
class Entry;
|
||||
class OPDSDumper;
|
||||
|
||||
class InternalServer {
|
||||
@@ -109,7 +108,7 @@ class InternalServer {
|
||||
bool blockExternalLinks,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit);
|
||||
virtual ~InternalServer() = default;
|
||||
virtual ~InternalServer();
|
||||
|
||||
MHD_Result handlerCallback(struct MHD_Connection* connection,
|
||||
const char* url,
|
||||
@@ -139,9 +138,12 @@ class InternalServer {
|
||||
std::unique_ptr<Response> handle_search(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_random(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catch(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_captured_external(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_content(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_raw(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_locally_customized_resource(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_widget(const RequestContext& request);
|
||||
|
||||
std::vector<std::string> search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper);
|
||||
@@ -153,6 +155,8 @@ class InternalServer {
|
||||
std::pair<std::string, Library::BookIdSet> selectBooks(const RequestContext& r) const;
|
||||
SearchInfo getSearchInfo(const RequestContext& r) const;
|
||||
|
||||
bool isLocallyCustomizedResource(const std::string& url) const;
|
||||
|
||||
private: // data
|
||||
std::string m_addr;
|
||||
int m_port;
|
||||
@@ -176,6 +180,9 @@ class InternalServer {
|
||||
std::string m_server_id;
|
||||
std::string m_library_id;
|
||||
|
||||
class CustomizedResources;
|
||||
std::unique_ptr<CustomizedResources> m_customizedResources;
|
||||
|
||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage, bool raw);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw);
|
||||
|
||||
@@ -154,8 +154,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const Requ
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
const auto bookName = request.get_url_part(3);
|
||||
const auto bookId = mp_nameMapper->getIdForName(bookName);
|
||||
const auto bookId = request.get_url_part(3);
|
||||
auto book = mp_library->getBookByIdThreadSafe(bookId);
|
||||
auto size = request.get_argument<unsigned int>("size");
|
||||
auto illustration = book.getIllustration(size);
|
||||
|
||||
@@ -53,18 +53,18 @@ std::string get_mime_type(const zim::Item& item)
|
||||
{
|
||||
try {
|
||||
return item.getMimetype();
|
||||
} catch (exception& e) {
|
||||
} catch (std::exception& e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
bool is_compressible_mime_type(const std::string& mimeType)
|
||||
{
|
||||
return mimeType.find("text/") != string::npos
|
||||
|| mimeType.find("application/javascript") != string::npos
|
||||
|| mimeType.find("application/atom") != string::npos
|
||||
|| mimeType.find("application/opensearchdescription") != string::npos
|
||||
|| mimeType.find("application/json") != string::npos;
|
||||
return mimeType.find("text/") != std::string::npos
|
||||
|| mimeType.find("application/javascript") != std::string::npos
|
||||
|| mimeType.find("application/atom") != std::string::npos
|
||||
|| mimeType.find("application/opensearchdescription") != std::string::npos
|
||||
|| mimeType.find("application/json") != std::string::npos;
|
||||
}
|
||||
|
||||
bool compress(std::string &content) {
|
||||
@@ -307,7 +307,7 @@ static ssize_t callback_reader_from_item(void* cls,
|
||||
{
|
||||
RunningResponse* response = static_cast<RunningResponse*>(cls);
|
||||
|
||||
size_t max_size_to_set = min<size_t>(
|
||||
size_t max_size_to_set = std::min<size_t>(
|
||||
max,
|
||||
response->item.getSize() - pos - response->range_start);
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@
|
||||
|
||||
#include <mustache.hpp>
|
||||
#include "byte_range.h"
|
||||
#include "entry.h"
|
||||
#include "etag.h"
|
||||
#include "i18n.h"
|
||||
|
||||
#include <zim/item.h>
|
||||
|
||||
extern "C" {
|
||||
#include "microhttpd_wrapper.h"
|
||||
}
|
||||
|
||||
@@ -49,6 +49,24 @@ void kiwix::loadICUExternalTables()
|
||||
#endif
|
||||
}
|
||||
|
||||
kiwix::ICULanguageInfo::ICULanguageInfo(const std::string& langCode)
|
||||
: locale(langCode.c_str())
|
||||
{}
|
||||
|
||||
std::string kiwix::ICULanguageInfo::iso3Code() const
|
||||
{
|
||||
return locale.getISO3Language();
|
||||
}
|
||||
|
||||
std::string kiwix::ICULanguageInfo::selfName() const
|
||||
{
|
||||
icu::UnicodeString langSelfNameICUString;
|
||||
locale.getDisplayLanguage(locale, langSelfNameICUString);
|
||||
std::string langSelfName;
|
||||
langSelfNameICUString.toUTF8String(langSelfName);
|
||||
return langSelfName;
|
||||
}
|
||||
|
||||
std::string kiwix::removeAccents(const std::string& text)
|
||||
{
|
||||
loadICUExternalTables();
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#define KIWIX_STRINGTOOLS_H
|
||||
|
||||
#include <unicode/unistr.h>
|
||||
#include <unicode/locid.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -41,6 +42,19 @@ std::string encodeDiples(const std::string& str);
|
||||
std::string removeAccents(const std::string& text);
|
||||
void loadICUExternalTables();
|
||||
|
||||
class ICULanguageInfo
|
||||
{
|
||||
public:
|
||||
explicit ICULanguageInfo(const std::string& langCode);
|
||||
|
||||
std::string iso3Code() const;
|
||||
std::string selfName() const;
|
||||
|
||||
private:
|
||||
const icu::Locale locale;
|
||||
};
|
||||
|
||||
|
||||
std::string urlEncode(const std::string& value, bool encodeReserved = false);
|
||||
std::string urlDecode(const std::string& value, bool component = false);
|
||||
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
skin/jquery-ui/jquery-ui.structure.min.css
|
||||
skin/jquery-ui/jquery-ui.min.js
|
||||
skin/jquery-ui/external/jquery/jquery.js
|
||||
skin/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png
|
||||
skin/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png
|
||||
skin/jquery-ui/images/ui-icons_222222_256x240.png
|
||||
skin/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png
|
||||
skin/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png
|
||||
skin/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png
|
||||
skin/jquery-ui/images/ui-icons_2e83ff_256x240.png
|
||||
skin/jquery-ui/images/ui-icons_cd0a0a_256x240.png
|
||||
skin/jquery-ui/images/ui-icons_888888_256x240.png
|
||||
skin/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png
|
||||
skin/jquery-ui/images/animated-overlay.gif
|
||||
skin/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png
|
||||
skin/jquery-ui/images/ui-icons_454545_256x240.png
|
||||
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
|
||||
@@ -27,6 +8,8 @@ skin/taskbar.js
|
||||
skin/iso6391To3.js
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
skin/autoComplete.min.js
|
||||
skin/widget.js
|
||||
skin/taskbar.css
|
||||
skin/index.css
|
||||
skin/fonts/Poppins.ttf
|
||||
@@ -38,6 +21,7 @@ templates/search_result.xml
|
||||
templates/error.html
|
||||
templates/error.xml
|
||||
templates/index.html
|
||||
templates/widget.html
|
||||
templates/suggestion.json
|
||||
templates/head_taskbar.html
|
||||
templates/taskbar_part.html
|
||||
@@ -53,3 +37,19 @@ templates/url_of_search_results_css
|
||||
opensearchdescription.xml
|
||||
ft_opensearchdescription.xml
|
||||
catalog_v2_searchdescription.xml
|
||||
skin/css/autoComplete.css
|
||||
skin/css/images/search.svg
|
||||
skin/favicon/android-chrome-192x192.png
|
||||
skin/favicon/android-chrome-512x512.png
|
||||
skin/favicon/apple-touch-icon.png
|
||||
skin/favicon/browserconfig.xml
|
||||
skin/favicon/favicon-16x16.png
|
||||
skin/favicon/favicon-32x32.png
|
||||
skin/favicon/favicon.ico
|
||||
skin/favicon/mstile-70x70.png
|
||||
skin/favicon/mstile-144x144.png
|
||||
skin/favicon/mstile-150x150.png
|
||||
skin/favicon/mstile-310x150.png
|
||||
skin/favicon/mstile-310x310.png
|
||||
skin/favicon/safari-pinned-tab.svg
|
||||
skin/favicon/site.webmanifest
|
||||
|
||||
1
static/skin/autoComplete.min.js
vendored
Normal file
91
static/skin/css/autoComplete.css
Normal file
@@ -0,0 +1,91 @@
|
||||
.autoComplete_wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input {
|
||||
width: 370px;
|
||||
height: 40px;
|
||||
padding-left: 20px;
|
||||
font-size: 1rem;
|
||||
color: rgba(123, 123, 123, 1);
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > input::placeholder {
|
||||
color: rgba(123, 123, 123, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul {
|
||||
position: absolute;
|
||||
max-height: 226px;
|
||||
overflow-y: scroll;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0;
|
||||
border-radius: 0.6rem;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 3px 6px rgba(149, 157, 165, 0.15);
|
||||
border: 1px solid rgba(33, 33, 33, 0.07);
|
||||
z-index: 1000;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul[hidden],
|
||||
.autoComplete_wrapper > ul:empty {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li {
|
||||
margin: 0.3rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: #212121;
|
||||
border-radius: 0.35rem;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li::selection {
|
||||
color: rgba(#ffffff, 0);
|
||||
background-color: rgba(#ffffff, 0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(123, 123, 123, 0.1);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark {
|
||||
background-color: transparent;
|
||||
color: rgba(255, 122, 122, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li mark::selection {
|
||||
color: rgba(#ffffff, 0);
|
||||
background-color: rgba(#ffffff, 0);
|
||||
}
|
||||
|
||||
.autoComplete_wrapper > ul > li[aria-selected="true"] {
|
||||
background-color: rgba(123, 123, 123, 0.1);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.autoComplete_wrapper > input {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
||||
8
static/skin/css/images/search.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" x="0px" y="0px" width="30" height="30" viewBox="0 0 171 171" style=" fill:#000000;">
|
||||
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal">
|
||||
<path d="M0,171.99609v-171.99609h171.99609v171.99609z" fill="none"></path>
|
||||
<g fill="#ff7a7a">
|
||||
<path d="M74.1,17.1c-31.41272,0 -57,25.58728 -57,57c0,31.41272 25.58728,57 57,57c13.6601,0 26.20509,-4.85078 36.03692,-12.90293l34.03301,34.03301c1.42965,1.48907 3.55262,2.08891 5.55014,1.56818c1.99752,-0.52073 3.55746,-2.08067 4.07819,-4.07819c0.52073,-1.99752 -0.0791,-4.12049 -1.56818,-5.55014l-34.03301,-34.03301c8.05215,-9.83182 12.90293,-22.37682 12.90293,-36.03692c0,-31.41272 -25.58728,-57 -57,-57zM74.1,28.5c25.2517,0 45.6,20.3483 45.6,45.6c0,25.2517 -20.3483,45.6 -45.6,45.6c-25.2517,0 -45.6,-20.3483 -45.6,-45.6c0,-25.2517 20.3483,-45.6 45.6,-45.6z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/skin/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/skin/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
static/skin/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
13
static/skin/favicon/browserconfig.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="skin/favicon/mstile-70x70.png"/>
|
||||
<square144x144logo src="skin/favicon/mstile-144x144.png"/>
|
||||
<square150x150logo src="skin/favicon/mstile-150x150.png"/>
|
||||
<square310x150logo src="skin/favicon/mstile-310x150.png"/>
|
||||
<square310x310logo src="skin/favicon/mstile-310x310.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
static/skin/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 632 B |
BIN
static/skin/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
static/skin/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/skin/favicon/mstile-144x144.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
static/skin/favicon/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/skin/favicon/mstile-310x150.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/skin/favicon/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
static/skin/favicon/mstile-70x70.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
47
static/skin/favicon/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2130 5897 c-3 -3 -38 -7 -78 -11 -41 -3 -77 -8 -80 -10 -4 -2 -27
|
||||
-7 -52 -10 -25 -3 -73 -13 -105 -22 -33 -8 -67 -17 -75 -19 -51 -11 -211 -75
|
||||
-305 -120 -93 -45 -289 -163 -305 -184 -3 -3 -27 -24 -55 -46 -108 -86 -238
|
||||
-229 -323 -357 -48 -71 -142 -245 -142 -263 0 -4 -6 -21 -14 -38 -21 -46 -51
|
||||
-149 -72 -247 -19 -89 -26 -391 -11 -475 3 -22 9 -56 12 -75 46 -278 218 -619
|
||||
460 -909 97 -117 141 -182 172 -256 19 -45 85 -251 118 -365 7 -25 35 -119 63
|
||||
-210 144 -469 182 -592 184 -607 3 -14 -10 -16 -77 -18 -93 -2 -140 -20 -185
|
||||
-71 -28 -33 -57 -110 -46 -122 4 -3 109 -6 234 -7 l227 -1 105 -51 c154 -74
|
||||
310 -148 440 -209 63 -30 133 -63 155 -74 25 -12 43 -16 49 -10 6 6 10 39 10
|
||||
74 1 56 -3 70 -29 108 -25 36 -49 52 -142 98 -61 30 -114 57 -116 60 -3 3 120
|
||||
5 273 5 l278 0 -3 25 c-9 69 -45 119 -110 153 -38 19 -58 20 -345 22 -272 1
|
||||
-309 3 -343 19 -48 24 -102 86 -115 134 -5 20 -42 143 -82 272 -40 129 -88
|
||||
287 -107 350 -19 63 -46 153 -60 200 -48 157 -30 273 54 357 65 65 153 95 237
|
||||
79 45 -9 96 -60 130 -132 20 -41 286 -885 286 -905 0 -2 -37 -5 -83 -5 -130
|
||||
-2 -196 -47 -227 -155 -7 -23 -5 -31 8 -37 9 -5 112 -8 227 -7 l210 1 110 -52
|
||||
c61 -28 167 -79 237 -112 70 -34 147 -70 172 -82 25 -11 87 -40 138 -65 54
|
||||
-26 96 -41 101 -36 19 19 20 100 3 146 -23 59 -63 91 -178 145 -54 25 -98 47
|
||||
-98 50 0 3 88 6 196 5 l196 0 60 -80 c208 -276 501 -419 838 -410 381 10 721
|
||||
235 881 582 80 174 109 431 69 614 -102 469 -504 798 -976 797 -446 0 -828
|
||||
-283 -958 -708 -35 -114 -45 -194 -41 -325 4 -93 10 -141 37 -255 5 -22 -465
|
||||
-24 -532 -3 -69 23 -114 78 -144 176 -210 687 -376 1234 -376 1241 0 9 88 51
|
||||
155 74 107 37 218 62 350 79 117 15 593 8 705 -11 14 -2 61 -7 105 -11 44 -3
|
||||
94 -8 111 -10 160 -20 504 -27 624 -11 240 30 439 120 611 273 102 91 142 113
|
||||
207 113 75 0 142 -25 252 -94 334 -208 677 -541 882 -856 46 -71 158 -266 158
|
||||
-275 0 -2 15 -36 34 -77 18 -40 41 -95 50 -121 l18 -48 31 19 c50 29 87 90 94
|
||||
153 10 95 -33 204 -168 434 -82 140 -272 379 -429 540 -214 220 -385 364 -669
|
||||
564 -77 54 -138 100 -136 103 2 2 7 42 11 89 26 336 -200 696 -540 858 -131
|
||||
63 -205 82 -346 88 -128 5 -216 -9 -332 -53 l-68 -26 -43 38 c-23 21 -44 40
|
||||
-47 44 -22 27 -248 190 -348 252 -133 81 -408 208 -452 208 -9 0 -20 4 -26 9
|
||||
-9 9 -189 56 -254 66 -19 3 -60 10 -90 16 -54 10 -421 24 -430 16z m2316
|
||||
-1050 c97 -49 163 -150 165 -253 2 -87 -33 -199 -39 -126 -6 72 -105 152 -187
|
||||
152 -95 0 -185 -84 -190 -177 -3 -50 14 -104 43 -137 16 -18 16 -19 -13 -12
|
||||
-16 4 -51 21 -76 37 -119 76 -167 241 -107 369 27 58 48 84 100 123 81 61 211
|
||||
71 304 24z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
19
static/skin/favicon/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
(function() {
|
||||
const root = $(`link[type='root']`).attr('href');
|
||||
const kiwixServe = (function() {
|
||||
const root = document.querySelector(`link[type='root']`).getAttribute('href');
|
||||
const incrementalLoadingParams = {
|
||||
start: 0,
|
||||
count: viewPortToCount()
|
||||
@@ -17,6 +17,7 @@
|
||||
let params = new URLSearchParams(window.location.search || filters || '');
|
||||
let timer;
|
||||
let languages = {};
|
||||
let allowBookClick = true;
|
||||
|
||||
function queryUrlBuilder() {
|
||||
let url = `${root}/catalog/search?`;
|
||||
@@ -77,8 +78,15 @@
|
||||
return queryNode != null ? queryNode.innerHTML : "";
|
||||
}
|
||||
|
||||
function generateTagLink(tagValue) {
|
||||
tagValue = tagValue.toLowerCase();
|
||||
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
|
||||
const tagMessage = `Filter by tag "${humanFriendlyTagValue}"`;
|
||||
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${tagValue}>${humanFriendlyTagValue}</span>`
|
||||
}
|
||||
|
||||
function generateBookHtml(book, sort = false) {
|
||||
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
|
||||
let 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) {
|
||||
@@ -91,9 +99,9 @@
|
||||
const langCode = getInnerHtml(book, 'language');
|
||||
const language = languages[langCode];
|
||||
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, ' ');
|
||||
const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))});
|
||||
const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue));
|
||||
const tagHtml = tagFilterLinks.join(' | ');
|
||||
let downloadLink;
|
||||
let zimSize = 0;
|
||||
try {
|
||||
@@ -113,17 +121,24 @@
|
||||
}
|
||||
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
|
||||
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
||||
divTag.innerHTML = `<a class="book__link" href="${link}" data-hover="Preview">
|
||||
if (!allowBookClick) {
|
||||
link = "javascript:void(0)";
|
||||
}
|
||||
divTag.innerHTML = `
|
||||
<div class="book__wrapper">
|
||||
<a class="book__link" href="${link}" data-hover="Preview">
|
||||
<div class="book__link__wrapper">
|
||||
<div class="book__icon" ${faviconAttr}></div>
|
||||
<div class="book__header">
|
||||
<div id="book__title">${title}</div>
|
||||
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">Download ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
|
||||
</div>
|
||||
<div class="book__description" title="${description}">${description}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
|
||||
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>
|
||||
</div></div></a>`;
|
||||
</div></div>`;
|
||||
return divTag;
|
||||
}
|
||||
|
||||
@@ -236,14 +251,16 @@
|
||||
toggleFooter();
|
||||
}
|
||||
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
|
||||
if (results) {
|
||||
let resultText = `${results} books`;
|
||||
if (results === 1) {
|
||||
resultText = `${results} book`;
|
||||
if (kiwixResultText) {
|
||||
if (results) {
|
||||
let resultText = `${results} books`;
|
||||
if (results === 1) {
|
||||
resultText = `${results} book`;
|
||||
}
|
||||
kiwixResultText.innerHTML = resultText;
|
||||
} else {
|
||||
kiwixResultText.innerHTML = ``;
|
||||
}
|
||||
kiwixResultText.innerHTML = resultText;
|
||||
} else {
|
||||
kiwixResultText.innerHTML = ``;
|
||||
}
|
||||
loader.style.display = 'none';
|
||||
return books;
|
||||
@@ -254,19 +271,33 @@
|
||||
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);
|
||||
const hfTitle = humanFriendlyTitle(title);
|
||||
if (valueEntryNode == 'language') {
|
||||
languages[value] = hfTitle;
|
||||
}
|
||||
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
|
||||
});
|
||||
document.querySelector(nodeQuery).innerHTML += optionStr;
|
||||
const entryList = data.querySelectorAll('entry');
|
||||
const nodeQueryElem = document.querySelector(nodeQuery);
|
||||
if (entryList && nodeQueryElem) {
|
||||
entryList.forEach(entry => {
|
||||
const title = getInnerHtml(entry, 'title');
|
||||
const value = getInnerHtml(entry, valueEntryNode);
|
||||
const hfTitle = humanFriendlyTitle(title);
|
||||
if (valueEntryNode == 'language') {
|
||||
languages[value] = hfTitle;
|
||||
}
|
||||
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
|
||||
});
|
||||
nodeQueryElem.innerHTML += optionStr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setNoResultsContent() {
|
||||
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
|
||||
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');
|
||||
loader.setAttribute('style', 'position: absolute; top: 50%');
|
||||
}
|
||||
|
||||
function checkAndInjectEmptyMessage() {
|
||||
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
|
||||
if (!bookOrderMap.size) {
|
||||
@@ -274,28 +305,7 @@
|
||||
noResultInjected = true;
|
||||
iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('div'));
|
||||
iso.layout();
|
||||
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);
|
||||
setTimeout(setNoResultsContent, 300);
|
||||
}
|
||||
return true;
|
||||
} else if (noResultInjected) {
|
||||
@@ -320,7 +330,7 @@
|
||||
const booksToFilter = new Set();
|
||||
const booksToDelete = new Set();
|
||||
iso.arrange({
|
||||
filter: function (idx, elem) {
|
||||
filter: function (elem) {
|
||||
const id = elem.getAttribute('data-id');
|
||||
const retVal = bookOrderMap.has(id);
|
||||
if (retVal) {
|
||||
@@ -344,6 +354,7 @@
|
||||
insertModal(downloadButton);
|
||||
}
|
||||
});
|
||||
refreshTagLinks();
|
||||
}
|
||||
|
||||
async function resetAndFilter(filterType = '', filterValue = '') {
|
||||
@@ -355,22 +366,16 @@
|
||||
params = new URLSearchParams(window.location.search);
|
||||
if (filterType) {
|
||||
params.set(filterType, filterValue);
|
||||
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
|
||||
window.history.pushState({}, null, `?${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';
|
||||
}
|
||||
});
|
||||
updateFilterColors();
|
||||
await loadAndDisplayBooks(true);
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', async () => {
|
||||
await resetAndFilter();
|
||||
document.querySelectorAll('.filter').forEach(filter => {filter.value = params.get(filter.name) || ''});
|
||||
updateVisibleParams();
|
||||
});
|
||||
|
||||
async function loadSubset() {
|
||||
@@ -384,16 +389,78 @@
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', (event) => {
|
||||
function updateFilterColors() {
|
||||
document.querySelectorAll('.filter').forEach(filter => {
|
||||
if (filter.value) {
|
||||
filter.style = 'background-color: #858585; color: #fff';
|
||||
} else {
|
||||
filter.style = 'background-color: #ffffff; color: black';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disableBookClick() {
|
||||
allowBookClick = false;
|
||||
}
|
||||
|
||||
function addTagElement(tagValue, resetFilter) {
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.style.display = 'inline-block';
|
||||
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
|
||||
tagElement.innerHTML = `${humanFriendlyTagValue}`;
|
||||
const tagMessage = `Stop filtering by tag "${humanFriendlyTagValue}"`;
|
||||
tagElement.setAttribute('aria-label', tagMessage);
|
||||
tagElement.setAttribute('title', tagMessage);
|
||||
if (resetFilter)
|
||||
resetAndFilter('tag', tagValue);
|
||||
}
|
||||
|
||||
function refreshTagLinks() {
|
||||
const tagLinks = document.getElementsByClassName('tag__link');
|
||||
[...tagLinks].forEach(elem => {
|
||||
if (!elem.getAttribute('click-listener')) {
|
||||
elem.addEventListener('click', () => addTagElement(elem.dataset.tag, true));
|
||||
elem.setAttribute('click-listener', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagElement(resetFilter) {
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.style.display = 'none';
|
||||
if (resetFilter)
|
||||
resetAndFilter('tag', '');
|
||||
}
|
||||
|
||||
function updateVisibleParams() {
|
||||
document.querySelectorAll('.filter').forEach(filter => {filter.value = params.get(filter.name) || ''});
|
||||
updateFilterColors();
|
||||
const tagKey = params.get('tag');
|
||||
if (tagKey !== null && tagKey.trim() !== '') {
|
||||
addTagElement(tagKey, false);
|
||||
} else {
|
||||
removeTagElement(false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBookCount(event) {
|
||||
if (timer) {clearTimeout(timer)}
|
||||
timer = setTimeout(() => {
|
||||
incrementalLoadingParams.count = incrementalLoadingParams.count && viewPortToCount();
|
||||
loadSubset();
|
||||
}, 100, event);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', (event) => updateBookCount(event));
|
||||
|
||||
window.addEventListener('scroll', loadSubset);
|
||||
|
||||
window.addEventListener('keydown', function (event) {
|
||||
if (event.key === "Escape" ) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
window.onload = async () => {
|
||||
iso = new Isotope( '.book__list', {
|
||||
itemSelector: '.book',
|
||||
@@ -404,10 +471,9 @@
|
||||
}
|
||||
},
|
||||
sortBy: 'weight',
|
||||
layoutMode: 'cellsByRow',
|
||||
cellsByRow: {
|
||||
columnWidth: '.book',
|
||||
rowHeight: '.book'
|
||||
layoutMode: 'masonry',
|
||||
masonry: {
|
||||
fitWidth: true
|
||||
}
|
||||
});
|
||||
footer = document.getElementById('kiwixfooter');
|
||||
@@ -419,15 +485,17 @@
|
||||
document.querySelectorAll('.filter').forEach(filter => {
|
||||
filter.addEventListener('change', () => {resetAndFilter(filter.name, filter.value)});
|
||||
});
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.addEventListener('click', () => removeTagElement(true));
|
||||
if (filters) {
|
||||
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
|
||||
}
|
||||
params.forEach((value, key) => {
|
||||
const selectBox = document.getElementsByName(key)[0];
|
||||
if (selectBox) {
|
||||
selectBox.value = value
|
||||
const currentLink = window.location.search;
|
||||
const newLink = `?${params.toString()}`;
|
||||
if (currentLink != newLink) {
|
||||
window.history.pushState({}, null, newLink);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateVisibleParams();
|
||||
updateBookCount();
|
||||
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
|
||||
if (!window.location.search) {
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
@@ -438,13 +506,12 @@
|
||||
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());
|
||||
}
|
||||
|
||||
return {
|
||||
updateBookCount,
|
||||
disableBookClick
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
9789
static/skin/jquery-ui/external/jquery/jquery.js
vendored
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 212 B |
|
Before Width: | Height: | Size: 208 B |
|
Before Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 280 B |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
16150
static/skin/jquery-ui/jquery-ui.js
vendored
7
static/skin/jquery-ui/jquery-ui.min.css
vendored
13
static/skin/jquery-ui/jquery-ui.min.js
vendored
@@ -9,6 +9,8 @@
|
||||
transition: 0.3s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #e3e3e3;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
#kiwixtoolbar>a {
|
||||
@@ -100,7 +102,6 @@ label[for="kiwix_button_show_toggle"],
|
||||
}
|
||||
|
||||
.kiwix #kiwixtoolbar #kiwixsearchform input[type='text'] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
box-sizing: border-box !important;
|
||||
width: 100%;
|
||||
@@ -123,20 +124,19 @@ label[for=kiwixsearchbox] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: calc(3em - 5px) !important;
|
||||
a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Try to fix buggy stuff in jquery-ui autocomplete */
|
||||
#ui-id-1,
|
||||
.ui-autocomplete {
|
||||
background: white !important;
|
||||
border: solid 1px grey !important;
|
||||
.autoComplete_wrapper > ul {
|
||||
max-height: none !important;
|
||||
column-count: 1 !important;
|
||||
}
|
||||
|
||||
li.ui-state-focus {
|
||||
font-weight: bold;
|
||||
body {
|
||||
padding-top: calc(3em - 5px) !important;
|
||||
}
|
||||
|
||||
@media(min-width:420px) {
|
||||
@@ -155,6 +155,11 @@ li.ui-state-focus {
|
||||
|
||||
@media (max-width: 645px) {
|
||||
|
||||
.autoComplete_wrapper > ul {
|
||||
max-height: none !important;
|
||||
column-count: 1 !important;
|
||||
}
|
||||
|
||||
#kiwix_button_show_toggle~label~.kiwix_button_cont.searching {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -3,95 +3,120 @@ function htmlDecode(input) {
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
|
||||
const jq = jQuery.noConflict(true);
|
||||
jq(document).ready(() => {
|
||||
(function ($) {
|
||||
const root = $( `link[type='root']` ).attr("href");
|
||||
function setupAutoHidingOfTheToolbar() {
|
||||
let lastScrollTop = 0;
|
||||
const delta = 5;
|
||||
let didScroll = false;
|
||||
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
|
||||
|
||||
const bookName = (window.location.pathname == `${root}/search`)
|
||||
? (new URLSearchParams(window.location.search)).get('content')
|
||||
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
|
||||
|
||||
const userlang = (new URLSearchParams(window.location.search)).get('userlang') || "en";
|
||||
$( "#kiwixsearchbox" ).autocomplete({
|
||||
|
||||
source: `${root}/suggest?content=${bookName}&userlang=${userlang}`,
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
|
||||
response: function( event, ui ) {
|
||||
for(const item of ui.content) {
|
||||
item.label = htmlDecode(item.label);
|
||||
item.value = htmlDecode(item.value);
|
||||
if (item.path) item.path = htmlDecode(item.path);
|
||||
}
|
||||
},
|
||||
|
||||
select: function(event, ui) {
|
||||
if (ui.item.kind === 'path') {
|
||||
window.location.href = `${root}/${bookName}/${encodeURI(ui.item.path)}`;
|
||||
} else {
|
||||
$( "#kiwixsearchbox" ).val(ui.item.value);
|
||||
$( "#kiwixsearchform" ).submit();
|
||||
}
|
||||
},
|
||||
}).data( "ui-autocomplete" )._renderItem = function( ul, item ) {
|
||||
return $( "<li>" )
|
||||
.data( "ui-autocomplete-item", item )
|
||||
.append( item.label )
|
||||
.appendTo( ul );
|
||||
};
|
||||
|
||||
/* cybook hack */
|
||||
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
||||
$("html").addClass("cybook");
|
||||
}
|
||||
|
||||
if ($(window).width() < 520) {
|
||||
var didScroll;
|
||||
var lastScrollTop = 0;
|
||||
var delta = 5;
|
||||
// on scroll, let the interval function know the user has scrolled
|
||||
$(window).scroll(function (event) {
|
||||
didScroll = true;
|
||||
});
|
||||
// run hasScrolled() and reset didScroll status
|
||||
setInterval(function () {
|
||||
if (didScroll) {
|
||||
hasScrolled();
|
||||
didScroll = false;
|
||||
}
|
||||
}, 250);
|
||||
function hasScrolled() {
|
||||
var st = $(this).scrollTop();
|
||||
|
||||
// Make sure they scroll more than delta
|
||||
if (Math.abs(lastScrollTop - st) <= delta)
|
||||
return;
|
||||
|
||||
// If they scrolled down and are past the navbar, add class .nav-up.
|
||||
// This is necessary so you never see what is "behind" the navbar.
|
||||
if (st > lastScrollTop) {
|
||||
// Scroll Down
|
||||
$('#kiwixtoolbar').css({ top: '-100%' });
|
||||
} else {
|
||||
// Scroll Up
|
||||
$('#kiwixtoolbar').css({ top: '0' });
|
||||
}
|
||||
|
||||
lastScrollTop = st;
|
||||
}
|
||||
}
|
||||
|
||||
$('#kiwixsearchbox').on({
|
||||
focus: function () {
|
||||
$('.kiwix_searchform').addClass('full_width');
|
||||
$('label[for="kiwix_button_show_toggle"], .kiwix_button_cont').addClass('searching');
|
||||
},
|
||||
blur: function () {
|
||||
$('.kiwix_searchform').removeClass('full_width');
|
||||
$('label[for="kiwix_button_show_toggle"], .kiwix_button_cont').removeClass('searching');
|
||||
}
|
||||
});
|
||||
})(jq);
|
||||
})
|
||||
window.addEventListener('scroll', () => {
|
||||
didScroll = true;
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
if (didScroll) {
|
||||
hasScrolled();
|
||||
didScroll = false;
|
||||
}
|
||||
}, 250);
|
||||
|
||||
function hasScrolled() {
|
||||
const st = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
if (Math.abs(lastScrollTop - st) <= delta)
|
||||
return;
|
||||
|
||||
if (st > lastScrollTop) {
|
||||
kiwixToolBar.style.top = '-100%';
|
||||
} else {
|
||||
kiwixToolBar.style.top = '0';
|
||||
}
|
||||
|
||||
lastScrollTop = st;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const root = document.querySelector(`link[type='root']`).getAttribute("href");
|
||||
const bookName = (window.location.pathname == `${root}/search`)
|
||||
? (new URLSearchParams(window.location.search)).get('content')
|
||||
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
|
||||
|
||||
const autoCompleteJS = new autoComplete(
|
||||
{
|
||||
selector: "#kiwixsearchbox",
|
||||
placeHolder: document.querySelector("#kiwixsearchbox").title,
|
||||
threshold: 1,
|
||||
debounce: 300,
|
||||
data : {
|
||||
src: async (query) => {
|
||||
try {
|
||||
// Fetch Data from external Source
|
||||
const source = await fetch(`${root}/suggest?content=${encodeURIComponent(bookName)}&term=${encodeURIComponent(query)}`);
|
||||
const data = await source.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
},
|
||||
keys: ['label'],
|
||||
},
|
||||
submit: true,
|
||||
searchEngine: (query, record) => {
|
||||
// We accept all records
|
||||
return true;
|
||||
},
|
||||
resultsList: {
|
||||
noResults: true,
|
||||
/* We must display 10 results (requested) + 1 potential link to do a full text search. */
|
||||
maxResults: 11,
|
||||
},
|
||||
resultItem: {
|
||||
element: (item, data) => {
|
||||
let searchLink;
|
||||
if (data.value.kind == "path") {
|
||||
searchLink = `${root}/${bookName}/${htmlDecode(data.value.path)}`;
|
||||
} else {
|
||||
searchLink = `${root}/search?content=${encodeURIComponent(bookName)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
|
||||
}
|
||||
item.innerHTML = `<a class="suggest" href="${searchLink}">${htmlDecode(data.value.label)}</a>`;
|
||||
},
|
||||
highlight: "autoComplete_highlight",
|
||||
selected: "autoComplete_selected"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
document.querySelector('#kiwixsearchform').addEventListener('submit', function(event) {
|
||||
try {
|
||||
const selectedElemLink = document.querySelector('.autoComplete_selected > a').href;
|
||||
if (selectedElemLink) {
|
||||
event.preventDefault();
|
||||
window.location = selectedElemLink;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
const kiwixSearchBox = document.querySelector('#kiwixsearchbox');
|
||||
const kiwixSearchForm = document.querySelector('.kiwix_searchform');
|
||||
kiwixSearchBox.addEventListener('focus', () => {
|
||||
kiwixSearchForm.classList.add('full_width');
|
||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.add('searching');
|
||||
document.querySelector('.kiwix_button_cont').classList.add('searching');
|
||||
});
|
||||
kiwixSearchBox.addEventListener('blur', () => {
|
||||
kiwixSearchForm.classList.remove('full_width');
|
||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.remove('searching');
|
||||
document.querySelector('.kiwix_button_cont').classList.remove('searching');
|
||||
});
|
||||
|
||||
// cybook hack
|
||||
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
||||
document.querySelector('html').classList.add('cybook');
|
||||
}
|
||||
|
||||
if (document.body.clientWidth < 520) {
|
||||
setupAutoHidingOfTheToolbar();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
107
static/skin/widget.js
Normal file
@@ -0,0 +1,107 @@
|
||||
function disableSearchFilters(widgetStyles) {
|
||||
const hideNavRule = `
|
||||
.kiwixNav {
|
||||
display: none;
|
||||
}`;
|
||||
const hideResultsLabelRule = `
|
||||
.kiwixHomeBody__results {
|
||||
display: none;
|
||||
}`;
|
||||
const hideTagFilterRule = `
|
||||
.book__tags {
|
||||
pointer-events: none;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [hideNavRule, hideResultsLabelRule, hideTagFilterRule]);
|
||||
}
|
||||
|
||||
function disableBookClick() {
|
||||
kiwixServe.disableBookClick();
|
||||
}
|
||||
|
||||
function disableDownload(widgetStyles) {
|
||||
const hideBookDownloadRule = `
|
||||
.book__download {
|
||||
display: none;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [hideBookDownloadRule]);
|
||||
}
|
||||
|
||||
function disableDescription(widgetStyles) {
|
||||
const decreaseHeightRule = `
|
||||
.book__wrapper {
|
||||
height:128px;
|
||||
grid-template-rows: 70px 0 1fr 1fr;
|
||||
}`;
|
||||
const hideDescRule = `
|
||||
.book__description {
|
||||
display: none;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [decreaseHeightRule, hideDescRule]);
|
||||
}
|
||||
|
||||
function hideFooter(widgetStyles) {
|
||||
const hideFooterRule = `
|
||||
.kiwixfooter {
|
||||
display: none !important;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [hideFooterRule]);
|
||||
}
|
||||
|
||||
function insertNewCssRules(stylesheet, ruleList) {
|
||||
if (stylesheet) {
|
||||
for (rule of ruleList) {
|
||||
stylesheet.insertRule(rule, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomCss(cssCode) {
|
||||
let customCSS = document.createElement('style');
|
||||
customCSS.innerHTML = cssCode;
|
||||
document.head.appendChild(customCSS);
|
||||
}
|
||||
|
||||
function addCustomJs(jsCode) {
|
||||
new Function(`"use strict";${jsCode}`)();
|
||||
}
|
||||
|
||||
function handleMessages(event) {
|
||||
if ('css' in event.data) {
|
||||
addCustomCss(event.data.css);
|
||||
}
|
||||
if ('js' in event.data) {
|
||||
addCustomJs(event.data.js);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWidget() {
|
||||
const params = new URLSearchParams(window.location.search || filters || '');
|
||||
const widgetStyleElem = document.createElement('style');
|
||||
document.head.appendChild(widgetStyleElem);
|
||||
|
||||
const widgetStyles = widgetStyleElem.sheet;
|
||||
|
||||
const disableFilters = params.has('disablefilter');
|
||||
const disableClick = params.has('disableclick');
|
||||
const disableDwld = params.has('disabledownload');
|
||||
const disableDesc = params.has('disabledesc');
|
||||
|
||||
const blankBase = document.createElement('base');
|
||||
blankBase.target = '_blank';
|
||||
document.head.appendChild(blankBase); // open all links in new tab
|
||||
|
||||
if (disableFilters)
|
||||
disableSearchFilters(widgetStyles);
|
||||
if (disableClick)
|
||||
disableBookClick();
|
||||
if (disableDwld)
|
||||
disableDownload(widgetStyles);
|
||||
if (disableDesc)
|
||||
disableDescription(widgetStyles);
|
||||
|
||||
hideFooter(widgetStyles);
|
||||
kiwixServe.updateBookCount();
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessages);
|
||||
handleWidget();
|
||||
@@ -16,9 +16,9 @@
|
||||
<articleCount>{{article_count}}</articleCount>
|
||||
<mediaCount>{{media_count}}</mediaCount>
|
||||
{{#icons}}<link rel="http://opds-spec.org/image/thumbnail"
|
||||
href="{{root}}/catalog/v2/illustration/{{{content_id}}}/?size={{icon_size}}"
|
||||
href="{{root}}/catalog/v2/illustration/{{{id}}}/?size={{icon_size}}"
|
||||
type="{{icon_mimetype}};width={{icon_size}};height={{icon_size}};scale=1"/>
|
||||
{{/icons}}<link type="text/html" href="{{root}}/{{{content_id}}}" />
|
||||
{{/icons}}<link type="text/html" href="{{root}}/content/{{{content_id}}}" />
|
||||
<author>
|
||||
<name>{{author_name}}</name>
|
||||
</author>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.min.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="{{root}}/skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="{{root}}/skin/jquery-ui/external/jquery/jquery.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/jquery-ui/jquery-ui.min.js?KIWIXCACHEID" defer></script>
|
||||
<link type="text/css" href="{{root}}/skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="{{root}}/skin/taskbar.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/autoComplete.min.js?KIWIXCACHEID"></script>
|
||||
|
||||
@@ -4,29 +4,20 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Welcome to Kiwix Server</title>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{root}}/skin/jquery-ui/external/jquery/jquery.js?KIWIXCACHEID"
|
||||
></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{{root}}/skin/jquery-ui/jquery-ui.min.js?KIWIXCACHEID"
|
||||
></script>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/jquery-ui/jquery-ui.min.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/index.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{root}}/skin/favicon/apple-touch-icon.png?KIWIXCACHEID">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{root}}/skin/favicon/favicon-32x32.png?KIWIXCACHEID">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{root}}/skin/favicon/favicon-16x16.png?KIWIXCACHEID">
|
||||
<link rel="manifest" href="{{root}}/skin/favicon/site.webmanifest">
|
||||
<link rel="mask-icon" href="{{root}}/skin/favicon/safari-pinned-tab.svg?KIWIXCACHEID" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="{{root}}/skin/favicon/favicon.ico?KIWIXCACHEID">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="msapplication-config" content="{{root}}/skin/favicon/browserconfig.xml?KIWIXCACHEID">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "poppins";
|
||||
@@ -58,12 +49,14 @@
|
||||
</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"/>
|
||||
<span class="kiwixButton tagFilterLabel"></span>
|
||||
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="kiwixHomeBody">
|
||||
<h3 class="kiwixHomeBody__results"></h3>
|
||||
<div class="book__list"></div>
|
||||
<div class="book__list">
|
||||
<h3 class="kiwixHomeBody__results"></h3>
|
||||
</div>
|
||||
<div id="fadeOut" class="fadeOut"></div>
|
||||
</div>
|
||||
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<form class="kiwixsearch" method="GET" action="{{root}}/search" id="kiwixsearchform">
|
||||
{{#hascontent}}<input type="hidden" name="content" value="{{content}}" />{{/hascontent}}
|
||||
<label for="kiwixsearchbox">🔍</label>
|
||||
<input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text" title="{{{SEARCHBOX_TOOLTIP}}}" aria-label="{{{SEARCHBOX_TOOLTIP}}}">
|
||||
<input autocomplete="off" id="kiwixsearchbox" name="pattern" type="text" size="50" title="{{{SEARCHBOX_TOOLTIP}}}" aria-label="{{{SEARCHBOX_TOOLTIP}}}">
|
||||
</form>
|
||||
</div>
|
||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||
|
||||
64
static/templates/widget.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!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>Welcome to Kiwix Server</title>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/index.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "poppins";
|
||||
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "roboto";
|
||||
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
</style>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
|
||||
<script src="{{root}}/skin/iso6391To3.js?KIWIXCACHEID"></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/widget.js?KIWIXCACHEID" defer></script>
|
||||
</head>
|
||||
<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'>
|
||||
<span class="kiwixButton tagFilterLabel"></span>
|
||||
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="kiwixHomeBody">
|
||||
<div class="book__list">
|
||||
<h3 class="kiwixHomeBody__results"></h3>
|
||||
</div>
|
||||
<div id="fadeOut" class="fadeOut"></div>
|
||||
</div>
|
||||
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
|
||||
<div id="kiwixfooter" class="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div>
|
||||
</body>
|
||||
<script>
|
||||
function closeModal() {
|
||||
for(modal of document.getElementsByClassName('modal-wrapper')) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
6
test/data/customized_resources.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
/non-existent-item text/plain ./test/helloworld.txt
|
||||
/ text/html ./test/welcome.html
|
||||
/skin/index.css application/json ./test/helloworld.txt
|
||||
/zimfile/A/Ray_Charles ray/charles ./test/welcome.html
|
||||
/content/zimfile/A/Ray_Charles charles/ray ./test/welcome.html
|
||||
/search text/html ./test/helloworld.txt
|
||||
1
test/data/helloworld.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hello world!
|
||||
@@ -10,7 +10,7 @@
|
||||
publisher="Kiwix"
|
||||
date="2020-03-31"
|
||||
name="wikipedia_en_ray_charles"
|
||||
tags="unittest;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes"
|
||||
tags="public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes"
|
||||
articleCount="284"
|
||||
mediaCount="2"
|
||||
size="556"
|
||||
@@ -28,7 +28,7 @@
|
||||
publisher="Kiwix"
|
||||
date="2020-03-31"
|
||||
name="wikipedia_ru_ray_charles"
|
||||
tags="unittest;wikipedia;_pictures:no;_videos:no;_details:no"
|
||||
tags="public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no"
|
||||
articleCount="284"
|
||||
mediaCount="2"
|
||||
size="123"
|
||||
|
||||
1
test/data/welcome.html
Normal file
@@ -0,0 +1 @@
|
||||
<html><head></head><body>Welcome</body></html>
|
||||
@@ -550,9 +550,11 @@ TEST_F(LibraryTest, filterByQuery)
|
||||
|
||||
// by default, filtering by query assumes partial query
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki"),
|
||||
"An example ZIM archive", // due to the "wikibooks" tag
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Géographie par Wikipédia",
|
||||
"Mathématiques", // due to the "wikipedia" tag
|
||||
"Ray Charles",
|
||||
"Wikiquote"
|
||||
);
|
||||
@@ -714,6 +716,7 @@ TEST_F(LibraryTest, filterByMultipleCriteria)
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia"),
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Géographie par Wikipédia",
|
||||
"Mathématiques", // due to the "wikipedia" tag
|
||||
"Ray Charles"
|
||||
);
|
||||
|
||||
|
||||
783
test/library_server.cpp
Normal file
@@ -0,0 +1,783 @@
|
||||
|
||||
#define CPPHTTPLIB_ZLIB_SUPPORT 1
|
||||
#include "./httplib.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#define SERVER_PORT 8001
|
||||
#include "server_testing_tools.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Testing of the library-related functionality of the server
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class LibraryServerTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
std::unique_ptr<ZimFileServer> zfs1_;
|
||||
|
||||
const int PORT = 8002;
|
||||
|
||||
protected:
|
||||
void SetUp() override {
|
||||
zfs1_.reset(new ZimFileServer(PORT, "./test/library.xml"));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
zfs1_.reset();
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a copy of 'text' where every line that fully matches 'pattern'
|
||||
// preceded by optional whitespace is replaced with the fixed string
|
||||
// 'replacement' preserving the leading whitespace
|
||||
std::string replaceLines(const std::string& text,
|
||||
const std::string& pattern,
|
||||
const std::string& replacement)
|
||||
{
|
||||
std::regex regex("^ *" + pattern + "$");
|
||||
std::ostringstream oss;
|
||||
std::istringstream iss(text);
|
||||
std::string line;
|
||||
while ( std::getline(iss, line) ) {
|
||||
if ( std::regex_match(line, regex) ) {
|
||||
for ( size_t i = 0; i < line.size() && line[i] == ' '; ++i )
|
||||
oss << ' ';
|
||||
oss << replacement << "\n";
|
||||
} else {
|
||||
oss << line << "\n";
|
||||
}
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string maskVariableOPDSFeedData(std::string s)
|
||||
{
|
||||
s = replaceLines(s, R"(<updated>\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ</updated>)",
|
||||
"<updated>YYYY-MM-DDThh:mm:ssZ</updated>");
|
||||
s = replaceLines(s, "<id>[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}</id>",
|
||||
"<id>12345678-90ab-cdef-1234-567890abcdef</id>");
|
||||
return s;
|
||||
}
|
||||
|
||||
#define OPDS_FEED_TAG \
|
||||
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
|
||||
" xmlns:dc=\"http://purl.org/dc/terms/\"\n" \
|
||||
" xmlns:opds=\"http://opds-spec.org/2010/catalog\">\n"
|
||||
|
||||
#define CATALOG_LINK_TAGS \
|
||||
" <link rel=\"self\" href=\"\" type=\"application/atom+xml\" />\n" \
|
||||
" <link rel=\"search\"" \
|
||||
" type=\"application/opensearchdescription+xml\"" \
|
||||
" href=\"/ROOT/catalog/searchdescription.xml\" />\n"
|
||||
|
||||
#define CHARLES_RAY_CATALOG_ENTRY \
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:charlesray</id>\n" \
|
||||
" <title>Charles, Ray</title>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\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" \
|
||||
" <link type=\"text/html\" href=\"/ROOT/content/zimfile%26other\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
" </author>\n" \
|
||||
" <publisher>\n" \
|
||||
" <name>Kiwix</name>\n" \
|
||||
" </publisher>\n" \
|
||||
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
|
||||
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile%26other.zim\" length=\"569344\" />\n" \
|
||||
" </entry>\n"
|
||||
|
||||
#define RAY_CHARLES_CATALOG_ENTRY \
|
||||
" <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" \
|
||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||
" <flavour></flavour>\n" \
|
||||
" <category>wikipedia</category>\n" \
|
||||
" <tags>public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
|
||||
" <articleCount>284</articleCount>\n" \
|
||||
" <mediaCount>2</mediaCount>\n" \
|
||||
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
|
||||
" href=\"/ROOT/catalog/v2/illustration/raycharles/?size=48\"\n" \
|
||||
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
|
||||
" <link type=\"text/html\" href=\"/ROOT/content/zimfile\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
" </author>\n" \
|
||||
" <publisher>\n" \
|
||||
" <name>Kiwix</name>\n" \
|
||||
" </publisher>\n" \
|
||||
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
|
||||
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
|
||||
" </entry>\n"
|
||||
|
||||
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY \
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:raycharles_uncategorized</id>\n" \
|
||||
" <title>Ray (uncategorized) Charles</title>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\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>public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no</tags>\n" \
|
||||
" <articleCount>284</articleCount>\n" \
|
||||
" <mediaCount>2</mediaCount>\n" \
|
||||
" <link type=\"text/html\" href=\"/ROOT/content/zimfile\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
" </author>\n" \
|
||||
" <publisher>\n" \
|
||||
" <name>Kiwix</name>\n" \
|
||||
" </publisher>\n" \
|
||||
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
|
||||
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"125952\" />\n" \
|
||||
" </entry>\n"
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_root_xml)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/root.xml");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>All zims</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
"\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_searchdescription_xml)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/searchdescription.xml");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(r->body,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
|
||||
" <ShortName>Zim catalog search</ShortName>\n"
|
||||
" <Description>Search zim files in the catalog.</Description>\n"
|
||||
" <Url type=\"application/atom+xml;profile=opds-catalog\"\n"
|
||||
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
|
||||
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
|
||||
" indexOffset=\"0\"\n"
|
||||
" template=\"/ROOT/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}¬ag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
|
||||
"</OpenSearchDescription>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_by_phrase)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?q=\"ray%20charles\"");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q="ray charles")</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_by_words)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?q=ray%20charles");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=ray charles)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>3</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_prefix_search)
|
||||
{
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?q=description:ray%20description:charles");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=description:ray description:charles)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?q=title:\"ray%20charles\"");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=title:"ray charles")</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?q=ray%20-uncategorized");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=ray -uncategorized)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_by_tag)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?tag=_category:jazz");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (tag=_category:jazz)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_by_category)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?category=jazz");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (category=jazz)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_results_pagination)
|
||||
{
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?count=0");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (count=0)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>3</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?count=1");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (count=1)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?start=1&count=1");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (count=1&start=1)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>1</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?start=100&count=10");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (count=10&start=100)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>100</startIndex>\n"
|
||||
" <itemsPerPage>0</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_root)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/root.xml");
|
||||
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:opds="https://specs.opds.io/opds-1.2">
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<link rel="self"
|
||||
href="/ROOT/catalog/v2/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="/ROOT/catalog/v2/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="search"
|
||||
href="/ROOT/catalog/v2/searchdescription.xml"
|
||||
type="application/opensearchdescription+xml"/>
|
||||
<title>OPDS Catalog Root</title>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
|
||||
<entry>
|
||||
<title>All entries</title>
|
||||
<link rel="subsection"
|
||||
href="/ROOT/catalog/v2/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.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>All entries (partial)</title>
|
||||
<link rel="subsection"
|
||||
href="/ROOT/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"
|
||||
href="/ROOT/catalog/v2/categories"
|
||||
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 categories in this catalog.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>List of languages</title>
|
||||
<link rel="subsection"
|
||||
href="/ROOT/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);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_searchdescription_xml)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/searchdescription.xml");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(r->body,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
|
||||
" <ShortName>Zim catalog search</ShortName>\n"
|
||||
" <Description>Search zim files in the catalog.</Description>\n"
|
||||
" <Url type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"\n"
|
||||
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
|
||||
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
|
||||
" indexOffset=\"0\"\n"
|
||||
" template=\"/ROOT/catalog/v2/entries?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
|
||||
"</OpenSearchDescription>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_categories)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/categories");
|
||||
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:opds="https://specs.opds.io/opds-1.2">
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<link rel="self"
|
||||
href="/ROOT/catalog/v2/categories"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="/ROOT/catalog/v2/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<title>List of categories</title>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
|
||||
<entry>
|
||||
<title>jazz</title>
|
||||
<link rel="subsection"
|
||||
href="/ROOT/catalog/v2/entries?category=jazz"
|
||||
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 with category of 'jazz'.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>wikipedia</title>
|
||||
<link rel="subsection"
|
||||
href="/ROOT/catalog/v2/entries?category=wikipedia"
|
||||
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 with category of 'wikipedia'.</content>
|
||||
</entry>
|
||||
</feed>
|
||||
)";
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_languages)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/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="/ROOT/catalog/v2/languages"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="/ROOT/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="/ROOT/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="/ROOT/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="/ROOT/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:dc=\"http://purl.org/dc/terms/\"\n" \
|
||||
" xmlns:opds=\"https://specs.opds.io/opds-1.2\"\n" \
|
||||
" xmlns:opensearch=\"http://a9.com/-/spec/opensearch/1.1/\">\n" \
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
|
||||
"\n" \
|
||||
" <link rel=\"self\"\n" \
|
||||
" href=\"/ROOT/catalog/v2/" x "\"\n" \
|
||||
" type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"/>\n" \
|
||||
" <link rel=\"start\"\n" \
|
||||
" href=\"/ROOT/catalog/v2/root.xml\"\n" \
|
||||
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
|
||||
" <link rel=\"up\"\n" \
|
||||
" href=\"/ROOT/catalog/v2/root.xml\"\n" \
|
||||
" 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)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("")
|
||||
" <title>All Entries</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
"\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
|
||||
{
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?start=1")
|
||||
" <title>Filtered Entries (start=1)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>1</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?count=2");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?count=2")
|
||||
" <title>Filtered Entries (count=2)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1&count=1");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?count=1&start=1")
|
||||
" <title>Filtered Entries (count=1&start=1)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>1</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?q=\"ray%20charles\"");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22")
|
||||
" <title>Filtered Entries (q="ray charles")</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_individual_entry_access)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/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("/ROOT/catalog/v2/entry/non-existent-entry");
|
||||
EXPECT_EQ(r1->status, 404);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_partial_entries)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT/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=\"/ROOT/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=\"/ROOT/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=\"/ROOT/catalog/v2/entry/raycharles_uncategorized\"\n"
|
||||
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
|
||||
" </entry>\n"
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
#define EXPECT_SEARCH_RESULTS(SEARCH_TERM, RESULT_COUNT, OPDS_ENTRIES) \
|
||||
{ \
|
||||
const auto r = zfs1_->GET("/ROOT/catalog/search?q=" SEARCH_TERM); \
|
||||
EXPECT_EQ(r->status, 200); \
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body), \
|
||||
OPDS_FEED_TAG \
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
|
||||
" <title>Filtered zims (q=" SEARCH_TERM ")</title>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <totalResults>" #RESULT_COUNT "</totalResults>\n" \
|
||||
" <startIndex>0</startIndex>\n" \
|
||||
" <itemsPerPage>" #RESULT_COUNT "</itemsPerPage>\n" \
|
||||
CATALOG_LINK_TAGS \
|
||||
\
|
||||
OPDS_ENTRIES \
|
||||
\
|
||||
"</feed>\n" \
|
||||
); \
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_includes_public_tags)
|
||||
{
|
||||
EXPECT_SEARCH_RESULTS("public_tag_without_a_value",
|
||||
1,
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
);
|
||||
|
||||
EXPECT_SEARCH_RESULTS("public_tag_with_a_value",
|
||||
1,
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
);
|
||||
|
||||
// prefix search works on tag names
|
||||
EXPECT_SEARCH_RESULTS("public_tag",
|
||||
2,
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
);
|
||||
|
||||
EXPECT_SEARCH_RESULTS("value_of_a_public_tag",
|
||||
1,
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
);
|
||||
|
||||
// prefix search works on tag values
|
||||
EXPECT_SEARCH_RESULTS("value_of",
|
||||
1,
|
||||
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||
);
|
||||
}
|
||||
|
||||
#define EXPECT_ZERO_RESULTS(SEARCH_TERM) EXPECT_SEARCH_RESULTS(SEARCH_TERM, 0, )
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_on_tags_is_not_an_any_substring_match)
|
||||
{
|
||||
EXPECT_ZERO_RESULTS("tag_with")
|
||||
EXPECT_ZERO_RESULTS("alue_of_a_public_tag")
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_excludes_hidden_tags)
|
||||
{
|
||||
EXPECT_ZERO_RESULTS("_private_tag_without_a_value");
|
||||
EXPECT_ZERO_RESULTS("private_tag_without_a_value");
|
||||
EXPECT_ZERO_RESULTS("value_of_a_private_tag");
|
||||
|
||||
#undef EXPECT_ZERO_RESULTS
|
||||
}
|
||||
|
||||
#undef EXPECT_SEARCH_RESULTS
|
||||
@@ -17,6 +17,7 @@ tests = [
|
||||
if build_machine.system() != 'windows'
|
||||
tests += [
|
||||
'server',
|
||||
'library_server',
|
||||
'server_search'
|
||||
]
|
||||
endif
|
||||
@@ -35,7 +36,10 @@ if gtest_dep.found() and not meson.is_cross_build()
|
||||
'zimfile&other.zim',
|
||||
'corner_cases.zim',
|
||||
'poor.zim',
|
||||
'library.xml'
|
||||
'library.xml',
|
||||
'customized_resources.txt',
|
||||
'helloworld.txt',
|
||||
'welcome.html',
|
||||
]
|
||||
foreach file : data_files
|
||||
# configure_file(input : 'data/' + file,
|
||||
|
||||
977
test/server.cpp
@@ -195,7 +195,7 @@ struct SearchResult
|
||||
|
||||
const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Genius_+_Soul_=_Jazz",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Genius_+_Soul_=_Jazz",
|
||||
/*title*/ "Genius + Soul = Jazz",
|
||||
/*snippet*/ R"SNIPPET(...Grammy Hall of Fame in 2011. It was re-issued in the UK, first in 1989 on the Castle Communications "Essential Records" label, and by Rhino Records in 1997 on a single CD together with Charles' 1970 My Kind of <b>Jazz</b>. In 2010, Concord Records released a deluxe edition comprising digitally remastered versions of Genius + Soul = <b>Jazz</b>, My Kind of <b>Jazz</b>, <b>Jazz</b> Number II, and My Kind of <b>Jazz</b> Part 3. Professional ratings Review scores Source Rating Allmusic link Warr.org link Encyclopedia of Popular Music...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -203,7 +203,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Jazz_Number_II",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Jazz_Number_II",
|
||||
/*title*/ "Jazz Number II",
|
||||
/*snippet*/ R"SNIPPET(<b>Jazz</b> Number II <b>Jazz</b> Number II is a 1973 album by Ray Charles. It is a collection of <b>jazz</b>/soul instrumentals featuring Charles on piano backed by his Big Band. Professional ratings Review scores Source Rating Allmusic link <b>Jazz</b> Number II Studio album by Ray Charles Released January 1973 Recorded 1971-72 Studio Charles’ Tangerine/RPM Studios, Los Angeles, CA Genre Soul, <b>jazz</b> Length 39:02 Label Tangerine Producer Ray Charles Ray Charles chronology Through the Eyes of Love (1972) <b>Jazz</b> Number II......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -211,7 +211,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/My_Kind_of_Jazz_Part_3",
|
||||
/*link*/ "/ROOT/content/zimfile/A/My_Kind_of_Jazz_Part_3",
|
||||
/*title*/ "My Kind of Jazz Part 3",
|
||||
/*snippet*/ R"SNIPPET(My Kind of <b>Jazz</b> Part 3 My Kind of <b>Jazz</b> Part 3 is a 1975 album by Ray Charles released by Crossover Records. Concord Records re-issued the contents in digital form in 2009. Professional ratings Review scores Source Rating Allmusic link My Kind of <b>Jazz</b> Part 3 Studio album by Ray Charles Released October 1975 Recorded 1975 in Los Angeles, CA Genre Soul, <b>jazz</b> Length 38:13 Label Crossover Producer Ray Charles Ray Charles chronology Renaissance (1975) My Kind of <b>Jazz</b> Part 3 (1975) Live In Japan (1975)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -219,7 +219,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/My_Kind_of_Jazz",
|
||||
/*link*/ "/ROOT/content/zimfile/A/My_Kind_of_Jazz",
|
||||
/*title*/ "My Kind of Jazz",
|
||||
/*snippet*/ R"SNIPPET(My Kind of <b>Jazz</b> My Kind of <b>Jazz</b> Studio album by Ray Charles Released April 1970 Recorded January 1-10, 1970 in Los Angeles, CA Genre <b>jazz</b> Length 30:20 Label Tangerine Producer Quincy Jones Ray Charles chronology Doing His Thing (1969) My Kind of <b>Jazz</b> (1970) Love Country Style (1970) Professional ratings Review scores Source Rating Allmusic link My Kind of <b>Jazz</b> is a 1970 album by Ray Charles....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -227,7 +227,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Hank_Crawford",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Hank_Crawford",
|
||||
/*title*/ "Hank Crawford",
|
||||
/*snippet*/ R"SNIPPET(...bop, <b>jazz</b>-funk, soul <b>jazz</b> alto saxophonist, arranger and songwriter. Crawford was musical director for Ray Charles before embarking on a solo career releasing many well-regarded albums on Atlantic, CTI and Milestone. Hank Crawford Background information Birth name Bennie Ross Crawford, Jr Born (1934-12-21)December 21, 1934 Memphis, Tennessee, U.S. Died January 29, 2009(2009-01-29) (aged 74) Memphis, Tennessee, U.S. Genres R&B, Hard bop, <b>Jazz</b>-funk, Soul <b>jazz</b> Occupation(s) Saxophonist, Songwriter......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -235,7 +235,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles",
|
||||
/*title*/ "Catchin' Some Rays: The Music of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> singer Roseanna Vitro, released in August 1997 on the Telarc <b>Jazz</b> label. Catchin' Some Rays: The Music of Ray Charles Studio album by Roseanna Vitro Released August 1997 Recorded March 26, 1997 at Sound on Sound, NYC April 4,1997 at Quad Recording Studios, NYC Genre Vocal <b>jazz</b> Length 61:00 Label Telarc <b>Jazz</b> CD-83419 Producer Paul Wickliffe Roseanna Vitro chronology Passion Dance (1996) Catchin' Some Rays: The Music of Ray Charles (1997) The Time of My Life: Roseanna Vitro Sings the Songs of......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -243,7 +243,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles",
|
||||
/*title*/ "That's What I Say: John Scofield Plays the Music of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(That's What I Say: John Scofield Plays the Music of Ray Charles Studio album by John Scofield Released June 7, 2005 (2005-06-07) Recorded December 2004 Studio Avatar Studios, New York City Genre <b>Jazz</b> Length 65:21 Label Verve Producer Steve Jordan John Scofield chronology EnRoute: John Scofield Trio LIVE (2004) That's What I Say: John Scofield Plays the Music of Ray Charles (2005) Out Louder (2006) Professional ratings Review scores Source Rating Allmusic All About <b>Jazz</b> All About <b>Jazz</b>...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -251,7 +251,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Tribute_to_Uncle_Ray",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Tribute_to_Uncle_Ray",
|
||||
/*title*/ "Tribute to Uncle Ray",
|
||||
/*snippet*/ R"SNIPPET(...Stevie Wonder" with the successful and popular Ray Charles who was also a blind African American musician. Like his debut, this album failed to generate hit singles as Motown struggled to find a sound to fit Wonder, who was just 12 when this album was released. Tribute to Uncle Ray Studio album by Little Stevie Wonder Released October 1962 Recorded 1962 Studio Studio A, Hitsville USA, Detroit Genre Soul, <b>jazz</b> Label Tamla Producer Henry Cosby, Clarence Paul Stevie Wonder chronology The <b>Jazz</b> Soul of Little Stevie (1962) Tribute to Uncle Ray (1962) Recorded Live: The 12 Year Old Genius (1963) Professional ratings Review scores Source Rating Allmusic...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -259,7 +259,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Best_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Best_of_Ray_Charles",
|
||||
/*title*/ "The Best of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(The Best of Ray Charles The Best of Ray Charles is a compilation album released in 1970 on the Atlantic <b>Jazz</b> label, featuring previously released instrumental (non-vocal) tracks recorded by Ray Charles between November 1956 and November 1958. The Best of Ray Charles Greatest hits album by Ray Charles Released 1970 Genre R&B, <b>Jazz</b> Length 34:06 Label Atlantic The instrumental, "Rockhouse" would later be covered, as "Ray's Rockhouse" (1985), by The Manhattan Transfer with lyrics by Jon Hendricks....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -267,7 +267,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Genius_Hits_the_Road",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Genius_Hits_the_Road",
|
||||
/*title*/ "The Genius Hits the Road",
|
||||
/*snippet*/ R"SNIPPET(...a hit single, "Georgia on My Mind". The Genius Hits the Road Studio album by Ray Charles Released September 1960 Recorded March 25 and 29, 1960 in New York City Genre R&B, blues, <b>jazz</b> Length 33:37 Label ABC-Paramount 335 Producer Sid Feller Ray Charles chronology Genius + Soul = <b>Jazz</b> (1961) The Genius Hits the Road (1960) Dedicated to You (1961) Singles from The Genius Hits the Road "Georgia on My Mind" Released: September 1960 Professional ratings Review scores Source Rating Allmusic Warr.org...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -275,7 +275,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Charles_at_Newport",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_at_Newport",
|
||||
/*title*/ "Ray Charles at Newport",
|
||||
/*snippet*/ R"SNIPPET(...Ray Charles at Newport is a 1958 live album of Ray Charles' July 5, 1958 performance at the Newport <b>Jazz</b> Festival. The detailed liner notes on the album were written by Kenneth Lee Karpe. All tracks from this Newport album, along with all tracks from his 1959 Herndon Stadium performance in Atlanta, were also released on the Atlantic compilation LP, Ray Charles Live. A later CD reissue of that compilation album included a previously unissued song from the 1958 Newport concert, "Swanee River Rock". Professional ratings Review scores Source Rating Allmusic link Discogs link Ray Charles at Newport Live album by Ray Charles Released November 1958 Recorded July 5, 1958 Venue Newport <b>Jazz</b> Festival, Newport, Rhode Island Genre R&B Length 40:28 Label Atlantic Producer Tom Dowd (engineer) Ray Charles chronology The Great Ray Charles (1957) Ray Charles at Newport (1958) Yes Indeed! (1958) Re-issue cover 1987 re-issue/compilation...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -283,7 +283,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles",
|
||||
/*title*/ "Here We Go Again: Celebrating the Genius of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...and <b>jazz</b> trumpeter Wynton Marsalis. It was recorded during concerts at the Rose Theater in New York City, on February 9 and 10, 2009. The album received mixed reviews, in which the instrumentation of Marsalis' orchestra was praised by the critics. Here We Go Again: Celebrating the Genius of Ray Charles Live album by Willie Nelson and Wynton Marsalis Released March 29, 2011 (2011-03-29) Recorded February 9 –10 2009 Venue Rose Theater, New York Genre <b>Jazz</b>, country Length 61:49 Label Blue Note......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -291,7 +291,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Confession_Blues",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Confession_Blues",
|
||||
/*title*/ "Confession Blues",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> Length 2:31 Label Down Beat Records Songwriter(s) R. C. Robinson (Ray Charles) Charles moved to Seattle in 1948, where he formed The McSon Trio with guitarist G. D. "Gossie" McKee and bass player Milton S. Garret. In late 1948, Jack Lauderdale of Down Beat Records heard Charles play at the Seattle <b>jazz</b> club, The Rocking Chair. The next day, Lauderdale took Charles and his trio to a Seattle recording studio where they recorded "Confession Blues" and "I Love You, I Love You". In February......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -299,7 +299,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Genius_Loves_Company",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Genius_Loves_Company",
|
||||
/*title*/ "Genius Loves Company",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> and pop standards performed by Charles and several guest musicians, such as Natalie Cole, Elton John, James Taylor, Norah Jones, B.B. King, Gladys Knight, Diana Krall, Van Morrison, Willie Nelson and Bonnie Raitt. Genius Loves Company was the last album recorded and completed by Charles before his death in June 2004. Genius Loves Company Studio album by Ray Charles Released August 31, 2004 Recorded June 2003–March 2004 Genre Rhythm and blues, soul, country, blues, <b>jazz</b>, pop Length 54:03......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -307,7 +307,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Love_Country_Style",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Love_Country_Style",
|
||||
/*title*/ "Love Country Style",
|
||||
/*snippet*/ R"SNIPPET(Love Country Style Love Country Style is a studio album by Ray Charles released in June 1970 on Charles' Tangerine Records label. Love Country Style Studio album by Ray Charles Released June 1970 Genre R&B Length 35:25 Label ABC/Tangerine Producer Joe Adams Ray Charles chronology My Kind of <b>Jazz</b> (1970) Love Country Style (1970) Volcanic Action of My Soul (1971) Professional ratings Review scores Source Rating Allmusic Christgau's Record Guide B...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -315,7 +315,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Doing_His_Thing",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Doing_His_Thing",
|
||||
/*title*/ "Doing His Thing",
|
||||
/*snippet*/ R"SNIPPET(Doing His Thing Doing His Thing is a 1969 studio album by Ray Charles, released by Tangerine Records. The cover artwork was by Lafayette Chew. Doing His Thing Studio album by Ray Charles Released May 1969 Recorded RPM Studios, Los Angeles, California Genre R&B, soul Length 32:33 Label ABC/Tangerine Producer Joe Adams Ray Charles chronology I'm All Yours Baby (1969) Doing His Thing (1969) My Kind of <b>Jazz</b> (1970)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -323,7 +323,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Inspiration_I_Feel",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Inspiration_I_Feel",
|
||||
/*title*/ "The Inspiration I Feel",
|
||||
/*snippet*/ R"SNIPPET(The Inspiration I Feel The Inspiration I Feel is an album by flautist Herbie Mann featuring tunes associated with Ray Charles recorded in 1968 and released on the Atlantic label. The Inspiration I Feel Studio album by Herbie Mann Released 1968 Recorded May 6 & 7, 1968 New York City Genre <b>Jazz</b> Length 34:28 Label Atlantic SD 1513 Producer Nesuhi Ertegun, Joel Dorn Herbie Mann chronology Windows Opened (1968) The Inspiration I Feel (1968) Memphis Underground (1968)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -331,7 +331,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Milt_Turner",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Milt_Turner",
|
||||
/*title*/ "Milt Turner",
|
||||
/*snippet*/ R"SNIPPET(...Turner After graduating from Pearl High School, he attended Tennessee State University, where he coincided with Hank Crawford, who he later recommended to join him in Ray Charles' band when he took over from William Peeples in the late 1950s. Milton Turner (1930-1993) was a <b>jazz</b> drummer. In 1962, he was a member of Phineas Newborn's trio with Leroy Vinnegar, on whose solo albums he would later appear, and in the early 1960s, Turner also recorded with Teddy Edwards. He never recorded as a leader....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -339,7 +339,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Rare_Genius",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Rare_Genius",
|
||||
/*title*/ "Rare Genius",
|
||||
/*snippet*/ R"SNIPPET(...studio recordings and demos made in the 1970s, 1980s and 1990s together with some contemporary instrumental and backing vocal parts. Rare Genius: The Undiscovered Masters Remix album by Ray Charles Released 2010 Genre Soul Length 41:36 Label Concord Producer Ray Charles, John Burk Ray Charles chronology Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010) Professional ratings Review scores Source Rating Allmusic (link) PopMatters (link) All About <b>Jazz</b> (link) favorable...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -347,7 +347,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Tangerine_Records_(1962)",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Tangerine_Records_(1962)",
|
||||
/*title*/ "Tangerine Records (1962)",
|
||||
/*snippet*/ R"SNIPPET(...in 1962. ABC-Paramount Records promoted and distributed it. Early singles labels were orange and later became black, red and white. Many of the later recordings are now sought after in "Northern Soul" circles. In 1973 Charles left ABC, closed Tangerine and started Crossover Records. Ray Charles Enterprises owns the catalog. Tangerine Records Parent company ABC-Paramount Records Founded 1962 Founder Ray Charles Defunct 1973 Distributor(s) ABC-Paramount Records Genre R&B, soul music, <b>jazz</b> music...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -355,7 +355,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Sings,_Basie_Swings",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Sings,_Basie_Swings",
|
||||
/*title*/ "Ray Sings, Basie Swings",
|
||||
/*snippet*/ R"SNIPPET(...from 1973 with newly recorded instrumental tracks by the contemporary Count Basie Orchestra. Professional ratings Review scores Source Rating AllMusic Ray Sings, Basie Swings Compilation album by Ray Charles, Count Basie Orchestra Released October 3, 2006 (2006-10-03) Recorded Mid-1970s, February - May 2006 Studio Los Angeles Genre Soul, <b>jazz</b>, Swing Label Concord/Hear Music Producer Gregg Field Ray Charles chronology Genius & Friends (2005) Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -363,7 +363,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/I_Remember_Brother_Ray",
|
||||
/*link*/ "/ROOT/content/zimfile/A/I_Remember_Brother_Ray",
|
||||
/*title*/ "I Remember Brother Ray",
|
||||
/*snippet*/ R"SNIPPET(...is an album by saxophonist David "Fathead" Newman, paying tribute to his bandleader and mentor Ray Charles, which was recorded in 2004 and released on the HighNote label the following year. I Remember Brother Ray Studio album by David "Fathead" Newman Released January 11, 2005 Recorded August 14, 2004 Studio Van Gelder Studio, Englewood Cliffs, NJ Genre <b>Jazz</b> Length 50:39 Label HighNote HCD 7135 Producer David "Fathead" Newman, Houston Person David "Fathead" Newman chronology Song for the New Man (2004) I Remember Brother Ray (2005) Cityscape (2006)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -371,7 +371,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Light_Out_of_Darkness_(A_Tribute_to_Ray_Charles)",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Light_Out_of_Darkness_(A_Tribute_to_Ray_Charles)",
|
||||
/*title*/ "Light Out of Darkness (A Tribute to Ray Charles)",
|
||||
/*snippet*/ R"SNIPPET(...to Ray Charles) is a 1993 studio album by Shirley Horn, recorded in tribute to Ray Charles. Light Out of Darkness (A Tribute to Ray Charles) Studio album by Shirley Horn Released 1993 Recorded April 30 and May 1–3, 1993, Clinton Recording Studios, New York City Genre Vocal <b>jazz</b> Length 62:53 Label Verve Producer Shirley Horn, Sheila Mathis, Richard Seidel, Lynn Butterer Shirley Horn chronology Here's to Life (1992) Light Out of Darkness (A Tribute to Ray Charles) (1993) I Love You, Paris (1994)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -379,7 +379,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Soul_Meeting",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Soul_Meeting",
|
||||
/*title*/ "Soul Meeting",
|
||||
/*snippet*/ R"SNIPPET(...in 1957 and released in 1961 on Atlantic Records. The album was later re-issued together with the other Charles–Jackson recording, Soul Brothers, on a 2 CD compilation together with other 'bonus' tracks from the same recording sessions. Professional ratings Review scores Source Rating Down Beat (Original Lp release) AllMusic link Soul Meeting Studio album by Ray Charles, Milt Jackson Released 1961 Recorded April 10, 1958 Genre R&B, <b>jazz</b> Length 37:43 Label Atlantic Producer Tom Dowd Ray Charles chronology The Genius Sings the Blues (1961) Soul Meeting (1961) The Genius After Hours (1961) Alternative cover compilation CD re-issue...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -387,7 +387,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Charles_in_Concert",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_in_Concert",
|
||||
/*title*/ "Ray Charles in Concert",
|
||||
/*snippet*/ R"SNIPPET(...between 1958 and 1975. In Concert Compilation album by Ray Charles Released 2003 Recorded Newport <b>Jazz</b> Festival (1958 July 5), Herndon Stadium Atlanta (1959 May 19), Sportpalast Berlin (1962 March 6), Shrine Auditorium Los Angeles (1964 September 20), Tokyo (1975 November 27) and Yokohama (1975 November 30) Genre R&B, soul Length 2 hours Label Rhino Handmade Producer Nesuhi Ertegun (Newport), Zenas Sears (Atlanta), Norman Granz (Berlin), Sid Feller (Los Angeles) and Ray Charles (Tokyo / Yokohama)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -395,7 +395,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Sticks_and_Stones_(Titus_Turner_song)",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Sticks_and_Stones_(Titus_Turner_song)",
|
||||
/*title*/ "Sticks and Stones (Titus Turner song)",
|
||||
/*snippet*/ R"SNIPPET(...in a 1960 version by Ray Charles, who added the Latin drum part. It was his first R&B hit with ABC-Paramount, followed in 1961 with "Hit The Road Jack". The song was also covered by Jerry Lee Lewis, The Zombies, Wanda Jackson and The Kingsmen, as well as Joe Cocker on Mad Dogs and Englishmen, and Elvis Costello in 1994 on the extended play version of Kojak Variety. In 1997, <b>jazz</b> singer Roseanna Vitro included the tune in her tribute to Charles, Catchin’ Some Rays: The Music of Ray Charles....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -403,7 +403,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Do_the_Twist!_with_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Do_the_Twist!_with_Ray_Charles",
|
||||
/*title*/ "Do the Twist! with Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...peaked at #11. Do the Twist! with Ray Charles Greatest hits album by Ray Charles Released 1961 Recorded 1954-1960 Genre R&B, Soul, <b>Jazz</b> Length 32:39 Label Atlantic Ray Charles chronology The Genius Sings the Blues (1961) Do the Twist! with Ray Charles (1961) Soul Meeting (1961) Professional ratings Review scores Source Rating Allmusic (link) The Rolling Stone Record Guide In 1963, the album got a new cover and was renamed The Greatest Ray Charles. Track listing and catalog number (Atlantic 8054) remained the same....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -411,7 +411,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Great_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Great_Ray_Charles",
|
||||
/*title*/ "The Great Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> album. Later CD re-issues often include as a bonus, six of eight tracks from The Genius After Hours. The original cover was by Marvin Israel. Professional ratings Review scores Source Rating Allmusic The Great Ray Charles Studio album by Ray Charles Released August 1957 Recorded April 30 - November 26, 1956 in New York City Genre Bebop Length 37:37 Label Atlantic Producer Ahmet Ertegün, Jerry Wexler Ray Charles chronology Ray Charles (or, Hallelujah I Love Her So) (1957) The Great Ray......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -419,7 +419,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Charles_Live",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_Live",
|
||||
/*title*/ "Ray Charles Live",
|
||||
/*snippet*/ R"SNIPPET(...<b>Jazz</b> Festival in 1958 and at Herndon Stadium in Atlanta in 1959, respectively). Later CD re-issues of this compilation include an additional, previously unreleased, track from the 1958 Newport concert, "Swanee River Rock." Live Live album by Ray Charles Released 1973 Recorded July 5, 1958 / May 28, 1959 Genre Soul, R&B Length 71:55 Label Atlantic 503 Producer Nesuhi Ertegün / Zenas Sears Ray Charles chronology From the Pages of My Mind (1986) Live (1973) Just Between Us (1988) Professional......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -427,7 +427,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Blue_Funk_(Ray_Charles_song)",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Blue_Funk_(Ray_Charles_song)",
|
||||
/*title*/ "Soul Brothers",
|
||||
/*snippet*/ R"SNIPPET(...on the original LP releases. Soul Brothers Studio album by Ray Charles, Milt Jackson Released June 1958 Recorded September 12, 1957 (Tracks 1-2) and April 10, 1958 (Tracks 3-7), in New York City Genre R&B, <b>jazz</b> Length 38:42 Label Atlantic, Studio One Producer Nesuhi Ertegun Ray Charles chronology Yes Indeed! (1958) Soul Brothers (1958) What'd I Say (1959) alternate release cover compilation CD / re-issue Professional ratings Review scores Source Rating AllMusic Down Beat (Original Lp release)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -435,7 +435,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Soul_Brothers",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Soul_Brothers",
|
||||
/*title*/ "Soul Brothers",
|
||||
/*snippet*/ R"SNIPPET(...and the eleventh studio album by Milt Jackson and released by Atlantic Records in 1958. The album was later re-issued in a 2 CD compilation together with the other Charles–Jackson album Soul Meeting and included additional tracks from the same recording sessions not present on the original LP releases. Soul Brothers Studio album by Ray Charles, Milt Jackson Released June 1958 Recorded September 12, 1957 (Tracks 1-2) and April 10, 1958 (Tracks 3-7), in New York City Genre R&B, <b>jazz</b> Length 38:42 Label Atlantic, Studio One Producer Nesuhi Ertegun Ray Charles chronology Yes Indeed! (1958) Soul Brothers (1958) What'd I Say (1959) alternate release cover compilation CD / re-issue Professional ratings Review scores Source Rating AllMusic Down Beat (Original Lp release)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -443,7 +443,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Charles_and_Betty_Carter",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_and_Betty_Carter",
|
||||
/*title*/ "Ray Charles and Betty Carter",
|
||||
/*snippet*/ R"SNIPPET(...Betty Carter Studio album by Ray Charles and Betty Carter Released August 1961 Recorded August 23, 1960 - June 14, 1961 Genre <b>Jazz</b> Length 41:38 Label ABC Producer Sid Feller Ray Charles chronology Dedicated to You (1961) Ray Charles and Betty Carter (1961) The Genius Sings the Blues (1961) Betty Carter chronology The Modern Sound of Betty Carter (1960) Ray Charles and Betty Carter (1961) 'Round Midnight (1962) Alternative cover / re-issue 1998 Rhino CD re-issue with Dedicated to You Professional ratings Review scores Source Rating Allmusic...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -451,7 +451,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ingredients_in_a_Recipe_for_Soul",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ingredients_in_a_Recipe_for_Soul",
|
||||
/*title*/ "Ingredients in a Recipe for Soul",
|
||||
/*snippet*/ R"SNIPPET(...6, 1960–April 28, 1963 Genre R&B, soul, country soul, vocal <b>jazz</b> Label ABC 465 Producer Sid Feller Ray Charles chronology Modern Sounds in Country and Western Music, Vol. 2 (1962) Ingredients in a Recipe for Soul (1963) Sweet & Sour Tears (1964) Alternative cover 1997 Rhino CD re-issue with Have a Smile with Me In 1990, the album was released on compact disc by DCC with four bonus tracks. In 1997, it was packaged together with 1964's Have a Smile with Me on a two-for-one CD reissue on Rhino with historical liner notes. Professional ratings Review scores......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -459,7 +459,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Genius_Sings_the_Blues",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Genius_Sings_the_Blues",
|
||||
/*title*/ "The Genius Sings the Blues",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b>, and southern R&B. The photo for the album cover was taken by renowned photographer Lee Friedlander. The Genius Sings the Blues was reissued in 2003 by Rhino Entertainment with liner notes by Billy Taylor. The Genius Sings the Blues Compilation album by Ray Charles Released October 1961 Recorded 1952–1960 Genre Rhythm and blues, piano blues, soul Length 34:19 Label Atlantic SD-8052 Producer Ahmet Ertegün, Jerry Wexler Ray Charles chronology Ray Charles and Betty Carter (1961) The Genius......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -467,7 +467,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Genius_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Genius_of_Ray_Charles",
|
||||
/*title*/ "The Genius of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...the sixth studio album by American recording artist Ray Charles, released in 1959 by Atlantic Records. The album eschewed the soul sound of his 1950s recordings, which fused <b>jazz</b>, gospel, and blues, for swinging pop with big band arrangements. It comprises a first half of big band songs and a second half of string-backed ballads. The Genius of Ray Charles sold fewer than 500,000 copies and charted at number 17 on the Billboard 200. "Let the Good Times Roll" and "Don't Let the Sun Catch You Cryin'" were released as singles in 1959. The Genius of Ray Charles Studio album by Ray Charles Released October 1959 Recorded May 6 and June 23, 1959 at 6 West Recording in New......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -475,7 +475,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Charles_in_Person",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles_in_Person",
|
||||
/*title*/ "Ray Charles in Person",
|
||||
/*snippet*/ R"SNIPPET(...night in Atlanta, Georgia at Morris Brown College's Herndon Stadium. All tracks from this album together with those from Ray Charles at Newport were also released on the 1987 Atlantic compilation CD, Ray Charles Live. Ray Charles: In Person Live album by Ray Charles Released July 1960 Recorded May 28, 1959 Genre R&B Length 29:19 Label Atlantic Producer Harris Zenas Ray Charles chronology The Genius of Ray Charles (1959) Ray Charles: In Person (1960) Genius + Soul = <b>Jazz</b> (1961) Re-issue cover 1987 re-issue / compilation Professional ratings Review scores Source Rating Allmusic The album was recorded by the concert sponsor, radio station WAOK. The station's lead disk jockey, Zenas "Daddy" Sears, recorded the album from the audience using a single microphone. The album is noted for its technical excellence in balancing band, singer, and audience, and also for its documentation of the jazzy R&B Ray Charles sound prior to his great crossover success....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -483,7 +483,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Don't_Let_the_Sun_Catch_You_Cryin'",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Don't_Let_the_Sun_Catch_You_Cryin'",
|
||||
/*title*/ "Don't Let the Sun Catch You Cryin'",
|
||||
/*snippet*/ R"SNIPPET(...R&B Sides" and No. 95 on the Billboard Hot 100. It was also recorded by Jackie DeShannon on her 1965 album This is Jackie De Shannon, Paul McCartney on his 1990 live album Tripping the Live Fantastic, Jex Saarelaht and Kate Ceberano on their album Open the Door - Live at Mietta's (1992) and <b>jazz</b> singer Roseanna Vitro on her 1997 album Catchin’ Some Rays: The Music of Ray Charles. Karin Krog and Steve Kuhn include it on their 2005 album, Together Again. Steve Alaimo released a version in 1963...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -491,7 +491,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/I_Don't_Need_No_Doctor",
|
||||
/*link*/ "/ROOT/content/zimfile/A/I_Don't_Need_No_Doctor",
|
||||
/*title*/ "I Don't Need No Doctor",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> guitar player John Scofield recorded a version for his album That's What I Say: John Scofield Plays the Music of Ray Charles in 2005, featuring the blues guitarist John Mayer on additional guitar and vocals. Mayer covered the song again with his band during his tour in summer 2007. A recorded live version from a Los Angeles show during that tour is available on Mayer's CD/DVD release Where the Light Is. A Ray Charles tribute album also provided the impetus for <b>jazz</b> singer Roseanna Vitro's......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -499,7 +499,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
|
||||
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...<b>Jazz</b> Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -507,7 +507,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Anthology_(Ray_Charles_album)",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Anthology_(Ray_Charles_album)",
|
||||
/*title*/ "Anthology (Ray Charles album)",
|
||||
/*snippet*/ R"SNIPPET(...Charles' '60s and '70s ABC-Paramount material", while Rhino Records, the issuing label, refers to it in the liner notes as "the compact disc edition of Ray Charles' Greatest Hits", alluding to the two Rhino LPs issued the same year. It is one of the first CDs to be released by Rhino. Anthology Greatest hits album by Ray Charles Released 1988 Recorded 1960-1972 Genre R&B soul <b>jazz</b> piano blues Length 67:25 (original), 66:18 (re-release) Label Rhino Producer Ray Charles Steve Hoffman Richard Foos Ray Charles chronology Just Between Us (1988) Anthology (1988) Would You Believe? (1990) Posthumous cover Professional ratings Review scores Source Rating AllMusic Charles, who retained the master rights (currently controlled by his estate since his June 2004 passing) to his ABC-Paramount recordings, supervised a remixing of the 20 songs on this compilation especially for this......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -515,7 +515,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Charles",
|
||||
/*title*/ "Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...1960s Background information Birth name Ray Charles Robinson Born (1930-09-23)September 23, 1930 Albany, Georgia, U.S. Died June 10, 2004(2004-06-10) (aged 73) Beverly Hills, California, U.S. Genres R&B soul blues gospel country <b>jazz</b> rock and roll Occupation(s) musician singer songwriter composer Instruments Vocals piano Years active 1947–2004 Labels Atlantic ABC Tangerine Warner Bros. Swing Time Concord Columbia Flashback Associated acts The Raelettes USA for Africa Billy Joel Gladys Knight Website raycharles.com Charles pioneered the soul music genre during the 1950s by combining blues, rhythm and blues, and gospel styles into the music he recorded for Atlantic. He contributed to the integration of country music......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -523,7 +523,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/The_Pages_of_My_Mind",
|
||||
/*link*/ "/ROOT/content/zimfile/A/The_Pages_of_My_Mind",
|
||||
/*title*/ "Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> rock and roll Occupation(s) musician singer songwriter composer Instruments Vocals piano Years active 1947–2004 Labels Atlantic ABC Tangerine Warner Bros. Swing Time Concord Columbia Flashback Associated acts The Raelettes USA for Africa Billy Joel Gladys Knight Website raycharles.com Charles pioneered the soul music genre during the 1950s by combining blues, rhythm and blues, and gospel styles into the music he recorded for Atlantic. He contributed to the integration of country music......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -531,7 +531,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Here_We_Go_Again_(Ray_Charles_song)",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again_(Ray_Charles_song)",
|
||||
/*title*/ "Here We Go Again (Ray Charles song)",
|
||||
/*snippet*/ R"SNIPPET(...was first covered in an instrumental <b>jazz</b> format, and many of the more recent covers have been sung as duets, such as one with Willie Nelson and Norah Jones with Wynton Marsalis accompanying. The song was released on their 2011 tribute album Here We Go Again: Celebrating the Genius of Ray Charles. The song lent its name to Red Steagall's 2007 album as well. Cover versions have appeared on compilation albums by a number of artists, even some who did not release "Here We Go Again" as a single....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -539,7 +539,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Modern_Sounds_in_Country_and_Western_Music",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Modern_Sounds_in_Country_and_Western_Music",
|
||||
/*title*/ "Modern Sounds in Country and Western Music",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b>. Charles produced the album with Sid Feller, who helped the singer select songs to record, and performed alongside saxophonist Hank Crawford, a string section conducted by Marty Paich, and a big band arranged by Gil Fuller and Gerald Wilson. Modern Sounds in Country and Western Music was an immediate critical and commercial success. The album and its four hit singles brought Charles greater mainstream notice and recognition in the pop market, as well as airplay on both R&B and country radio......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -692,11 +692,11 @@ struct TestData
|
||||
std::string url = "/ROOT/search?" + query;
|
||||
|
||||
if ( start >= 0 ) {
|
||||
url += "&start=" + to_string(start);
|
||||
url += "&start=" + std::to_string(start);
|
||||
}
|
||||
|
||||
if ( resultsPerPage != 0 ) {
|
||||
url += "&pageLength=" + to_string(resultsPerPage);
|
||||
url += "&pageLength=" + std::to_string(resultsPerPage);
|
||||
}
|
||||
|
||||
return url;
|
||||
@@ -750,9 +750,9 @@ struct TestData
|
||||
)";
|
||||
|
||||
const size_t lastResultIndex = std::min(totalResultCount, firstResultIndex + results.size() - 1);
|
||||
header = replace(header, "FIRSTRESULT", to_string(firstResultIndex));
|
||||
header = replace(header, "LASTRESULT", to_string(lastResultIndex));
|
||||
header = replace(header, "RESULTCOUNT", to_string(totalResultCount));
|
||||
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
|
||||
header = replace(header, "LASTRESULT", std::to_string(lastResultIndex));
|
||||
header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount));
|
||||
header = replace(header, "PATTERN", getPattern());
|
||||
return header;
|
||||
}
|
||||
@@ -824,9 +824,9 @@ struct TestData
|
||||
const auto realResultsPerPage = resultsPerPage?resultsPerPage:25;
|
||||
const auto url = makeUrl(query + "&format=xml", firstResultIndex, realResultsPerPage);
|
||||
header = replace(header, "URL", replace(url, "&", "&"));
|
||||
header = replace(header, "FIRSTRESULT", to_string(firstResultIndex));
|
||||
header = replace(header, "ITEMCOUNT", to_string(realResultsPerPage));
|
||||
header = replace(header, "RESULTCOUNT", to_string(totalResultCount));
|
||||
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
|
||||
header = replace(header, "ITEMCOUNT", std::to_string(realResultsPerPage));
|
||||
header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount));
|
||||
header = replace(header, "PATTERN", getPattern());
|
||||
auto queryLang = getLang();
|
||||
if (queryLang.empty()) {
|
||||
@@ -937,7 +937,7 @@ TEST_F(ServerTest, searchResults)
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/We_Gonna_Move_to_the_Outskirts_of_Town",
|
||||
/*link*/ "/ROOT/content/zimfile/A/We_Gonna_Move_to_the_Outskirts_of_Town",
|
||||
/*title*/ "We Gonna Move to the Outskirts of Town",
|
||||
/*snippet*/ R"SNIPPET(...to the Outskirts of Town "We Gonna Move to the Outskirts of Town" is a country blues song recorded September 3, 1936 by Casey Bill Weldon (voice and guitar). The song has been covered by many other musicians, most often under the title "I'm Gonna Move to the Outskirts of Town", and sometimes simply Outskirts of Town. All recordings seem to credit Weldon as songwriter, often as Weldon or as Will Weldon or as William Weldon. Some cover versions give credit also to Andy <b>Razaf</b> and/or to Roy Jacobs....)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -955,7 +955,7 @@ TEST_F(ServerTest, searchResults)
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Eleanor_Rigby",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Eleanor_Rigby",
|
||||
/*title*/ "Eleanor Rigby",
|
||||
/*snippet*/ R"SNIPPET(...-side "<b>Yellow</b> Submarine" (double A-side) Released 5 August 1966 (1966-08-05) Format 7-inch single Recorded 28–29 April & 6 June 1966 Studio EMI, London Genre Baroque pop, art rock Length 2:08 Label Parlophone (UK), Capitol (US) Songwriter(s) Lennon–McCartney Producer(s) George Martin The Beatles singles chronology "Paperback Writer" (1966) "Eleanor Rigby" / "<b>Yellow</b> Submarine" (1966) "Strawberry Fields Forever" / "Penny Lane" (1967) Music video "Eleanor Rigby" on YouTube The song continued the......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -963,7 +963,7 @@ TEST_F(ServerTest, searchResults)
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
|
||||
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...standard and has been recorded by many artists, including Greta Keller, for whom some say McKuen wrote the lyrics. "If You Go Away" Single by Damita Jo from the album If You Go Away B-side "<b>Yellow</b> Days" Released 1966 Genre Jazz Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -981,7 +981,7 @@ TEST_F(ServerTest, searchResults)
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Eleanor_Rigby",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Eleanor_Rigby",
|
||||
/*title*/ "Eleanor Rigby",
|
||||
/*snippet*/ R"SNIPPET(...-side "<b>Yellow</b> Submarine" (double A-side) Released 5 August 1966 (1966-08-05) Format 7-inch single Recorded 28–29 April & 6 June 1966 Studio EMI, London Genre Baroque pop, art rock Length 2:08 Label Parlophone (UK), Capitol (US) Songwriter(s) Lennon–McCartney Producer(s) George Martin The Beatles singles chronology "Paperback Writer" (1966) "Eleanor Rigby" / "<b>Yellow</b> Submarine" (1966) "Strawberry Fields Forever" / "Penny Lane" (1967) Music video "Eleanor Rigby" on YouTube The song continued the......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -989,7 +989,7 @@ TEST_F(ServerTest, searchResults)
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
|
||||
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...standard and has been recorded by many artists, including Greta Keller, for whom some say McKuen wrote the lyrics. "If You Go Away" Single by Damita Jo from the album If You Go Away B-side "<b>Yellow</b> Days" Released 1966 Genre Jazz Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -1007,7 +1007,7 @@ TEST_F(ServerTest, searchResults)
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/Eleanor_Rigby",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Eleanor_Rigby",
|
||||
/*title*/ "Eleanor Rigby",
|
||||
/*snippet*/ R"SNIPPET(...-side "<b>Yellow</b> Submarine" (double A-side) Released 5 August 1966 (1966-08-05) Format 7-inch single Recorded 28–29 April & 6 June 1966 Studio EMI, London Genre Baroque pop, art rock Length 2:08 Label Parlophone (UK), Capitol (US) Songwriter(s) Lennon–McCartney Producer(s) George Martin The Beatles singles chronology "Paperback Writer" (1966) "Eleanor Rigby" / "<b>Yellow</b> Submarine" (1966) "Strawberry Fields Forever" / "Penny Lane" (1967) Music video "Eleanor Rigby" on YouTube The song continued the......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -1015,7 +1015,7 @@ TEST_F(ServerTest, searchResults)
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
|
||||
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...standard and has been recorded by many artists, including Greta Keller, for whom some say McKuen wrote the lyrics. "If You Go Away" Single by Damita Jo from the album If You Go Away B-side "<b>Yellow</b> Days" Released 1966 Genre Jazz Length 3:49 Label Epic Records Songwriter(s) Jacques Brel, Rod McKuen Producer(s) Bob Morgan Damita Jo singles chronology "Gotta Travel On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -1322,7 +1322,7 @@ TEST_F(ServerTest, searchResults)
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
|
||||
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -1330,7 +1330,7 @@ TEST_F(ServerTest, searchResults)
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/example/Wikibooks.html",
|
||||
/*link*/ "/ROOT/content/example/Wikibooks.html",
|
||||
/*title*/ "Wikibooks",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
|
||||
/*bookTitle*/ "Wikibooks",
|
||||
@@ -1351,7 +1351,7 @@ TEST_F(ServerTest, searchResults)
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/zimfile/A/If_You_Go_Away",
|
||||
/*link*/ "/ROOT/content/zimfile/A/If_You_Go_Away",
|
||||
/*title*/ "If You Go Away",
|
||||
/*snippet*/ R"SNIPPET(...<b>Travel</b> On" (1965) "If You Go Away" (1966) "Walk Away" (1967) Damita Jo reached #10 on the Adult Contemporary chart and #68 on the Billboard Hot 100 in 1966 for her version of the song. Terry Jacks recorded a version of the song which was released as a single in 1974 and reached #29 on the Adult Contemporary chart, #68 on the Billboard Hot 100, and went to #8 in the UK. The complex melody is partly derivative of classical music - the poignant "But if you stay..." passage comes from Franz Liszt's......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "../include/server.h"
|
||||
#include "../include/name_mapper.h"
|
||||
#include "../include/tools.h"
|
||||
#include <zim/entry.h>
|
||||
#include <zim/item.h>
|
||||
|
||||
// Output generated via mustache templates sometimes contains end-of-line
|
||||
// whitespace. This complicates representing the expected output of a unit-test
|
||||
|
||||
@@ -31,6 +31,32 @@ using namespace kiwix;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Some unit-tests may fail because of partial/missing ICU data. This test
|
||||
// is intended to pinpoint to the root cause in such build environments.
|
||||
TEST(stringTools, ICULanguageInfo)
|
||||
{
|
||||
ASSERT_GE(ICULanguageInfo("en").selfName(), "English");
|
||||
ASSERT_GE(ICULanguageInfo("eng").selfName(), "English");
|
||||
ASSERT_GE(ICULanguageInfo("fr").selfName(), "français");
|
||||
ASSERT_GE(ICULanguageInfo("fra").selfName(), "français");
|
||||
ASSERT_GE(ICULanguageInfo("de").selfName(), "Deutsch");
|
||||
ASSERT_GE(ICULanguageInfo("deu").selfName(), "Deutsch");
|
||||
ASSERT_GE(ICULanguageInfo("es").selfName(), "español");
|
||||
ASSERT_GE(ICULanguageInfo("spa").selfName(), "español");
|
||||
ASSERT_GE(ICULanguageInfo("it").selfName(), "italiano");
|
||||
ASSERT_GE(ICULanguageInfo("ita").selfName(), "italiano");
|
||||
ASSERT_GE(ICULanguageInfo("ru").selfName(), "русский");
|
||||
ASSERT_GE(ICULanguageInfo("rus").selfName(), "русский");
|
||||
ASSERT_GE(ICULanguageInfo("hy").selfName(), "հայերեն");
|
||||
ASSERT_GE(ICULanguageInfo("hye").selfName(), "հայերեն");
|
||||
ASSERT_GE(ICULanguageInfo("zh").selfName(), "中文");
|
||||
ASSERT_GE(ICULanguageInfo("zho").selfName(), "中文");
|
||||
ASSERT_GE(ICULanguageInfo("ar").selfName(), "العربية");
|
||||
ASSERT_GE(ICULanguageInfo("ara").selfName(), "العربية");
|
||||
ASSERT_GE(ICULanguageInfo("c++").selfName(), "c++");
|
||||
}
|
||||
|
||||
TEST(stringTools, join)
|
||||
{
|
||||
std::vector<std::string> list = { "a", "b", "c" };
|
||||
|
||||