#include #include "routes.h" #include "../blankie/murl.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 Nodes 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 httplib::Request& req, const Config& config, const Illust& illust); static inline std::string generate_description_text(const httplib::Request& req, const Config& config, std::string description); static inline Nodes generate_ogp_nodes(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); 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) { body.nodes.push_back(generate_description(req, config, *illust.comment)); } 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)); } static inline Element generate_user_link(const httplib::Request& req, const Config& config, const Illust& illust) { std::string profile_picture = proxy_image_url(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(); if (show_pages) { std::string id = std::to_string(i + 1); div.nodes.push_back(Element("a", {{"class", "landmark"}, {"id", id}, {"href", "#"s + id}}, {id, "/", std::to_string(illust.images.size())})); } Element img("img", {{"loading", "lazy"}, {"src", proxy_image_url(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(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(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 Nodes parse_description_line(const httplib::Request& req, const Config& config, std::string str) { Nodes nodes; std::smatch sm; while (std::regex_search(str, sm, blankie::murl::full_url_regex)) { std::string prefix = sm.prefix(); std::string url_str = sm.str(0); std::string suffix = sm.suffix(); if (prefix.ends_with('(') && url_str.ends_with(')')) { url_str.pop_back(); suffix.insert(0, 1, ')'); } if (!prefix.empty()) { nodes.push_back(std::move(prefix)); } blankie::murl::Url url(std::move(url_str)); url_str = url.is_host_equal("pixiv.net") || url.is_host_equal("www.pixiv.net") ? proxy_pixiv_url(req, config, std::move(url)) : url.to_string(); nodes.push_back(Element("a", {{"href", url_str}}, {url_str})); str = std::move(suffix); } if (!str.empty()) { nodes.push_back(std::move(str)); } return nodes; } static inline Element generate_description(const httplib::Request& req, const Config& config, const std::string& description) { Element p("p"); size_t pos = 0; size_t last_pos = 0; auto add = [&](std::string str) { if (!p.nodes.empty()) { p.nodes.push_back(Element("br")); } Nodes nodes = parse_description_line(req, config, std::move(str)); p.nodes.insert(p.nodes.end(), nodes.begin(), nodes.end()); }; while ((pos = description.find('\n', pos)) != std::string::npos) { add(description.substr(last_pos, pos - last_pos)); last_pos = ++pos; } if (description.size() > last_pos) { add(description.substr(last_pos)); } return p; } 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; } static inline std::string generate_description_text(const httplib::Request& req, const Config& config, std::string description) { std::string new_description; std::smatch sm; new_description.reserve(description.size()); while (std::regex_search(description, sm, blankie::murl::full_url_regex)) { std::string prefix = sm.prefix(); std::string url_str = sm.str(0); std::string suffix = sm.suffix(); if (prefix.ends_with('(') && url_str.ends_with(')')) { url_str.pop_back(); suffix.insert(0, 1, ')'); } new_description += std::move(prefix); blankie::murl::Url url(std::move(url_str)); url_str = url.is_host_equal("pixiv.net") || url.is_host_equal("www.pixiv.net") ? proxy_pixiv_url(req, config, std::move(url)) : url.to_string(); new_description += std::move(url_str); description = std::move(suffix); } new_description += std::move(description); return new_description; } static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Config& config, const Illust& illust) { 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", get_origin(req, config) + "/artworks/" + std::to_string(illust.illust_id)}}, {}) }); if (illust.comment) { nodes.push_back(Element("meta", {{"property", "og:description"}, {"content", generate_description_text(req, config, *illust.comment)}}, {})); } // 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(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)); }