From 231f06c2e3d96840d10ef01dac3541947cd67ee8 Mon Sep 17 00:00:00 2001 From: Simon Hardt Date: Fri, 4 Jun 2021 00:50:07 +0200 Subject: [PATCH] Add: universal task display --- Modules/Server/CMakeLists.txt | 4 +- Modules/Server/src/api.cpp | 111 ++++----------------- Modules/Server/src/file_status.cpp | 76 +++++++++++++++ Modules/Server/src/file_status.hpp | 46 +++++++++ Modules/Server/src/main.cpp | 6 +- Modules/Website/src/Files.elm | 130 ++++++------------------- Modules/Website/src/MainPage.elm | 2 +- Modules/Website/src/Status.elm | 150 +++-------------------------- Modules/Website/src/TaskStatus.elm | 132 +++++++++++++++++++++++++ 9 files changed, 326 insertions(+), 331 deletions(-) create mode 100644 Modules/Server/src/file_status.cpp create mode 100644 Modules/Server/src/file_status.hpp create mode 100644 Modules/Website/src/TaskStatus.elm diff --git a/Modules/Server/CMakeLists.txt b/Modules/Server/CMakeLists.txt index 05c0852..a60ebfe 100644 --- a/Modules/Server/CMakeLists.txt +++ b/Modules/Server/CMakeLists.txt @@ -5,7 +5,9 @@ set(src_files src/api.cpp src/worker.hpp src/worker.cpp - src/tables.hpp) + src/tables.hpp + src/file_status.hpp + src/file_status.cpp) add_executable(localTubeServer ${src_files}) diff --git a/Modules/Server/src/api.cpp b/Modules/Server/src/api.cpp index 2844294..d642833 100644 --- a/Modules/Server/src/api.cpp +++ b/Modules/Server/src/api.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -16,6 +17,8 @@ #include #include +#include "file_status.hpp" + using json = nlohmann::json; Api::Api(soci::session& sql, httplib::Server& server) : sql(sql) @@ -116,53 +119,10 @@ void Api::files(httplib::Request const& rq, httplib::Response& rs) { try { + auto data = TaskInfo::getAll(sql); + 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); - } + res["files"] = data; rs.set_content(res.dump(), "application/json"); } @@ -191,14 +151,12 @@ void Api::file(const httplib::Request& rq, httplib::Response& rs) 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); + 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( @@ -207,7 +165,6 @@ void Api::file(const httplib::Request& rq, httplib::Response& rs) [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); @@ -237,54 +194,26 @@ void Api::status(httplib::Request const& rq, httplib::Response& rs) 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) + try { - auto const& el = data.value(); + auto data = TaskInfo::getId(sql, hash); - 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) + if (data) { - boost::optional file_data; - sql << "SELECT * FROM Files WHERE hash=:hash", soci::use(hash, "hash"), - soci::into(file_data); + response["status"] = data.value(); - 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); } - - } else + } + catch (std::exception const& e) { + spdlog::error("[Api Status] Excepion: {}", e.what()); rs.status = 404; - response["error"] = fmt::format("File '{}' not found!", hash); + response["error"] = fmt::format("Exception: {}", e.what()); } rs.set_content(response.dump(), "application/json"); } diff --git a/Modules/Server/src/file_status.cpp b/Modules/Server/src/file_status.cpp new file mode 100644 index 0000000..d9335ae --- /dev/null +++ b/Modules/Server/src/file_status.cpp @@ -0,0 +1,76 @@ +#include "file_status.hpp" + +#include + +#include + +#include "tables.hpp" + +std::optional TaskInfo::getId(soci::session& sql, std::string_view hash) +{ + auto hash_str = std::string { hash }; + + soci::rowset row_data = (sql.prepare << R"(-- +SELECT Tasks.hash, + Tasks.status, + Tasks.timestamp AS TaskTimestamp, + Tasks.url, + Files.timestamp AS FileTimestamp, + Files.filename, + Files.format +FROM Tasks LEFT OUTER JOIN Files ON Files.id = Tasks.id +WHERE Tasks.hash = :file_hash ;)", + soci::use(hash_str, "file_hash")); + + for (auto const& el : row_data) + { + return fromRow(el); + } + return {}; +} + +std::vector TaskInfo::getAll(soci::session& sql) +{ + soci::rowset row_data(sql.prepare << R"(-- +SELECT Tasks.hash, + Tasks.status, + Tasks.timestamp AS TaskTimestamp, + Tasks.url, + Files.timestamp AS FileTimestamp, + Files.filename, + Files.format +FROM Tasks LEFT JOIN Files ON Files.id = Tasks.id +ORDER BY TaskTimestamp DESC;")"); + + std::vector data; + + for (auto const& el : row_data) + { + data.push_back(fromRow(el)); + } + + return data; +} + +TaskInfo TaskInfo::fromRow(soci::row const& row) +{ + TaskInfo status; + + + + auto file_status = row.get("status"); + + status.hash = row.get("hash"); + status.name = fmt::format("Task {}", status.hash); + status.status = magic_enum::enum_name(file_status); + status.timestamp = row.get("TaskTimestamp"); + status.url = row.get("url"); + + if (file_status == FileStatus::COMPLETE) + { + status.file = { .timestamp = row.get("FileTimestamp"), + .filename = row.get("filename"), + .format = row.get("format") }; + } + return status; +} \ No newline at end of file diff --git a/Modules/Server/src/file_status.hpp b/Modules/Server/src/file_status.hpp new file mode 100644 index 0000000..33e4548 --- /dev/null +++ b/Modules/Server/src/file_status.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +#include +#include + +using json = nlohmann::json; + +struct FileInfo +{ + int64_t timestamp; + std::string filename; + std::string format; +}; + +struct TaskInfo +{ + std::string name; + std::string hash; + std::string status; + std::string url; + int64_t timestamp; + std::optional file; + + static std::optional getId(soci::session& sql, std::string_view hash); + static std::vector getAll(soci::session& sql); + + static TaskInfo fromRow(soci::row const& row); +}; + +inline void to_json(json& j, const TaskInfo& p) +{ + j = json { { "name", p.name }, + { "source", p.url }, + { "status", p.status }, + { "timestamp", p.timestamp }, + { "hash", p.hash } }; + + if(p.file) + { + j["file"]["timestamp"] = p.file.value().timestamp; + j["file"]["filename"] = p.file.value().filename; + j["file"]["format"] = p.file.value().format; + } +} \ No newline at end of file diff --git a/Modules/Server/src/main.cpp b/Modules/Server/src/main.cpp index b45fb4c..06244ce 100644 --- a/Modules/Server/src/main.cpp +++ b/Modules/Server/src/main.cpp @@ -65,7 +65,7 @@ int main() File::create_table(sql); } - Worker worker(sql); + //Worker worker(sql); httplib::Server srv; srv.set_mount_point("/", "www/"); @@ -75,5 +75,7 @@ int main() Api api(sql, srv); spdlog::info("Listing on 0.0.0.0 Port 80"); - srv.listen("0.0.0.0", 80); + + + srv.listen("0.0.0.0", 8888); } diff --git a/Modules/Website/src/Files.elm b/Modules/Website/src/Files.elm index f1c6671..094ac2c 100644 --- a/Modules/Website/src/Files.elm +++ b/Modules/Website/src/Files.elm @@ -6,41 +6,27 @@ import Element.Font as Font import Element.Region as Region import Globals exposing (apiFiles) import Http -import Json.Decode exposing (Decoder, int, list, string, succeed) +import Json.Decode exposing (Decoder, list, succeed) import Json.Decode.Pipeline exposing (required) +import TaskStatus -type alias File = - { name : String - , file_name : String - , source : String - , id : Int - , status : String - } - - -type alias FilesResponse = - { queue : List File } -- Model -- -type alias Model = - { files : List File - , filter : String - , error : Maybe String - } +type Model + = RequestError String + | Loading + | Files (List TaskStatus.Status) + initModel : Model -initModel = - { files = - [] - , filter = "" - , error = Nothing - } +initModel = Loading + init : ( Model, Cmd Msg ) @@ -55,26 +41,18 @@ init = type Msg = Reload | QueryRequestResult (Result Http.Error FilesResponse) + | TaskStatusMsg TaskStatus.Msg -- Request -- - - -fileDecoder : Decoder File -fileDecoder = - succeed File - |> required "name" string - |> required "file_name" string - |> required "source" string - |> required "id" int - |> required "status" string - +type alias FilesResponse = + { files : List TaskStatus.Status } queueDecoder : Decoder FilesResponse queueDecoder = succeed FilesResponse - |> required "queue" (list fileDecoder) + |> required "files" (list TaskStatus.statusDecoder) @@ -85,10 +63,13 @@ update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of QueryRequestResult (Ok res) -> - ( { model | files = res.queue }, Cmd.none ) + ( Files res.files, Cmd.none ) - QueryRequestResult (Err (Http.BadBody er)) -> - ( { model | error = Just er }, Cmd.none ) + QueryRequestResult (Err (Http.BadBody err)) -> + ( RequestError err, Cmd.none ) + + TaskStatusMsg sub_msg -> + ( model, Cmd.map TaskStatusMsg <| TaskStatus.update sub_msg) _ -> ( model, Cmd.none ) @@ -126,62 +107,6 @@ cardShadowOver = } -viewFile : File -> Element Msg -viewFile file = - el - [ width fill - - -- , Background.color (rgb 0.8 0.8 0.8) - , padding 10 - , Border.rounded 3 - , cardShadow - , height (px 110) - , mouseOver [ cardShadowOver ] - ] - (Element.column - [ width fill - , spacing 6 - ] - [ Element.row - [ width fill ] - [ el - [ Region.heading 1 - , alignLeft - , Font.size 24 - ] - (text file.name) - , el - [ Region.heading 1 - , alignRight - , Font.color (rgba 0 0 0 0.6) - , Font.size 20 - ] - (text (String.concat [ "[", file.status, "]" ])) - ] - , el - [ Region.heading 2 - , Font.size 16 - , spacingXY 0 10 - ] - (text (String.concat [ "File name: ", file.file_name ])) - , el - [ Region.heading 2 - , Font.size 16 - ] - (text (String.concat [ "Source: ", file.source ])) - , download - [ Region.footer - , Font.color (rgb 0 0 0.8) - , Font.size 16 - , alignBottom - , alignRight - ] - { url = String.concat [ "http://127.0.0.1/api/file/", String.fromInt file.id ] - , label = text "Download" - } - ] - ) - view : Model -> Element Msg view model = @@ -199,19 +124,26 @@ view model = , Font.size 36 ] (text "Files") - , case model.error of - Just e -> + , case model of + Loading -> el [ Font.color (rgb 1 0 0) , Font.size 16 ] - (text e) + (text "Loading") - Nothing -> + RequestError err -> + el + [ Font.color (rgb 1 0 0) + , Font.size 16 + ] + (text <| String.concat ["Loading", err]) + + Files files -> Element.column [ width fill , centerX , spacing 20 ] - (List.map viewFile model.files) + ( files |> List.map TaskStatus.statusView |> List.map (Element.map TaskStatusMsg) ) ] diff --git a/Modules/Website/src/MainPage.elm b/Modules/Website/src/MainPage.elm index 99b31c7..2072925 100644 --- a/Modules/Website/src/MainPage.elm +++ b/Modules/Website/src/MainPage.elm @@ -44,7 +44,7 @@ type Msg | UpdateFormFormatChnaged Format | PostForm Form | PostResult (Result Http.Error PostFormResponse) - | None + type AudioFormats diff --git a/Modules/Website/src/Status.elm b/Modules/Website/src/Status.elm index 80ad3e5..d81093d 100644 --- a/Modules/Website/src/Status.elm +++ b/Modules/Website/src/Status.elm @@ -1,22 +1,16 @@ module Status exposing (Model, Msg, init, queryStatus, update, view) -import Color exposing (color) -import Debug exposing (toString) import Delay import Element exposing (..) -import Element.Background as Background -import Element.Border as Border -import Element.Font as Font -import Element.Input as Input -import File.Download exposing (url) import Files exposing (Msg(..)) -import Globals exposing (apiDownloadFile, apiStatus) +import Globals exposing (apiStatus) import Http -import Json.Decode exposing (Decoder, int, list, string, succeed) -import Json.Decode.Pipeline exposing (optional, required) +import Json.Decode exposing (Decoder, succeed) +import Json.Decode.Pipeline exposing (required) import Route exposing (Route(..)) -import String exposing (right) import Task +import TaskStatus + @@ -26,7 +20,7 @@ import Task type Msg = Reload | StatusRequestResult (Result Http.Error StatusResponse) - | Download String + | TaskStatusMsg TaskStatus.Msg @@ -35,7 +29,7 @@ type Msg type alias Model = { file_id : String - , status : Maybe Status + , status : Maybe TaskStatus.Status } @@ -47,55 +41,16 @@ init file_id = -- Request - -type alias File = - { format : String - , filename : String - , timestamp : Int - } - - -type alias Status = - { file : Maybe File - , name : String - , source : String - , status : String - , status_id : Int - , format : String - , timestamp : Int - , id : String - } - - type alias StatusResponse = - { status : Status } + { status : TaskStatus.Status } -fileDecoder : Decoder File -fileDecoder = - succeed File - |> required "format" string - |> required "name" string - |> required "timestamp" int - - -statusDecoder : Decoder Status -statusDecoder = - succeed Status - |> optional "file" (Json.Decode.map Just fileDecoder) Nothing - |> required "name" string - |> required "source" string - |> required "status" string - |> required "status_id" int - |> required "task_file_type" string - |> required "timestamp" int - |> required "id" string statusResponseDecoder : Decoder StatusResponse statusResponseDecoder = succeed StatusResponse - |> required "status" statusDecoder + |> required "status" TaskStatus.statusDecoder queryStatus : String -> Cmd Msg @@ -110,48 +65,6 @@ queryStatus id = -- Update -cardShadow : Attr decorative msg -cardShadow = - Border.shadow - { offset = ( 0, 4 ) - , size = 4 - , blur = 8 - , color = rgba 0 0 0 0.2 - } - - -cardShadowOver : Attr decorative msg -cardShadowOver = - Border.shadow - { offset = ( 0, 4 ) - , size = 8 - , blur = 16 - , color = rgba 0 0 0 0.2 - } - - -card : List (Attribute msg) -> Element msg -> Element msg -card attr element = - el - (List.concat - [ attr - , [ Border.rounded 15, cardShadow, mouseOver [ cardShadowOver ], padding 20 ] - ] - ) - element - - -innerCard : List (Attribute msg) -> Element msg -> Element msg -innerCard attr element = - el - (List.concat - [ attr - , [ Border.roundEach { topLeft = 15, topRight = 15, bottomLeft = 15, bottomRight = 15 }, padding 5 ] - ] - ) - element - - send : msg -> Cmd msg send msg = Task.succeed msg @@ -170,48 +83,11 @@ update msg model = StatusRequestResult _ -> ( { model | status = Nothing }, Cmd.none ) - Download file_id -> - ( model, url <| apiDownloadFile file_id ) + TaskStatusMsg sub_msg -> + ( model, Cmd.map TaskStatusMsg <| TaskStatus.update sub_msg ) --- View ---Just (Download. - - -statusView : Status -> Element Msg -statusView status = - card [ width fill, height shrink ] - (Element.column - [ width fill ] - [ row [ width fill, height <| px 40 ] - [ el [ Font.size 28, alignLeft ] <| text status.name - , el [ Font.size 28, alignRight ] <| text (String.concat [ "[", status.status, "]" ]) - ] - , link [ Font.bold, Font.underline, Font.color color.blue, Font.size 16, padding 2 ] - { url = status.source, label = text status.source } - , el [ height <| px 10 ] none - , case status.file of - Just file -> - innerCard [ width fill, height shrink, Background.color color.grey ] <| - row [ width fill, spacing 20 ] - [ Input.button - [ Background.color color.blue - , Font.color color.white - , Border.color color.red - , paddingXY 32 16 - , Border.rounded 15 - ] - { onPress = Just <| Download status.id - , label = Element.text "Download" - } - , el [ Font.size 16 ] <| text file.filename - ] - - Nothing -> - Element.none - ] - ) view : Model -> Element Msg @@ -225,8 +101,8 @@ view model = ] [ case model.status of Just status -> - statusView status + TaskStatus.statusView status |> Element.map TaskStatusMsg Nothing -> - Element.text "Test" + Element.text "File Not Found" ] diff --git a/Modules/Website/src/TaskStatus.elm b/Modules/Website/src/TaskStatus.elm new file mode 100644 index 0000000..7fca7f9 --- /dev/null +++ b/Modules/Website/src/TaskStatus.elm @@ -0,0 +1,132 @@ +module TaskStatus exposing (..) +import Json.Decode exposing (Decoder, int, list, string, succeed) +import Json.Decode.Pipeline exposing (optional, required) +import Element.Background as Background +import Element.Border as Border +import Element.Font as Font +import Element.Input as Input +import Element exposing (..) +import String +import Globals exposing (apiDownloadFile) +import File.Download exposing (url) +import Color exposing (color) + +type alias File = + { format : String + , filename : String + , timestamp : Int + } + + +type alias Status = + { file : Maybe File + , name : String + , source : String + , status : String + , timestamp : Int + , id : String + } + + +fileDecoder : Decoder File +fileDecoder = + succeed File + |> required "format" string + |> required "filename" string + |> required "timestamp" int + + +statusDecoder : Decoder Status +statusDecoder = + succeed Status + |> optional "file" (Json.Decode.map Just fileDecoder) Nothing + |> required "name" string + |> required "source" string + |> required "status" string + |> required "timestamp" int + |> required "hash" string + +type Msg + = Download String + +update : Msg -> Cmd Msg +update msg = + case msg of + Download id -> url <| apiDownloadFile id + +statusView : Status -> Element Msg +statusView status = + card [ width fill, height shrink ] + (Element.column + [ width fill ] + [ row [ width fill, height <| px 40 ] + [ el [ Font.size 28, alignLeft ] <| text status.name + , el [ Font.size 28, alignRight ] <| text (String.concat [ "[", status.status, "]" ]) + ] + , link [ Font.bold, Font.underline, Font.color color.blue, Font.size 16, padding 2 ] + { url = status.source, label = text status.source } + , el [ height <| px 10 ] none + , case status.file of + Just file -> + innerCard [ width fill, height shrink, Background.color color.grey ] <| + row [ width fill, spacing 20 ] + [ Input.button + [ Background.color color.blue + , Font.color color.white + , Border.color color.red + , paddingXY 32 16 + , Border.rounded 15 + ] + { onPress = Just <| Download status.id + , label = Element.text "Download" + } + , el [ Font.size 16 ] <| text file.filename + ] + + Nothing -> + Element.none + ] + ) + + + +cardShadow : Attr decorative msg +cardShadow = + Border.shadow + { offset = ( 0, 4 ) + , size = 4 + , blur = 8 + , color = rgba 0 0 0 0.2 + } + + +cardShadowOver : Attr decorative msg +cardShadowOver = + Border.shadow + { offset = ( 0, 4 ) + , size = 8 + , blur = 16 + , color = rgba 0 0 0 0.2 + } + + +card : List (Attribute msg) -> Element msg -> Element msg +card attr element = + el + (List.concat + [ attr + , [ Border.rounded 15, cardShadow, mouseOver [ cardShadowOver ], padding 20 ] + ] + ) + element + + +innerCard : List (Attribute msg) -> Element msg -> Element msg +innerCard attr element = + el + (List.concat + [ attr + , [ Border.roundEach { topLeft = 15, topRight = 15, bottomLeft = 15, bottomRight = 15 }, padding 5 ] + ] + ) + element \ No newline at end of file