299 lines
8.5 KiB
C++
299 lines
8.5 KiB
C++
//
|
|
// Created by s-Kaonnull on 25.05.2021.
|
|
//
|
|
#include "api.hpp"
|
|
#include "tables.hpp"
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
|
|
#include <soci/boost-optional.h>
|
|
|
|
#include <ctre.hpp>
|
|
#include <fmt/chrono.h>
|
|
#include <fmt/ostream.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <openssl/md5.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
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<bool>();
|
|
|
|
if (data.contains("format"))
|
|
task.format = data["format"].get<std::string>();
|
|
|
|
task.timestamp = std::time(nullptr);
|
|
|
|
task.hash = fmt::format(
|
|
"{:X}",
|
|
std::hash<std::string>()(
|
|
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<Task> 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<int>(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<soci::row> 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<int>("timestamp");
|
|
|
|
current["timestamp"] = fmt::format("{:%Y-%m-%d %H:%M:%S}", *std::localtime(&time));
|
|
current["hash"] = el.get<std::string>("hash");
|
|
current["audio_only"] = el.get<int>("audio_only");
|
|
current["filename"] = el.get<std::string>("filename");
|
|
current["format"] = el.get<std::string>("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<std::ifstream>(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_t>(size));
|
|
// fs.read(&rs.body[0], static_cast<std::streamsize>(size));
|
|
|
|
rs.set_header("Content-Disposition", fmt::format("attachment; filename={};", dw.filename));
|
|
|
|
rs.set_content_provider(
|
|
static_cast<size_t>(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<char> 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<Task> data;
|
|
|
|
sql << "SELECT * FROM Tasks WHERE hash=:hash", soci::use(hash, "hash"), soci::into(data);
|
|
|
|
/*soci::rowset<Task> 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<int>(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> 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");
|
|
}
|