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/api.cpp
src/worker.hpp src/worker.hpp
src/worker.cpp src/worker.cpp
src/tables.hpp) src/tables.hpp
src/file_status.hpp
src/file_status.cpp)
add_executable(localTubeServer ${src_files}) add_executable(localTubeServer ${src_files})

View File

@@ -7,6 +7,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <functional> #include <functional>
#include <exception>
#include <soci/boost-optional.h> #include <soci/boost-optional.h>
@@ -16,6 +17,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "file_status.hpp"
using json = nlohmann::json; using json = nlohmann::json;
Api::Api(soci::session& sql, httplib::Server& server) : sql(sql) 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 try
{ {
auto data = TaskInfo::getAll(sql);
json res; json res;
res["files"] = data;
// 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"); 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); 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); fs->seekg(0, std::ios_base::end);
auto size = fs->tellg(); auto size = fs->tellg();
fs->seekg(0); 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_header("Content-Disposition", fmt::format("attachment; filename={};", dw.filename));
rs.set_content_provider( 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 { [file = fs](size_t offset, size_t length, httplib::DataSink& sink) mutable {
size_t size = std::min(length, (size_t)1024 * 500); size_t size = std::min(length, (size_t)1024 * 500);
//spdlog::info("Stream offset {} length {}", offset, length);
std::vector<char> data; std::vector<char> data;
data.resize(size); data.resize(size);
@@ -237,54 +194,26 @@ void Api::status(httplib::Request const& rq, httplib::Response& rs)
std::string hash = rq.matches[1]; 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; json response;
try
auto& status = response["status"];
if (data)
{ {
auto const& el = data.value(); auto data = TaskInfo::getId(sql, hash);
status["name"] = fmt::format("Task {}", el.hash); if (data)
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; response["status"] = data.value();
sql << "SELECT * FROM Files WHERE hash=:hash", soci::use(hash, "hash"),
soci::into(file_data);
if (file_data) } else
{ {
auto const& file = file_data.value(); rs.status = 404;
response["error"] = fmt::format("File '{}' not found!", hash);
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 catch (std::exception const& e)
{ {
spdlog::error("[Api Status] Excepion: {}", e.what());
rs.status = 404; 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"); 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); File::create_table(sql);
} }
Worker worker(sql); //Worker worker(sql);
httplib::Server srv; httplib::Server srv;
srv.set_mount_point("/", "www/"); srv.set_mount_point("/", "www/");
@@ -75,5 +75,7 @@ int main()
Api api(sql, srv); Api api(sql, srv);
spdlog::info("Listing on 0.0.0.0 Port 80"); 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 Element.Region as Region
import Globals exposing (apiFiles) import Globals exposing (apiFiles)
import Http 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 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 -- -- Model --
type alias Model = type Model
{ files : List File = RequestError String
, filter : String | Loading
, error : Maybe String | Files (List TaskStatus.Status)
}
initModel : Model initModel : Model
initModel = initModel = Loading
{ files =
[]
, filter = ""
, error = Nothing
}
init : ( Model, Cmd Msg ) init : ( Model, Cmd Msg )
@@ -55,26 +41,18 @@ init =
type Msg type Msg
= Reload = Reload
| QueryRequestResult (Result Http.Error FilesResponse) | QueryRequestResult (Result Http.Error FilesResponse)
| TaskStatusMsg TaskStatus.Msg
-- Request -- -- Request --
type alias FilesResponse =
{ files : List TaskStatus.Status }
fileDecoder : Decoder File
fileDecoder =
succeed File
|> required "name" string
|> required "file_name" string
|> required "source" string
|> required "id" int
|> required "status" string
queueDecoder : Decoder FilesResponse queueDecoder : Decoder FilesResponse
queueDecoder = queueDecoder =
succeed FilesResponse succeed FilesResponse
|> required "queue" (list fileDecoder) |> required "files" (list TaskStatus.statusDecoder)
@@ -85,10 +63,13 @@ update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = update msg model =
case msg of case msg of
QueryRequestResult (Ok res) -> QueryRequestResult (Ok res) ->
( { model | files = res.queue }, Cmd.none ) ( Files res.files, Cmd.none )
QueryRequestResult (Err (Http.BadBody er)) -> QueryRequestResult (Err (Http.BadBody err)) ->
( { model | error = Just er }, Cmd.none ) ( RequestError err, Cmd.none )
TaskStatusMsg sub_msg ->
( model, Cmd.map TaskStatusMsg <| TaskStatus.update sub_msg)
_ -> _ ->
( model, Cmd.none ) ( 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 -> Element Msg
view model = view model =
@@ -199,19 +124,26 @@ view model =
, Font.size 36 , Font.size 36
] ]
(text "Files") (text "Files")
, case model.error of , case model of
Just e -> Loading ->
el el
[ Font.color (rgb 1 0 0) [ Font.color (rgb 1 0 0)
, Font.size 16 , 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 Element.column
[ width fill [ width fill
, centerX , centerX
, spacing 20 , 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 | UpdateFormFormatChnaged Format
| PostForm Form | PostForm Form
| PostResult (Result Http.Error PostFormResponse) | PostResult (Result Http.Error PostFormResponse)
| None
type AudioFormats type AudioFormats

View File

@@ -1,22 +1,16 @@
module Status exposing (Model, Msg, init, queryStatus, update, view) module Status exposing (Model, Msg, init, queryStatus, update, view)
import Color exposing (color)
import Debug exposing (toString)
import Delay import Delay
import Element exposing (..) 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 Files exposing (Msg(..))
import Globals exposing (apiDownloadFile, apiStatus) import Globals exposing (apiStatus)
import Http import Http
import Json.Decode exposing (Decoder, int, list, string, succeed) import Json.Decode exposing (Decoder, succeed)
import Json.Decode.Pipeline exposing (optional, required) import Json.Decode.Pipeline exposing (required)
import Route exposing (Route(..)) import Route exposing (Route(..))
import String exposing (right)
import Task import Task
import TaskStatus
@@ -26,7 +20,7 @@ import Task
type Msg type Msg
= Reload = Reload
| StatusRequestResult (Result Http.Error StatusResponse) | StatusRequestResult (Result Http.Error StatusResponse)
| Download String | TaskStatusMsg TaskStatus.Msg
@@ -35,7 +29,7 @@ type Msg
type alias Model = type alias Model =
{ file_id : String { file_id : String
, status : Maybe Status , status : Maybe TaskStatus.Status
} }
@@ -47,55 +41,16 @@ init file_id =
-- Request -- 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 = 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 : Decoder StatusResponse
statusResponseDecoder = statusResponseDecoder =
succeed StatusResponse succeed StatusResponse
|> required "status" statusDecoder |> required "status" TaskStatus.statusDecoder
queryStatus : String -> Cmd Msg queryStatus : String -> Cmd Msg
@@ -110,48 +65,6 @@ queryStatus id =
-- Update -- 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 -> Cmd msg
send msg = send msg =
Task.succeed msg Task.succeed msg
@@ -170,48 +83,11 @@ update msg model =
StatusRequestResult _ -> StatusRequestResult _ ->
( { model | status = Nothing }, Cmd.none ) ( { model | status = Nothing }, Cmd.none )
Download file_id -> TaskStatusMsg sub_msg ->
( model, url <| apiDownloadFile file_id ) ( 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 view : Model -> Element Msg
@@ -225,8 +101,8 @@ view model =
] ]
[ case model.status of [ case model.status of
Just status -> Just status ->
statusView status TaskStatus.statusView status |> Element.map TaskStatusMsg
Nothing -> 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