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 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 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} set_target_properties(${PROJECT_NAME}
PROPERTIES PROPERTIES
CXX_STANDARD 20 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 - Does not contain an asinine amount of Javascript
- Allows you to open the original versions of cover images and profile pictures - Allows you to open the original versions of cover images and profile pictures
- Can view illustrations and list user illustrations - Can view illustrations and list user illustrations
- Can search for newest and oldest illustrations
## Missing Features ## Missing Features
This list is not exhaustive, nor does it mean that these are being worked on. 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 search
- No ability to login - No ability to login
- No ability to see comments or like counts - No ability to see comments or like counts

View File

@ -38,6 +38,8 @@
"(?:\\?" QUERY ")?" \ "(?:\\?" QUERY ")?" \
"(?:#" FRAGMENT ")?" "(?:#" 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 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); 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)); 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::string normalize_path(const std::string& str) {
std::vector<std::string> segments; std::vector<std::string> segments;
std::string res; std::string res;
@ -137,6 +185,30 @@ std::string normalize_path(const std::string& str) {
} // namespace murl } // namespace murl
} // namespace blankie } // 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) { 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 (length == 2 && str[offset] == '.' && str[offset + 1] == '.') {
if (segments.empty()) { if (segments.empty()) {

View File

@ -38,6 +38,8 @@ struct Url {
std::string to_string() const; 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); std::string normalize_path(const std::string& str);
} // namespace murl } // namespace murl

View File

@ -52,5 +52,9 @@ std::string Element::serialize() const {
} // namespace blankie } // namespace blankie
static inline bool is_autoclosing_tag(const char* tag) { 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) { server.Get("/artworks/(\\d+)", [&](const httplib::Request& req, httplib::Response& res) {
artworks_route(req, res, config, pixiv_client); 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) { server.Get("/member\\.php", [&](const httplib::Request& req, httplib::Response& res) {
std::string id = req.get_param_value("id"); 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); 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 #ifndef NDEBUG
server.Get("/debug/exception/known", [](const httplib::Request& req, httplib::Response& res) { 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 "numberhelper.h"
#include "pixivclient.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() { PixivClient::PixivClient() {
this->_www_pixiv_net_client.set_keep_alive(true); this->_www_pixiv_net_client.set_keep_alive(true);
this->_www_pixiv_net_client.set_default_headers({ 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) { User PixivClient::get_user(uint64_t user_id) {
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/details", { httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/details", {
{"lang", "en"}, {"id", std::to_string(user_id)} {"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>(); 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) { if (page != 0) {
params.insert({"p", std::to_string(page + 1)}); 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>(); return this->_handle_result(std::move(res)).get<Illusts>();
} }
Illust PixivClient::get_illust(uint64_t illust_id) { Illust PixivClient::get_illust(uint64_t illust_id) {
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/illust/details", { httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/illust/details", {
{"lang", "en"}, {"illust_id", std::to_string(illust_id)} {"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>(); 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) { nlohmann::json PixivClient::_handle_result(httplib::Result res) {
if (!res) { if (!res) {
throw HTTPLibException(res.error()); throw HTTPLibException(res.error());

View File

@ -14,6 +14,9 @@ public:
Illusts get_illusts(uint64_t user_id, size_t page); Illusts get_illusts(uint64_t user_id, size_t page);
Illust get_illust(uint64_t illust_id); 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: private:
nlohmann::json _handle_result(httplib::Result res); nlohmann::json _handle_result(httplib::Result res);
httplib::Client _www_pixiv_net_client{"https://www.pixiv.net"}; 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_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_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_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 nlohmann::json& j);
static Images get_profile_pictures(const std::string& url);
static Images get_illust_image(const nlohmann::json& j); static Images get_illust_image(const nlohmann::json& j);
const std::string& Images::original_or_thumbnail() const { 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) { void from_json(const nlohmann::json& j, Tag& tag) {
j.at("tag").get_to(tag.japanese); j.at("tag").get_to(tag.japanese);
if (j.contains("romaji")) {
tag.romaji = j["romaji"].get<std::string>();
}
if (j.contains("translation")) { if (j.contains("translation")) {
tag.english = j["translation"].get<std::string>(); 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); 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 std::regex resolution_path_regex("/c/(\\d+x\\d+)(.+)");
static inline std::optional<std::string> get_1920x960_cover_image(blankie::murl::Url url) { static inline std::optional<std::string> get_1920x960_cover_image(blankie::murl::Url url) {
std::smatch sm; std::smatch sm;
@ -182,22 +250,56 @@ static Images get_profile_pictures(const nlohmann::json& j) {
return images; 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) { static Images get_illust_image(const nlohmann::json& j) {
Images images; Images images;
ssize_t add_360x360_to = -1;
auto add_if_exists = [&](const char* key) { auto add_if_exists = [&](const char* key) {
if (j.contains(key) && j[key].is_string()) { if (j.contains(key) && j[key].is_string()) {
images.thumbnails.push_back(j[key].get<std::string>()); images.thumbnails.push_back(j[key].get<std::string>());
return true;
} }
return false;
}; };
add_if_exists("url_ss"); add_if_exists("url_ss");
add_if_exists("url_placeholder"); add_if_exists("url_placeholder");
add_if_exists("url_small"); 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"); add_if_exists("url");
if (j.contains("url_big") && j["url_big"].is_string()) { if (j.contains("url_big") && j["url_big"].is_string()) {
images.original = j["url_big"].get<std::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; return images;
} }

View File

@ -27,7 +27,6 @@ struct User {
struct Tag { struct Tag {
std::string japanese; std::string japanese;
std::optional<std::string> romaji;
std::optional<std::string> english; std::optional<std::string> english;
}; };
@ -53,7 +52,19 @@ struct Illusts {
size_t total_pages; 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, User& user);
void from_json(const nlohmann::json& j, Tag& tag); 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, Illust& illust);
void from_json(const nlohmann::json& j, Illusts& illusts); 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 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 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_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 bool is_true(const std::string& str);
static inline std::string time_to_string(time_t time); 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 body("body", {
Element("h2", {illust.title}), Element("h2", {illust.title}),
generate_user_link(req, config, illust), generate_user_link(req, config, illust),
Element("br"),
!preview ? generate_images(req, config, illust) : generate_preview_images(req, config, illust), !preview ? generate_images(req, config, illust) : generate_preview_images(req, config, illust),
Element("br") Element("br")
}); });
if (illust.comment) { if (illust.comment) {
body.nodes.push_back(generate_description(req, config, *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)})); body.nodes.push_back(Element("p", {time_to_string(illust.upload_time)}));
serve(req, res, config, std::move(illust.title), std::move(body)); 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; 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"}}, {}); Element div("div", {{"class", "illusttags"}}, {});
if (illust.ai_generated) { if (illust.ai_generated) {
@ -170,11 +171,12 @@ static inline Element generate_illust_tags(const Illust& illust) {
for (const Tag& i : illust.tags) { for (const Tag& i : illust.tags) {
std::string tag = [&]() { std::string tag = [&]() {
if (i.english) return *i.english; if (i.english) return *i.english;
if (i.romaji) return *i.romaji;
return i.japanese; 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; return div;

View File

@ -37,6 +37,11 @@ void css_route(const httplib::Request& req, httplib::Response& res) {
max-width: 100%; max-width: 100%;
} }
.center {
text-align: center;
display: block;
}
/* USER PAGE (and a tiny bit for illustrations page) */ /* USER PAGE (and a tiny bit for illustrations page) */
.cover { .cover {
width: 100%; width: 100%;
@ -61,10 +66,6 @@ void css_route(const httplib::Request& req, httplib::Response& res) {
} }
/* USER ILLUSTRATIONS PAGE (and illustrations page) */ /* USER ILLUSTRATIONS PAGE (and illustrations page) */
.center {
text-align: center;
display: block;
}
.grid { .grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -94,6 +95,14 @@ void css_route(const httplib::Request& req, httplib::Response& res) {
padding-bottom: 1em; padding-bottom: 1em;
} }
/* SEARCH RESULTS PAGE */
.searchsuggestions {
text-align: left;
display: inline-block;
margin-top: .5em;
margin-bottom: 0;
}
/* ERROR PAGE */ /* ERROR PAGE */
.error { .error {
text-align: center; 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. (", "Pixwhile is an alternative frontend to Pixiv that utilizes no Javascript. (",
Element("a", {{"href", "https://gitlab.com/blankX/pixwhile"}}, {"source code"}), 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("h2", {"Try it out"}),
Element("ul", { Element("ul", {
get_artwork_element(106623268, "アル社長の日常", 1960050, "torino"), 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("ul", {
Element("li", {"Does not contain an asinine amount of Javascript"}), 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", {"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("h2", {"Missing Features"}),
Element("p", {"This list is not exhaustive, nor does it mean that these are being worked on."}), Element("p", {"This list is not exhaustive, nor does it mean that these are being worked on."}),
Element("ul", { 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 login"}),
Element("li", {"No ability to see comments or like counts"}), 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 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 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 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 "../../pixivclient.h"
#include "common.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) { 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 user_id = to_ull(req.matches[1].str());
uint64_t page = req.has_param("p") ? to_ull(req.get_param_value("p")) - 1 : 0; 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", { Element body("body", {
generate_user_header(std::move(user), config), generate_user_header(std::move(user), config),
generate_pager(illusts, page, true), generate_illusts_pager(req, config, illusts, page, "illusts")
Element("br"),
generate_content(req, config, illusts),
generate_pager(illusts, page, false)
}); });
serve(req, res, config, user.display_name + "'s illustrations", std::move(body)); 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 <regex>
#include "config.h" #include "config.h"
#include "pixivmodels.h"
#include "servehelper.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) { void serve(const httplib::Request& req, httplib::Response& res, const Config& config, std::string title, Element element) {
using namespace std::string_literals; 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)); serve(req, res, config, "Redirecting to "s + std::move(url) + "", std::move(body));
} }
std::string get_origin(const httplib::Request& req, const Config& config) { std::string get_origin(const httplib::Request& req, const Config& config) {
if (req.has_header("X-Canonical-Origin")) { if (req.has_header("X-Canonical-Origin")) {
return req.get_header_value("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)); 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" #include "blankie/serializer.h"
struct Config; // forward declaration from config.h struct Config; // forward declaration from config.h
struct Illusts; // forward declaration from pixivmodels.h
using Element = blankie::html::Element; using Element = blankie::html::Element;
void serve(const httplib::Request& req, httplib::Response& res, const Config& config, std::string title, Element 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, 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); 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); 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 get_origin(const httplib::Request& req, const Config& config);
std::string proxy_url(blankie::murl::Url base, blankie::murl::Url url); std::string proxy_url(blankie::murl::Url base, blankie::murl::Url url);
std::string proxy_image_url(const Config& config, 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);