Add: universal task display

This commit is contained in:
2021-06-04 00:50:07 +02:00
parent 737097fbed
commit 231f06c2e3
9 changed files with 326 additions and 331 deletions

View File

@@ -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})

View File

@@ -7,6 +7,7 @@
#include <filesystem>
#include <fstream>
#include <functional>
#include <exception>
#include <soci/boost-optional.h>
@@ -16,6 +17,8 @@
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#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<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);
}
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<std::ifstream>(dw.local_path, std::ios_base::binary | std::ios::in);
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(
@@ -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<char> 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<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)
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<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)
if (data)
{
boost::optional<File> 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");
}

View File

@@ -0,0 +1,76 @@
#include "file_status.hpp"
#include <vector>
#include <soci/boost-optional.h>
#include "tables.hpp"
std::optional<TaskInfo> TaskInfo::getId(soci::session& sql, std::string_view hash)
{
auto hash_str = std::string { hash };
soci::rowset<soci::row> 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> TaskInfo::getAll(soci::session& sql)
{
soci::rowset<soci::row> 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<TaskInfo> 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<FileStatus>("status");
status.hash = row.get<std::string>("hash");
status.name = fmt::format("Task {}", status.hash);
status.status = magic_enum::enum_name(file_status);
status.timestamp = row.get<int>("TaskTimestamp");
status.url = row.get<std::string>("url");
if (file_status == FileStatus::COMPLETE)
{
status.file = { .timestamp = row.get<int>("FileTimestamp"),
.filename = row.get<std::string>("filename"),
.format = row.get<std::string>("format") };
}
return status;
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
#include <soci/session.h>
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<FileInfo> file;
static std::optional<TaskInfo> getId(soci::session& sql, std::string_view hash);
static std::vector<TaskInfo> 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;
}
}

View File

@@ -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);
}

View File

@@ -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) )
]

View File

@@ -44,7 +44,7 @@ type Msg
| UpdateFormFormatChnaged Format
| PostForm Form
| PostResult (Result Http.Error PostFormResponse)
| None
type AudioFormats

View File

@@ -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"
]

View File

@@ -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