Compare commits

...

10 Commits

18 changed files with 532 additions and 74 deletions

View File

@ -25,7 +25,7 @@ add_link_options(${FLAGS})
add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp
blankie/serializer.cpp blankie/escape.cpp blankie/murl.cpp
routes/home.cpp routes/css.cpp routes/artworks.cpp routes/users/common.cpp routes/users/illustrations.cpp)
routes/home.cpp routes/css.cpp routes/artworks.cpp routes/tags.cpp routes/users/common.cpp routes/users/illustrations.cpp)
set_target_properties(${PROJECT_NAME}
PROPERTIES
CXX_STANDARD 20

View File

@ -5,11 +5,13 @@ Pixwhile is an alternative frontend to Pixiv that utilizes no Javascript.
- Does not contain an asinine amount of Javascript
- Allows you to open the original versions of cover images and profile pictures
- Can view illustrations and list user illustrations
- Can search for newest and oldest illustrations
## Missing Features
This list is not exhaustive, nor does it mean that these are being worked on.
- Can only search for newest and oldest illustrations
- No search
- No ability to login
- No ability to see comments or like counts

View File

@ -38,6 +38,8 @@
"(?:\\?" QUERY ")?" \
"(?:#" FRAGMENT ")?"
static inline void hexencode(char c, char out[2]);
static inline char hexdecode(char nibble1, char nibble2);
static void handle_segment(std::vector<std::string>& segments, const std::string& str, size_t offset, size_t length);
static std::string tolower(std::string str);
@ -93,6 +95,52 @@ bool Url::is_host_equal(std::string other) const {
return tolower(this->hostname) == tolower(std::move(other));
}
std::string escape(const std::string& in) {
std::string out;
char encoded[2];
size_t pos = 0;
size_t last_pos = 0;
out.reserve(in.size());
while ((pos = in.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", pos)) != std::string::npos) {
out.append(in, last_pos, pos - last_pos);
hexencode(in[pos], encoded);
out += '%';
out.append(encoded, 2);
pos++;
last_pos = pos;
}
if (in.size() > last_pos) {
out.append(in, last_pos);
}
return out;
}
std::string unescape(const std::string& in) {
std::string out;
size_t pos = 0;
size_t last_pos = 0;
out.reserve(in.size());
while ((pos = in.find('%', pos)) != std::string::npos) {
if (pos + 2 >= in.size()) {
throw std::invalid_argument("String abruptly terminated while finding percent-encoded nibbles");
}
out.append(in, last_pos, pos - last_pos);
out += hexdecode(in[pos + 1], in[pos + 2]);
pos += 3;
last_pos = pos;
}
if (in.size() > last_pos) {
out.append(in, last_pos);
}
return out;
}
std::string normalize_path(const std::string& str) {
std::vector<std::string> segments;
std::string res;
@ -137,6 +185,30 @@ std::string normalize_path(const std::string& str) {
} // namespace murl
} // namespace blankie
static inline void hexencode(char c, char out[2]) {
char nibble1 = (c >> 4) & 0xF;
char nibble2 = c & 0xF;
auto hexencode = [](char nibble) -> char {
return nibble < 10
? '0' + nibble
: 'A' + nibble - 10;
};
out[0] = hexencode(nibble1);
out[1] = hexencode(nibble2);
}
static inline char hexdecode(char nibble1, char nibble2) {
auto hexdecode = [](char nibble) -> char {
if (nibble >= '0' && nibble <= '9') return nibble - '0';
if (nibble >= 'A' && nibble <= 'F') return nibble - 'A' + 10;
if (nibble >= 'a' && nibble <= 'f') return nibble - 'a' + 10;
throw std::invalid_argument("Invalid percent-encoded nibble received");
};
return static_cast<char>((hexdecode(nibble1) << 4) | hexdecode(nibble2));
}
static void handle_segment(std::vector<std::string>& segments, const std::string& str, size_t offset, size_t length) {
if (length == 2 && str[offset] == '.' && str[offset + 1] == '.') {
if (segments.empty()) {

View File

@ -38,6 +38,8 @@ struct Url {
std::string to_string() const;
};
std::string escape(const std::string& in);
std::string unescape(const std::string& in);
std::string normalize_path(const std::string& str);
} // namespace murl

View File

@ -52,5 +52,9 @@ std::string Element::serialize() const {
} // namespace blankie
static inline bool is_autoclosing_tag(const char* tag) {
return !strncmp(tag, "link", 5) || !strncmp(tag, "meta", 5) || !strncmp(tag, "img", 4) || !strncmp(tag, "br", 3);
return !strncmp(tag, "link", 5)
|| !strncmp(tag, "meta", 5)
|| !strncmp(tag, "img", 4)
|| !strncmp(tag, "br", 3)
|| !strncmp(tag, "label", 6);
}

View File

@ -37,6 +37,12 @@ int main(int argc, char** argv) {
server.Get("/artworks/(\\d+)", [&](const httplib::Request& req, httplib::Response& res) {
artworks_route(req, res, config, pixiv_client);
});
server.Get("/tags/([^/]+)", [&](const httplib::Request& req, httplib::Response& res) {
serve_redirect(req, res, config, get_origin(req, config) + "/tags/" + req.matches.str(1) + "/illustrations");
});
server.Get("/tags/([^/]+)/illustrations", [&](const httplib::Request& req, httplib::Response& res) {
tags_route(req, res, config, pixiv_client);
});
server.Get("/member\\.php", [&](const httplib::Request& req, httplib::Response& res) {
std::string id = req.get_param_value("id");
@ -56,6 +62,15 @@ int main(int argc, char** argv) {
}
serve_redirect(req, res, config, get_origin(req, config) + "/artworks/" + illust_id);
});
server.Get("/search", [&](const httplib::Request& req, httplib::Response& res) {
std::string q = req.get_param_value("q");
if (q.empty()) {
res.status = 400;
serve_error(req, res, config, "400: Bad Request", "Missing or empty search query");
return;
}
serve_redirect(req, res, config, get_origin(req, config) + "/tags/" + blankie::murl::escape(std::move(q)));
});
#ifndef NDEBUG
server.Get("/debug/exception/known", [](const httplib::Request& req, httplib::Response& res) {

View File

@ -1,17 +1,21 @@
#include "blankie/murl.h"
#include "numberhelper.h"
#include "pixivclient.h"
static const constexpr char* touch_user_agent = "Mozilla/5.0 (Android 12; Mobile; rv:97.0) Gecko/97.0 Firefox/97.0";
static const constexpr char* desktop_user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:111.0) Gecko/20100101 Firefox/111.0";
PixivClient::PixivClient() {
this->_www_pixiv_net_client.set_keep_alive(true);
this->_www_pixiv_net_client.set_default_headers({
{"User-Agent", "Mozilla/5.0 (Android 12; Mobile; rv:97.0) Gecko/97.0 Firefox/97.0"}
{"Cookie", "webp_available=1"}
});
}
User PixivClient::get_user(uint64_t user_id) {
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/details", {
{"lang", "en"}, {"id", std::to_string(user_id)}
}, httplib::Headers());
}, {{"User-Agent", touch_user_agent}});
return this->_handle_result(std::move(res)).at("user_details").get<User>();
}
@ -20,17 +24,54 @@ Illusts PixivClient::get_illusts(uint64_t user_id, size_t page) {
if (page != 0) {
params.insert({"p", std::to_string(page + 1)});
}
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/illusts", std::move(params), httplib::Headers());
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/illusts", std::move(params), {{"User-Agent", touch_user_agent}});
return this->_handle_result(std::move(res)).get<Illusts>();
}
Illust PixivClient::get_illust(uint64_t illust_id) {
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/illust/details", {
{"lang", "en"}, {"illust_id", std::to_string(illust_id)}
}, httplib::Headers());
}, {{"User-Agent", touch_user_agent}});
return this->_handle_result(std::move(res)).get<Illust>();
}
SearchResults PixivClient::search_illusts(const std::string& query, size_t page, const std::string& order) {
using namespace std::string_literals;
httplib::Result res = this->_www_pixiv_net_client.Get("/ajax/search/illustrations/"s + blankie::murl::escape(query), {
{"lang", "en"},
{"mode", "all"},
{"p", std::to_string(page + 1)},
{"s_mode", "s_tag_full"},
{"type", "illust_and_ugoira"},
{"order", order},
{"word", query}
}, {{"User-Agent", desktop_user_agent}});
return this->_handle_result(std::move(res)).get<SearchResults>();
}
std::vector<SearchSuggestion> PixivClient::get_search_suggestions(const std::string& query) {
httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", {
{"lang", "en"}, {"keyword", query}
}, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}});
if (!res) {
throw HTTPLibException(res.error());
}
nlohmann::json j = nlohmann::json::parse(std::move(res->body)).at("candidates");
std::vector<SearchSuggestion> search_suggestions;
search_suggestions.reserve(j.size());
for (const nlohmann::json& i : j) {
SearchSuggestion search_suggestion = i.get<SearchSuggestion>();
if (search_suggestion.tag != query) {
search_suggestions.push_back(std::move(search_suggestion));
}
}
return search_suggestions;
}
nlohmann::json PixivClient::_handle_result(httplib::Result res) {
if (!res) {
throw HTTPLibException(res.error());

View File

@ -14,6 +14,9 @@ public:
Illusts get_illusts(uint64_t user_id, size_t page);
Illust get_illust(uint64_t illust_id);
SearchResults search_illusts(const std::string& query, size_t page, const std::string& order);
std::vector<SearchSuggestion> get_search_suggestions(const std::string& query);
private:
nlohmann::json _handle_result(httplib::Result res);
httplib::Client _www_pixiv_net_client{"https://www.pixiv.net"};

View File

@ -8,7 +8,9 @@
static inline std::optional<std::string> get_1920x960_cover_image(blankie::murl::Url url);
static inline std::optional<std::string> get_original_cover_image(blankie::murl::Url url, const nlohmann::json& cover_image);
static inline std::optional<std::string> get_original_profile_picture(blankie::murl::Url url);
static inline std::optional<std::string> get_360x360_illust_thumbnail(blankie::murl::Url url);
static Images get_profile_pictures(const nlohmann::json& j);
static Images get_profile_pictures(const std::string& url);
static Images get_illust_image(const nlohmann::json& j);
const std::string& Images::original_or_thumbnail() const {
@ -80,9 +82,6 @@ void from_json(const nlohmann::json& j, User& user) {
void from_json(const nlohmann::json& j, Tag& tag) {
j.at("tag").get_to(tag.japanese);
if (j.contains("romaji")) {
tag.romaji = j["romaji"].get<std::string>();
}
if (j.contains("translation")) {
tag.english = j["translation"].get<std::string>();
}
@ -129,6 +128,75 @@ void from_json(const nlohmann::json& j, Illusts& illusts) {
j.at("lastPage").get_to(illusts.total_pages);
}
void from_json(const nlohmann::json& j, SearchResults& search_results) {
const nlohmann::json& tag_translations = j.at("tagTranslation");
auto get_translated_tag = [&](const std::string& japanese) -> std::optional<std::string> {
if (!tag_translations.is_object() || !tag_translations.contains(japanese)) {
return std::nullopt;
}
const nlohmann::json& tag = tag_translations[japanese];
return tag.contains("en")
? std::optional(tag["en"].get<std::string>())
: std::nullopt;
};
const nlohmann::json& illusts = j.at("illust").at("data");
search_results.illusts.illusts.reserve(illusts.size());
for (const nlohmann::json& i : illusts) {
const nlohmann::json& i_tags = i.at("tags");
std::vector<Tag> tags;
tags.reserve(i_tags.size());
for (const nlohmann::json& tag : i_tags) {
std::string japanese = tag.get<std::string>();
tags.push_back({japanese, get_translated_tag(std::move(japanese))});
}
Illust illust = {
.username = "",
.user_display_name = i.at("userName").get<std::string>(),
.user_id = to_ull(i.at("userId").get<std::string>()),
.user_profile_pictures = get_profile_pictures(i.at("profileImageUrl").get<std::string>()),
.illust_id = to_ull(i.at("id").get<std::string>()),
.title = i.at("title").get<std::string>(),
.ai_generated = i.at("aiType").get<int>() == 2,
// pixiv does have a createDate field, but it can't be portably parsed by strptime
// and i cba to use regex for it, especially when it's not even used in this context
.upload_time = -1,
.comment = std::nullopt,
.tags = std::move(tags),
.images = {get_illust_image(i)}
};
search_results.illusts.illusts.push_back(illust);
}
j.at("illust").at("total").get_to(search_results.illusts.total_illusts);
search_results.illusts.total_pages = search_results.illusts.total_illusts / 60;
if (search_results.illusts.total_illusts % 60 != 0) {
search_results.illusts.total_pages++;
}
if (search_results.illusts.total_pages > 10) {
search_results.illusts.total_pages = 10;
}
search_results.tag_translations.reserve(tag_translations.size());
for (auto &[key, val] : tag_translations.items()) {
std::optional<std::string> translated_tag = get_translated_tag(key);
if (translated_tag) {
search_results.tag_translations.insert({std::move(key), std::move(*translated_tag)});
}
}
}
void from_json(const nlohmann::json& j, SearchSuggestion& search_suggestion) {
j.at("tag_name").get_to(search_suggestion.tag);
if (j.at("type").get_ref<const nlohmann::json::string_t&>() == "tag_translation") {
search_suggestion.english_tag = j.at("tag_translation").get<std::string>();
}
}
static std::regex resolution_path_regex("/c/(\\d+x\\d+)(.+)");
static inline std::optional<std::string> get_1920x960_cover_image(blankie::murl::Url url) {
std::smatch sm;
@ -182,22 +250,56 @@ static Images get_profile_pictures(const nlohmann::json& j) {
return images;
}
static Images get_profile_pictures(const std::string& url) {
return {
.original = get_original_profile_picture(url),
.thumbnails = {url}
};
}
static std::regex illust_360x360_thumbnail_path_regex(
"(?:/c/[^/]+?/img-master|/img-original)?"
"(/img/.*/\\d+_p\\d+)(?:_master1200|_square1200)?\\.\\w{3,4}"
);
static inline std::optional<std::string> get_360x360_illust_thumbnail(blankie::murl::Url url) {
using namespace std::string_literals;
std::smatch sm;
if (!std::regex_match(url.path, sm, illust_360x360_thumbnail_path_regex)) {
return std::nullopt;
}
url.path = "/c/360x360_10_webp/img-master"s + sm.str(1) + "_square1200.jpg";
return url.to_string();
}
static Images get_illust_image(const nlohmann::json& j) {
Images images;
ssize_t add_360x360_to = -1;
auto add_if_exists = [&](const char* key) {
if (j.contains(key) && j[key].is_string()) {
images.thumbnails.push_back(j[key].get<std::string>());
return true;
}
return false;
};
add_if_exists("url_ss");
add_if_exists("url_placeholder");
add_if_exists("url_small");
add_if_exists("url_s");
if (!add_if_exists("url_s")) {
add_360x360_to = static_cast<ssize_t>(images.thumbnails.size());
}
add_if_exists("url");
if (j.contains("url_big") && j["url_big"].is_string()) {
images.original = j["url_big"].get<std::string>();
}
if (add_360x360_to >= 0) {
std::optional<std::string> c_360x360 = get_360x360_illust_thumbnail(images.original_or_thumbnail());
if (c_360x360) {
images.thumbnails.insert(images.thumbnails.begin() + add_360x360_to, std::move(*c_360x360));
}
}
return images;
}

View File

@ -27,7 +27,6 @@ struct User {
struct Tag {
std::string japanese;
std::optional<std::string> romaji;
std::optional<std::string> english;
};
@ -53,7 +52,19 @@ struct Illusts {
size_t total_pages;
};
struct SearchResults {
Illusts illusts;
std::unordered_map<std::string, std::string> tag_translations;
};
struct SearchSuggestion {
std::string tag;
std::optional<std::string> english_tag;
};
void from_json(const nlohmann::json& j, User& user);
void from_json(const nlohmann::json& j, Tag& tag);
void from_json(const nlohmann::json& j, Illust& illust);
void from_json(const nlohmann::json& j, Illusts& illusts);
void from_json(const nlohmann::json& j, SearchResults& search_results);
void from_json(const nlohmann::json& j, SearchSuggestion& search_suggestion);

View File

@ -11,7 +11,7 @@ static inline Element generate_images(const httplib::Request& req, const Config&
static inline Element generate_preview_images(const httplib::Request& req, const Config& config, const Illust& illust);
static inline std::vector<blankie::html::Node> parse_description_line(const httplib::Request& req, const Config& config, std::string str);
static inline Element generate_description(const httplib::Request& req, const Config& config, const std::string& description);
static inline Element generate_illust_tags(const Illust& illust);
static inline Element generate_illust_tags(const httplib::Request& req, const Config& config, const Illust& illust);
static inline bool is_true(const std::string& str);
static inline std::string time_to_string(time_t time);
@ -42,13 +42,14 @@ void artworks_route(const httplib::Request& req, httplib::Response& res, const C
Element body("body", {
Element("h2", {illust.title}),
generate_user_link(req, config, illust),
Element("br"),
!preview ? generate_images(req, config, illust) : generate_preview_images(req, config, illust),
Element("br")
});
if (illust.comment) {
body.nodes.push_back(generate_description(req, config, *illust.comment));
}
body.nodes.push_back(generate_illust_tags(illust));
body.nodes.push_back(generate_illust_tags(req, config, illust));
body.nodes.push_back(Element("p", {time_to_string(illust.upload_time)}));
serve(req, res, config, std::move(illust.title), std::move(body));
}
@ -160,7 +161,7 @@ static inline Element generate_description(const httplib::Request& req, const Co
return p;
}
static inline Element generate_illust_tags(const Illust& illust) {
static inline Element generate_illust_tags(const httplib::Request& req, const Config& config, const Illust& illust) {
Element div("div", {{"class", "illusttags"}}, {});
if (illust.ai_generated) {
@ -170,11 +171,12 @@ static inline Element generate_illust_tags(const Illust& illust) {
for (const Tag& i : illust.tags) {
std::string tag = [&]() {
if (i.english) return *i.english;
if (i.romaji) return *i.romaji;
return i.japanese;
}();
div.nodes.push_back(Element("span", {"#", std::move(tag)}));
div.nodes.push_back(Element("a", {{"href", get_origin(req, config) + "/tags/" + blankie::murl::escape(i.japanese)}}, {
"#", std::move(tag)
}));
}
return div;

View File

@ -37,6 +37,11 @@ void css_route(const httplib::Request& req, httplib::Response& res) {
max-width: 100%;
}
.center {
text-align: center;
display: block;
}
/* USER PAGE (and a tiny bit for illustrations page) */
.cover {
width: 100%;
@ -61,10 +66,6 @@ void css_route(const httplib::Request& req, httplib::Response& res) {
}
/* USER ILLUSTRATIONS PAGE (and illustrations page) */
.center {
text-align: center;
display: block;
}
.grid {
display: flex;
flex-wrap: wrap;
@ -94,6 +95,14 @@ void css_route(const httplib::Request& req, httplib::Response& res) {
padding-bottom: 1em;
}
/* SEARCH RESULTS PAGE */
.searchsuggestions {
text-align: left;
display: inline-block;
margin-top: .5em;
margin-bottom: 0;
}
/* ERROR PAGE */
.error {
text-align: center;

View File

@ -15,6 +15,12 @@ void home_route(const httplib::Request& req, httplib::Response& res, const Confi
"Pixwhile is an alternative frontend to Pixiv that utilizes no Javascript. (",
Element("a", {{"href", "https://gitlab.com/blankX/pixwhile"}}, {"source code"}),
")",
Element("form", {{"method", "get"}, {"action", "search"}}, {
Element("br"),
Element("input", {{"name", "q"}, {"required", ""}}, {}),
" ",
Element("button", {"Search for illustrations"})
}),
Element("h2", {"Try it out"}),
Element("ul", {
get_artwork_element(106623268, "アル社長の日常", 1960050, "torino"),
@ -24,12 +30,13 @@ void home_route(const httplib::Request& req, httplib::Response& res, const Confi
Element("ul", {
Element("li", {"Does not contain an asinine amount of Javascript"}),
Element("li", {"Allows you to open the original versions of cover images and profile pictures"}),
Element("li", {"Can view illustrations and list user illustrations"})
Element("li", {"Can view illustrations and list user illustrations"}),
Element("li", {"Can search for newest and oldest illustrations"})
}),
Element("h2", {"Missing Features"}),
Element("p", {"This list is not exhaustive, nor does it mean that these are being worked on."}),
Element("ul", {
Element("li", {"No search"}),
Element("li", {"Can only search for newest and oldest illustrations"}),
Element("li", {"No ability to login"}),
Element("li", {"No ability to see comments or like counts"}),
})

View File

@ -9,3 +9,4 @@ void home_route(const httplib::Request& req, httplib::Response& res, const Confi
void css_route(const httplib::Request& req, httplib::Response& res);
void user_illustrations_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client);
void artworks_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client);
void tags_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client);

177
routes/tags.cpp Normal file
View File

@ -0,0 +1,177 @@
#include "routes.h"
#include "../numberhelper.h"
#include "../servehelper.h"
#include "../pixivclient.h"
static inline Element generate_header(const httplib::Request& req, const Config& config,
const SearchResults& search_results, const std::string& query, const std::vector<std::string>& tags, const std::string& order,
const std::vector<SearchSuggestion>& search_suggestions);
static inline Element generate_search_suggestions(const httplib::Request& req, const Config& config,
const std::vector<std::string>& tags, const std::vector<SearchSuggestion>& search_suggestions, bool open_by_default);
static std::string tags_to_string(const std::unordered_map<std::string, std::string>& tag_translations, const std::vector<std::string>& tags);
static inline std::vector<std::string> split(const std::string& str, char c);
static inline std::string join(const std::vector<std::string>& items, char c);
void tags_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client) {
std::string query = blankie::murl::unescape(req.matches.str(1));
std::string order = req.has_param("order") ? req.get_param_value("order") : "date_d";
unsigned long long page = req.has_param("p") ? to_ull(req.get_param_value("p")) - 1 : 0;
std::vector<std::string> tags = split(query, ' ');
if (tags.empty()) {
res.status = 400;
serve_error(req, res, config, "400: Bad Request", "Empty search query");
return;
}
SearchResults search_results;
try {
search_results = pixiv_client.search_illusts(query, page, order);
} catch (const std::exception& e) {
res.status = 500;
serve_error(req, res, config, "500: Internal server error", "Failed to search for illusts", e.what());
return;
}
std::vector<SearchSuggestion> search_suggestions;
try {
search_suggestions = pixiv_client.get_search_suggestions(tags.back());
} catch (const std::exception& e) {
res.status = 500;
serve_error(req, res, config, "500: Internal server error", "Failed to get search suggestions", e.what());
return;
}
Element body("body", {
generate_header(req, config, search_results, query, tags, order, search_suggestions),
Element("br"),
generate_illusts_pager(req, config, search_results.illusts, page, "illusts")
});
serve(req, res, config, tags_to_string(std::move(search_results.tag_translations), std::move(tags)), std::move(body));
}
static inline Element generate_header(const httplib::Request& req, const Config& config,
const SearchResults& search_results, const std::string& query, const std::vector<std::string>& tags, const std::string& order,
const std::vector<SearchSuggestion>& search_suggestions) {
auto sort_element = [&](const char* title, const char* new_order) {
std::string url = get_origin(req, config) + "/tags/" + blankie::murl::escape(query) + "/illustrations?order=" + new_order;
Element ret("a", {{"href", std::move(url)}}, {title});
if (new_order == order) {
ret = Element("b", {std::move(ret)});
}
return ret;
};
Element header("header", {{"class", "center"}}, {
Element("form", {{"method", "get"}, {"action", get_origin(req, config) + "/search"}}, {
Element("input", {{"name", "q"}, {"required", ""}, {"value", query}}, {}),
" ",
Element("button", {"Search for illustrations"})
})
});
if (!search_suggestions.empty()) {
header.nodes.push_back(generate_search_suggestions(req, config, tags, search_suggestions, search_results.illusts.total_illusts == 0));
}
header.nodes.push_back(Element("br"));
if (search_results.illusts.total_illusts != 1) {
header.nodes.push_back("There are ");
header.nodes.push_back(std::to_string(search_results.illusts.total_illusts));
header.nodes.push_back(" illustrations");
} else {
header.nodes.push_back("There is 1 illustration");
}
header.nodes.push_back(" of ");
header.nodes.push_back(Element("b", {tags_to_string(search_results.tag_translations, tags)}));
header.nodes.push_back(Element("br"));
header.nodes.push_back("Sort by: ");
header.nodes.push_back(sort_element("Newest", "date_d"));
header.nodes.push_back(" ");
header.nodes.push_back(sort_element("Oldest", "date"));
return header;
}
static inline Element generate_search_suggestions(const httplib::Request& req, const Config& config,
const std::vector<std::string>& tags, const std::vector<SearchSuggestion>& search_suggestions, bool open_by_default) {
std::vector<blankie::html::Node> ul_nodes;
ul_nodes.reserve(search_suggestions.size());
for (const SearchSuggestion& search_suggestion : search_suggestions) {
std::string text = search_suggestion.tag;
if (search_suggestion.english_tag) {
text += " (";
text += *search_suggestion.english_tag;
text += ')';
}
std::vector<std::string> new_tags = tags;
new_tags.pop_back();
new_tags.push_back(search_suggestion.tag);
std::string url = get_origin(req, config) + "/tags/" + blankie::murl::escape(join(new_tags, ' '));
ul_nodes.push_back(Element("li", {
Element("a", {{"href", std::move(url)}}, {std::move(text)})
}));
}
Element details("details", {
Element("summary", {"Search suggestions"}),
Element("ul", {{"class", "searchsuggestions"}}, ul_nodes)
});
if (open_by_default) {
details.attributes.push_back({"open", ""});
}
return details;
}
static std::string tags_to_string(const std::unordered_map<std::string, std::string>& tag_translations, const std::vector<std::string>& tags) {
std::string str;
for (const std::string& tag : tags) {
if (!str.empty()) {
str += ' ';
}
str += '#';
auto translated_tag = tag_translations.find(tag);
str += translated_tag != tag_translations.cend() ? translated_tag->second : tag;
}
return str;
}
static inline std::vector<std::string> split(const std::string& str, char c) {
std::vector<std::string> ret;
size_t pos = 0;
size_t last_pos = 0;
while ((pos = str.find(c, pos)) != std::string::npos) {
if (pos - last_pos > 0) {
ret.push_back(str.substr(last_pos, pos - last_pos));
}
pos++;
last_pos = pos;
}
if (str.size() > last_pos) {
ret.push_back(str.substr(last_pos));
}
return ret;
}
static inline std::string join(const std::vector<std::string>& items, char c) {
std::string ret;
for (size_t i = 0; i < items.size(); i++) {
if (i) {
ret += c;
}
ret += items[i];
}
return ret;
}

View File

@ -6,9 +6,6 @@
#include "../../pixivclient.h"
#include "common.h"
static Element generate_pager(const Illusts& illusts, size_t page, bool first_selector);
static inline Element generate_content(const httplib::Request& req, const Config& config, const Illusts& illusts);
void user_illustrations_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client) {
uint64_t user_id = to_ull(req.matches[1].str());
uint64_t page = req.has_param("p") ? to_ull(req.get_param_value("p")) - 1 : 0;
@ -35,55 +32,7 @@ void user_illustrations_route(const httplib::Request& req, httplib::Response& re
Element body("body", {
generate_user_header(std::move(user), config),
generate_pager(illusts, page, true),
Element("br"),
generate_content(req, config, illusts),
generate_pager(illusts, page, false)
generate_illusts_pager(req, config, illusts, page, "illusts")
});
serve(req, res, config, user.display_name + "'s illustrations", std::move(body));
}
static Element generate_pager(const Illusts& illusts, size_t page, bool first_selector) {
auto link = [](size_t new_page, const char* text, bool add_link) {
using namespace std::string_literals;
Element b("b");
std::string href = "?p="s + std::to_string(new_page) + "#pageselector";
if (add_link) {
b.nodes.push_back(Element("a", {{"href", std::move(href)}}, {text}));
} else {
b.nodes.push_back(text);
}
return b;
};
Element div("div", {{"class", "center"}}, {
link(1, "First", page != 0), " ",
link(page, "Prev", page != 0), " ",
std::to_string(page + 1), "/", std::to_string(illusts.total_pages), " ",
link(page + 2, "Next", page + 1 < illusts.total_pages), " ",
link(illusts.total_pages, "Last", page + 1 < illusts.total_pages)
});
if (first_selector) {
div.attributes.push_back({"id", "pageselector"});
}
return div;
}
static inline Element generate_content(const httplib::Request& req, const Config& config, const Illusts& illusts) {
Element div("div", {{"class", "grid"}}, {});
div.nodes.reserve(illusts.illusts.size());
for (const Illust& i : illusts.illusts) {
std::string illust_url = get_origin(req, config) + "/artworks/" + std::to_string(i.illust_id);
std::string image_url = proxy_image_url(config, i.images[0].thumbnail_or_original(1));
div.nodes.push_back(Element("a", {{"href", {std::move(illust_url)}}}, {
Element("img", {{"loading", "lazy"}, {"src", std::move(image_url)}}, {}),
Element("p", {i.title})
}));
}
return div;
}

View File

@ -1,8 +1,12 @@
#include <regex>
#include "config.h"
#include "pixivmodels.h"
#include "servehelper.h"
static Element generate_pager(const Illusts& illusts, size_t page, const char* id);
static inline Element generate_content(const httplib::Request& req, const Config& config, const Illusts& illusts);
void serve(const httplib::Request& req, httplib::Response& res, const Config& config, std::string title, Element element) {
using namespace std::string_literals;
@ -55,6 +59,8 @@ void serve_redirect(const httplib::Request& req, httplib::Response& res, const C
serve(req, res, config, "Redirecting to "s + std::move(url) + "", std::move(body));
}
std::string get_origin(const httplib::Request& req, const Config& config) {
if (req.has_header("X-Canonical-Origin")) {
return req.get_header_value("X-Canonical-Origin");
@ -91,3 +97,54 @@ std::string proxy_image_url(const Config& config, blankie::murl::Url url) {
}
return proxy_url(config.image_proxy_url, std::move(url));
}
Element generate_illusts_pager(const httplib::Request& req, const Config& config, const Illusts& illusts, size_t page, const char* id) {
return Element("div", {{"id", id}}, {
generate_pager(illusts, page, id),
Element("br"),
generate_content(req, config, illusts),
generate_pager(illusts, page, id)
});
}
static Element generate_pager(const Illusts& illusts, size_t page, const char* id) {
auto link = [&](size_t new_page, const char* text, bool add_link) {
using namespace std::string_literals;
Element b("b");
if (add_link) {
std::string href = "?p="s + std::to_string(new_page) + '#' + id;
b.nodes.push_back(Element("a", {{"href", std::move(href)}}, {text}));
} else {
b.nodes.push_back(text);
}
return b;
};
return Element("div", {{"class", "center"}}, {
link(1, "First", page != 0), " ",
link(page, "Prev", page != 0), " ",
std::to_string(page + 1), "/", std::to_string(illusts.total_pages), " ",
link(page + 2, "Next", page + 1 < illusts.total_pages), " ",
link(illusts.total_pages, "Last", page + 1 < illusts.total_pages)
});
}
static inline Element generate_content(const httplib::Request& req, const Config& config, const Illusts& illusts) {
Element div("div", {{"class", "grid"}}, {});
div.nodes.reserve(illusts.illusts.size());
for (const Illust& i : illusts.illusts) {
std::string illust_url = get_origin(req, config) + "/artworks/" + std::to_string(i.illust_id);
std::string image_url = proxy_image_url(config, i.images[0].thumbnail_or_original(1));
div.nodes.push_back(Element("a", {{"href", {std::move(illust_url)}}}, {
Element("img", {{"loading", "lazy"}, {"src", std::move(image_url)}}, {}),
Element("p", {i.title})
}));
}
return div;
}

View File

@ -7,12 +7,16 @@
#include "blankie/serializer.h"
struct Config; // forward declaration from config.h
struct Illusts; // forward declaration from pixivmodels.h
using Element = blankie::html::Element;
void serve(const httplib::Request& req, httplib::Response& res, const Config& config, std::string title, Element element);
void serve_error(const httplib::Request& req, httplib::Response& res, const Config& config,
std::string title, std::optional<std::string> subtitle = std::nullopt, std::optional<std::string> info = std::nullopt);
void serve_redirect(const httplib::Request& req, httplib::Response& res, const Config& config, std::string url);
std::string get_origin(const httplib::Request& req, const Config& config);
std::string proxy_url(blankie::murl::Url base, blankie::murl::Url url);
std::string proxy_image_url(const Config& config, blankie::murl::Url url);
Element generate_illusts_pager(const httplib::Request& req, const Config& config, const Illusts& illusts, size_t page, const char* id);