#include #include "routes.h" #include "../blankie/murl.h" #include "../blankie/escape.h" #include "../blankie/unescape.h" #include "../servehelper.h" #include "../numberhelper.h" #include "../pixivclient.h" static inline Element generate_user_link(const httplib::Request& req, const Config& config, const Illust& illust); static inline Element generate_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 Element generate_illust_tags(const httplib::Request& req, const Config& config, const Illust& illust); static inline blankie::html::HTMLString fix_description_links(const httplib::Request& req, const Config& config, blankie::html::HTMLString str); static inline std::string generate_description_text(const httplib::Request& req, const Config& config, blankie::html::HTMLString description); static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Config& config, const Illust& illust, bool preview); static inline bool is_true(const std::string& str); static inline std::string time_to_string(time_t time); static inline std::string proxy_pixiv_url(const httplib::Request& req, const Config& config, blankie::murl::Url url); void artworks_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client) { uint64_t illust_id = to_ull(req.matches.str(1)); bool preview = is_true(req.get_param_value("preview")); Illust illust; try { illust = pixiv_client.get_illust(illust_id); } catch (const PixivException& e) { if (e.status == 404) { res.status = 404; serve_error(req, res, config, "404: Illust not found", e.what()); } else { res.status = 500; serve_error(req, res, config, "500: Internal server error", "Failed to fetch illust information", e.what()); } return; } catch (const std::exception& e) { res.status = 500; serve_error(req, res, config, "500: Internal server error", "Failed to fetch illust information", e.what()); return; } 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_html) { body.nodes.push_back(Element("p", {fix_description_links(req, config, blankie::html::HTMLString(*illust.comment_html))})); } 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, illust.title, std::move(body), generate_ogp_nodes(req, config, illust, preview)); } static inline Element generate_user_link(const httplib::Request& req, const Config& config, const Illust& illust) { std::string profile_picture = proxy_image_url(req, config, illust.user_profile_pictures.thumbnail_or_original().url); std::string user_link = get_origin(req, config) + "/users/" + std::to_string(illust.user_id); return Element("a", {{"class", "user_metadata"}, {"href", std::move(user_link)}}, { Element("img", {{"class", "user_profile_picture small"}, {"loading", "lazy"}, {"src", std::move(profile_picture)}}, {}), Element("b", {illust.user_display_name}) }); } static inline Element generate_images(const httplib::Request& req, const Config& config, const Illust& illust) { using namespace std::string_literals; Element div("div", {{"class", "illust-images"}}, {}); bool show_pages = illust.images.size() > 1; if (show_pages) { div.nodes.push_back(Element("a", {{"href", "?preview=1"}}, {"Preview Images"})); } div.nodes.reserve(div.nodes.size() + (show_pages ? illust.images.size() * 2 : illust.images.size())); for (size_t i = 0; i < illust.images.size(); i++) { const Images& images = illust.images[i]; const Image& thumbnail = images.thumbnail_or_original(); const Image& original = images.original_or_thumbnail(); std::string id = std::to_string(i + 1); if (show_pages) { div.nodes.push_back(Element("a", {{"class", "landmark"}, {"href", "#"s + id}}, {id, "/", std::to_string(illust.images.size())})); } Element img("img", {{"loading", "lazy"}, {"id", std::move(id)}, {"src", proxy_image_url(req, config, thumbnail.url)}}, {}); if (thumbnail.size) { img.attributes.push_back({"width", std::to_string(thumbnail.size->first)}); img.attributes.push_back({"height", std::to_string(thumbnail.size->second)}); } div.nodes.push_back(Element("a", {{"href", proxy_image_url(req, config, original.url)}}, { std::move(img) })); } return div; } static inline Element generate_preview_images(const httplib::Request& req, const Config& config, const Illust& illust) { std::string no_preview_link = get_origin(req, config) + "/artworks/" + std::to_string(illust.illust_id); Element div("div", { Element("a", {{"class", "center"}, {"href", no_preview_link}}, {"Go back"}), Element("br") }); Element grid("div", {{"class", "grid"}}, {}); grid.nodes.reserve(illust.images.size()); for (size_t i = 0; i < illust.images.size(); i++) { const Images& images = illust.images[i]; std::string thumbnail = proxy_image_url(req, config, images.thumbnail_or_original(1).url); std::string link = no_preview_link + '#' + std::to_string(i + 1); grid.nodes.push_back(Element("a", {{"href", std::move(link)}}, { Element("img", {{"loading", "lazy"}, {"src", std::move(thumbnail)}}, {}) })); } div.nodes.push_back(std::move(grid)); return div; } static inline Element generate_illust_tags(const httplib::Request& req, const Config& config, const Illust& illust) { Element div("div", {{"class", "illust-tags"}}, {}); if (illust.ai_generated) { div.nodes.push_back(Element("b", {"AI-Generated "})); } div.nodes.reserve(div.nodes.size() + illust.tags.size()); for (const Tag& i : illust.tags) { std::string tag = [&]() { if (i.english) return *i.english; return i.japanese; }(); div.nodes.push_back(Element("a", {{"href", get_origin(req, config) + "/tags/" + blankie::murl::escape(i.japanese)}}, { "#", std::move(tag) })); } return div; } const std::regex start_link_regex(""); static inline blankie::html::HTMLString fix_description_links(const httplib::Request& req, const Config& config, blankie::html::HTMLString str) { using namespace std::string_literals; blankie::html::HTMLString out; std::smatch sm; out.str.reserve(str.str.size()); while (std::regex_search(str.str, sm, start_link_regex)) { out.str += sm.prefix(); std::string url_str; blankie::murl::Url url(sm.str(1)); if (url.is_host_equal("pixiv.net") || url.is_host_equal("www.pixiv.net")) { url_str = proxy_pixiv_url(req, config, std::move(url)); } else if (url.path == "/jump.php") { url_str = blankie::murl::unescape(std::move(url.query)); } else { url_str = url.to_string(); } out.str += ""; str.str = sm.suffix(); } out.str += std::move(str.str); return out; } const std::regex link_regex(".+?"); const std::regex tag_regex("<(.+?)>"); static inline std::string generate_description_text(const httplib::Request& req, const Config& config, blankie::html::HTMLString description) { description = fix_description_links(req, config, std::move(description)); std::string new_description; std::smatch sm; new_description.reserve(description.str.size()); while (std::regex_search(description.str, sm, link_regex)) { new_description += sm.prefix(); new_description += sm.str(1); description.str = sm.suffix(); } new_description += std::move(description.str); description.str = std::move(new_description); new_description.reserve(description.str.size()); while (std::regex_search(description.str, sm, tag_regex)) { new_description += sm.prefix(); if (sm.str(1) == "br /") { new_description += '\n'; } description.str = sm.suffix(); } new_description += std::move(description.str); return blankie::html::unescape(std::move(new_description)); } static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Config& config, const Illust& illust, bool preview) { std::string url = get_origin(req, config) + "/artworks/" + std::to_string(illust.illust_id); if (preview) { url += "?preview=1"; } Nodes nodes({ Element("meta", {{"property", "og:title"}, {"content", illust.title}}, {}), Element("meta", {{"property", "og:type"}, {"content", "photo"}}, {}), Element("meta", {{"property", "og:site_name"}, {"content", "Pixwhile"}}, {}), Element("meta", {{"property", "og:url"}, {"content", std::move(url)}}, {}) }); if (illust.comment_html) { nodes.push_back(Element("meta", {{"property", "og:description"}, {"content", generate_description_text(req, config, blankie::html::HTMLString(*illust.comment_html))}}, {})); } // i don't even know what multiple og:images do anymore // https://stackoverflow.com/questions/13424780/facebook-multiple-ogimage-tags-which-is-default nodes.reserve(nodes.size() + illust.images.size() * 3); for (const Images& images : illust.images) { const Image& image = images.thumbnail_or_original(); nodes.push_back(Element("meta", {{"property", "og:image"}, {"content", proxy_image_url(req, config, image.url)}}, {})); if (image.size) { nodes.push_back(Element("meta", {{"property", "og:image:width"}, {"content", std::to_string(image.size->first)}}, {})); nodes.push_back(Element("meta", {{"property", "og:image:height"}, {"content", std::to_string(image.size->second)}}, {})); } } return nodes; } static inline bool is_true(const std::string& str) { return !str.empty() && str != "0" && str != "false" && str != "no"; } static inline std::string time_to_string(time_t time) { struct tm tm; char str[1024] = {0}; if (!gmtime_r(&time, &tm)) { return std::to_string(time); } size_t len = strftime(str, 1024, "%F %H:%M:%S %z", &tm); if (len == 0) { return std::to_string(time); } return std::string(str, len); } static inline std::string proxy_pixiv_url(const httplib::Request& req, const Config& config, blankie::murl::Url url) { if (url.path.size() >= 4 && url.path.starts_with("/en/")) { url.path.erase(0, 3); } else if (url.path.size() == 3 && url.path == "/en") { url.path = '/'; } // TODO strip the lang query parameter return proxy_url(get_origin(req, config), std::move(url)); }