// // Created by s-Kaonnull on 25.05.2021. // #include "api.hpp" #include "tables.hpp" #include #include #include #include #include #include #include #include #include #include using json = nlohmann::json; Api::Api(soci::session& sql, httplib::Server& server) : sql(sql) { RegisterServerHandles(server); } void Api::RegisterServerHandles(httplib::Server& server) { spdlog::info("Register api endpoints..."); server.Post("/api/add", std::bind_front(&Api::add, this)); server.Get("/api/files", std::bind_front(&Api::files, this)); server.Get("/api/file/([A-F0-9]+)", std::bind_front(&Api::file, this)); server.Get("/api/status/([A-F0-9]+)", std::bind_front(&Api::status, this)); server.Options("/(.*)", [this](httplib::Request const& rq, httplib::Response& rp) {}); server.set_pre_routing_handler([this](httplib::Request const& rq, httplib::Response& rp) { this->set_cross_headers(rp); spdlog::info("Request {} {}", rq.method, rq.path); return httplib::Server::HandlerResponse::Unhandled; }); } /* /api/add * * body: json * - url * - audio_only - default false * - format - optional default source * * return: json * - status: bool * - file_id: hash * - error: string * */ void Api::add(const httplib::Request& rq, httplib::Response& rs) { try { auto data = json::parse(rq.body); // Parse Request Task task; task.url = data["url"]; task.audio_only = data.contains("audio_only") && data["audio_only"].get(); if (data.contains("format")) task.format = data["format"].get(); task.timestamp = std::time(nullptr); task.hash = fmt::format( "{:X}", std::hash()( task.url + fmt::format("{:%Y-%m-%d %H:%M:%S}", *std::localtime(&task.timestamp)))); task.status = FileStatus::PENDING; // Insert Sql sql << "INSERT INTO Tasks (hash, timestamp, url, audio_only, status, format) " "values(:hash, :timestamp, :url, :audio_only, :status, :format) ", soci::use(task); /*long long int tmp; sql.get_last_insert_id("Files", tmp); f.id = tmp;*/ json jr; jr["status"] = "Ok"; jr["file_id"] = task.hash; spdlog::info("New Task: {}", task); rs.set_content(jr.dump(), "application/json"); } catch (json::exception const& e) { spdlog::error("Api Error: {}", e.what()); json jr; jr["status"] = "Err"; jr["error"] = e.what(); rs.status = 400; rs.set_content(jr.dump(), "application/json"); } } void Api::files(httplib::Request const& rq, httplib::Response& rs) { try { json res; // Query incomplete Tasks soci::rowset data = (sql.prepare << "SELECT * " "FROM Tasks " "WHERE status!=2 " "ORDER BY id DESC "); for (auto const& el : data) { json current; current["hash"] = el.hash; current["status"] = magic_enum::enum_name(el.status); current["status_id"] = static_cast(el.status); current["source"] = el.url; current["name"] = fmt::format("File {}", el.id); current["audio_only"] = el.audio_only; current["format"] = el.format.value_or("Source"); res["queue"].push_back(current); } // Downloaded soci::rowset files = (sql.prepare << R"(SELECT Tasks.id, Files.timestamp, Files.hash, audio_only, filename, Files.format FROM Files, Tasks WHERE Tasks.hash == Files.hash ORDER BY Files.timestamp DESC )"); for (auto const& el : files) { json current; std::time_t time = el.get("timestamp"); current["timestamp"] = fmt::format("{:%Y-%m-%d %H:%M:%S}", *std::localtime(&time)); current["hash"] = el.get("hash"); current["audio_only"] = el.get("audio_only"); current["filename"] = el.get("filename"); current["format"] = el.get("format"); res["files"].push_back(current); } rs.set_content(res.dump(), "application/json"); } catch (std::exception const& e) { spdlog::error(e.what()); throw e; } } void Api::file(const httplib::Request& rq, httplib::Response& rs) { if (!rq.matches[1].matched) { rs.status = 404; rs.set_content("Parameter error", "text/plain"); return; } auto file = rq.matches[1]; spdlog::info("File Request: {}", file); try { File dw; sql << fmt::format("SELECT * FROM Files WHERE hash='{}';", file), soci::into(dw); auto fs = std::make_shared(dw.local_path, std::ios_base::binary | std::ios::in); fs->seekg(0, std::ios_base::end); auto size = fs->tellg(); fs->seekg(0); // rs.body.resize(static_cast(size)); // fs.read(&rs.body[0], static_cast(size)); rs.set_header("Content-Disposition", fmt::format("attachment; filename={};", dw.filename)); rs.set_content_provider( static_cast(size), fmt::format("application/{}", dw.format).c_str(), [file = fs](size_t offset, size_t length, httplib::DataSink& sink) mutable { size_t size = std::min(length, (size_t)1024 * 500); //spdlog::info("Stream offset {} length {}", offset, length); std::vector data; data.resize(size); file->seekg(offset); file->read(data.data(), data.size()); sink.write(data.data(), data.size()); return true; }); } catch (std::exception const& e) { spdlog::error(e.what()); rs.status = 404; rs.set_content("File Not Found", "text/plain"); } } void Api::status(httplib::Request const& rq, httplib::Response& rs) { if (!rq.matches[1].matched) { rs.status = 404; rs.set_content("Parameter error", "text/plain"); return; } std::string hash = rq.matches[1]; boost::optional data; sql << "SELECT * FROM Tasks WHERE hash=:hash", soci::use(hash, "hash"), soci::into(data); /*soci::rowset data = (sql.prepare << "SELECT * " "FROM Tasks " "WHERE hash=:hash ", soci::use(hash, "hash"));*/ json response; auto& status = response["status"]; if (data) { auto const& el = data.value(); status["name"] = fmt::format("Task {}", el.hash); status["source"] = el.url; status["status"] = magic_enum::enum_name(el.status); status["status_id"] = static_cast(el.status); status["timestamp"] = el.timestamp; status["task_file_type"] = fmt::format("{}: {}", el.audio_only ? "Audio" : "Video", el.format.value_or("Auto")); status["id"] = el.hash; if (el.status == FileStatus::COMPLETE) { boost::optional file_data; sql << "SELECT * FROM Files WHERE hash=:hash", soci::use(hash, "hash"), soci::into(file_data); if (file_data) { auto const& file = file_data.value(); auto& file_status = status["file"]; file_status["timestamp"] = file.timestamp; file_status["name"] = file.filename; file_status["format"] = fmt::format("{}: {}", el.audio_only ? "Audio" : "Video", file.format); } } } else { rs.status = 404; response["error"] = fmt::format("File '{}' not found!", hash); } rs.set_content(response.dump(), "application/json"); } void Api::set_cross_headers(httplib::Response& rs) { rs.set_header("Access-Control-Allow-Origin", "*"); rs.set_header("Access-Control-Allow-Methods", "GET, POST, PUT"); rs.set_header("Access-Control-Allow-Headers", "Content-Type"); }