Compare commits
10 Commits
7ecbb7f8bc
...
2ecfe10584
Author | SHA1 | Date |
---|---|---|
blankie | 2ecfe10584 | |
blankie | 4586931adf | |
blankie | 71994b234b | |
blankie | fd946d1336 | |
blankie | 81dd9c9973 | |
blankie | b71cbd039d | |
blankie | a448dd9b6e | |
blankie | 36a750762b | |
blankie | c4407d86e5 | |
blankie | 14a08e4937 |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
15
main.cpp
15
main.cpp
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"};
|
||||
|
|
110
pixivmodels.cpp
110
pixivmodels.cpp
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"}),
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue