#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "file.h" namespace pt = boost::property_tree; using namespace std::string_literals; namespace fs = std::filesystem; namespace { class TempDir { private: fs::path m_path; fs::path m_oldpath; public: TempDir() { char templ[] = "/tmp/downtubeXXXXXX"; char *temp = mkdtemp(templ); if (temp == nullptr) { throw std::runtime_error("Can't create temporary directory."); } m_path = temp; m_oldpath = fs::current_path(); fs::current_path(m_path); } ~TempDir() { fs::current_path(m_oldpath); fs::remove_all(m_path); } const fs::path& getPath() const {return m_path;} }; std::regex re{"https?://(www\\.youtube\\.com/watch\\?v=|youtu\\.be/)[[:alnum:]_&=-]+", std::regex::extended}; // www.youtube.com/watch?v=ItjMIxS3-rI or https://youtu.be/ItjMIxS3-rI } // anonymous namespace int main(void) { int result = FCGX_Init(); if (result != 0) { // error on init fprintf(stderr, "Error: FCGX_Init()\n"); return 1; } result = FCGX_IsCGI(); if (result) { fprintf(stderr, "Error: No FCGI environment available.\n"); return 1; } FCGX_Request request; result = FCGX_InitRequest(&request, 0, 0); if (result != 0) { fprintf(stderr, "Error: FCGX_InitRequest()\n"); return 1; } while (FCGX_Accept_r(&request) >= 0) { try { char* method = FCGX_GetParam("REQUEST_METHOD", request.envp); // POST for server actions, changes if (!strcmp(method, "POST")) { size_t contentLength { std::stoul(FCGX_GetParam("CONTENT_LENGTH", request.envp)) }; std::string postData(contentLength, '\0'); // contentLength number of bytes, initialize with 0 if (FCGX_GetStr(postData.data(), contentLength, request.in) != contentLength) { throw std::runtime_error("Bad data read: Content length mismatch.\r\n"); } // postData contains POST data std::string contentType(FCGX_GetParam("CONTENT_TYPE", request.envp)); std::string xmlData = postData; // default: interpret whole POST data as xml request pt::ptree xml; std::istringstream ss{xmlData}; pt::xml_parser::read_xml(ss, xml, pt::xml_parser::no_comments | pt::xml_parser::trim_whitespace); std::string url {xml.get("request.url")}; std::string format {xml.get("request.format")}; std::string command {xml.get("request.command")}; //FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out); //FCGX_FPrintF(request.out, "url: %s\r\n", url.c_str()); //FCGX_FPrintF(request.out, "format: %s\r\n", format.c_str()); // mp3, mp4 if (format != "mp3" && format != "mp4") { throw std::runtime_error("Bad format: "s + format); } if (!std::regex_match(url, re)) { throw std::runtime_error("Bad URL"); } // remove trailing "&..." size_t and_pos {url.find('&')}; if (and_pos != std::string::npos) url = url.substr(0, and_pos); //FCGX_FPrintF(request.out, "command: %s\r\n", command.c_str()); TempDir tempDir; //FCGX_FPrintF(request.out, "path: %s\r\n", tempDir.getPath().string().c_str()); if (command == "getfilename") { //std::string cmd{"youtube-dl -o '%(title)s."s + format + "' --get-filename --no-call-home --restrict-filenames "s + url + " > filename.txt"}; // Recoding to MP4 is too slow currently. So keep original format for now std::string cmd{"youtube-dl -o '%(title)s.%(ext)s' --get-filename --no-call-home --restrict-filenames "s + url + " > filename.txt"}; if (system(cmd.c_str())) throw std::runtime_error("Can't guess filename"); std::string filename {File::getFile("filename.txt")}; boost::algorithm::trim(filename); if (format == "mp3") filename = fs::path{filename}.stem().string() + ".mp3"; FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out); FCGX_FPrintF(request.out, "%s", filename.c_str()); } else if (command == "getfile") { if (format == "mp3") { std::string cmd{"youtube-dl --no-warnings --no-call-home --no-progress -x --audio-format mp3 -o 'audio.%(ext)s' --restrict-filenames "s + url}; system(cmd.c_str()); // Ignore error - "ERROR: Stream #1:0 -> #0:1 (copy)" - seems to be ok std::string filedata {File::getFile("audio.mp3")}; // may throw if (filedata.size() > 30000000) throw std::runtime_error("File too big"); FCGX_PutS("Content-Type: application/octet-stream\r\n", request.out); FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", filedata.size()); FCGX_PutStr(filedata.c_str(), filedata.size(), request.out); } else if (format == "mp4") { //std::string cmd{"youtube-dl --no-warnings --no-call-home --no-progress --recode-video mp4 -o video.mp4 --restrict-filenames "s + url}; // Recoding to MP4 is too slow currently. So keep original format for now std::string cmd{"youtube-dl --no-warnings --no-call-home --no-progress -o video.mp4 --restrict-filenames "s + url}; system(cmd.c_str()); // Ignore error // youtube-dl gets it wrong and creates, e.g. video.mkv. // So find it and load it fs::directory_iterator di{fs::current_path()}; fs::path filename; for (const auto& i: di) { if (boost::algorithm::starts_with(i.path().filename().string(), "video."s)) { filename = i.path().filename().string(); break; } } if (filename.empty()) { throw std::runtime_error("No video file found."); } std::string filedata {File::getFile(filename)}; // may throw if (filedata.size() > 300000000) throw std::runtime_error("File too big"); FCGX_PutS("Content-Type: application/octet-stream\r\n", request.out); FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", filedata.size()); FCGX_PutStr(filedata.c_str(), filedata.size(), request.out); } else { throw std::runtime_error("Bad format for unknown reason: "s + format); // should have been caught above already! } } else { throw std::runtime_error("Bad command: "s + command); } // Name: // youtube-dl -o "%(title)s.mp3" --get-filename --no-call-home --restrict-filenames $SOURCE > filename.txt // // MP3: // youtube-dl --no-warnings --no-call-home --no-progress -x --audio-format mp3 --embed-thumbnail -o "%(title)s.(ext)s" --restrict-filenames $SOURCE // // MP4: // youtube-dl --no-warnings --no-call-home --no-progress --recode-video mp4 -o "%(title)s.(ext)s" --restrict-filenames $SOURCE } else { throw std::runtime_error("Unsupported method.\r\n"); } } catch (const std::runtime_error& ex) { FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out); FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out); FCGX_FPrintF(request.out, "Error: %s\r\n", ex.what()); } catch (const std::exception& ex) { FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out); FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out); FCGX_FPrintF(request.out, "Unknown exception: %s\r\n", ex.what()); } } return 0; }