♻️ refactor
Some checks failed
ci/woodpecker/push/build Pipeline failed

This commit is contained in:
JMARyA 2025-02-09 07:09:20 +01:00
parent 2ac2559c23
commit 59dae90b4d
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
13 changed files with 368 additions and 138 deletions

12
src/config.rs Normal file
View file

@ -0,0 +1,12 @@
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub general: GeneralConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GeneralConfig {
pub private: bool,
pub video_path: String,
}

View file

@ -1,10 +1,17 @@
use based::{asset::AssetRoutes, auth::User, get_pg};
use based::{
asset::AssetRoutes,
auth::User,
get_pg,
ui::components::{AppBar, Shell},
};
use rocket::{http::Method, routes};
use std::path::Path;
mod config;
mod library;
mod meta;
mod pages;
mod yt_meta;
use based::ui::prelude::*;
#[rocket::launch]
async fn launch() -> _ {
@ -12,14 +19,10 @@ async fn launch() -> _ {
std::env::set_var("RUST_BACKTRACE", "1");
env_logger::init();
let args: Vec<String> = std::env::args().collect();
let conf: config::Config =
toml::from_str(&std::fs::read_to_string("config.toml").unwrap()).unwrap();
if args.len() != 2 {
eprintln!("Usage: {} <directory_path>", args[0]);
std::process::exit(1);
}
let dir_path = args[1].clone();
let dir_path = conf.general.video_path.clone();
let pg = get_pg!();
@ -50,6 +53,13 @@ async fn launch() -> _ {
.to_cors()
.expect("error creating CORS options");
let shell = Shell::new(
Nothing(),
Nothing(),
Background(Text("").white()).color(Colors::Black),
)
.use_ui();
rocket::build()
.mount_assets()
.mount(
@ -74,4 +84,6 @@ async fn launch() -> _ {
)
.attach(cors)
.manage(lib)
.manage(conf)
.manage(shell)
}

View file

@ -1,12 +1,21 @@
use based::request::assets::DataResponse;
use based::{auth::MaybeUser, request::assets::DataResponse};
use rocket::{get, State};
use tokio::{fs::File, io::AsyncReadExt};
use crate::library::Library;
use crate::{config::Config, library::Library};
#[get("/video/raw?<v>")]
pub async fn video_file(v: &str, library: &State<Library>) -> Option<DataResponse> {
pub async fn video_file(
v: &str,
library: &State<Library>,
conf: &State<Config>,
user: MaybeUser,
) -> Option<DataResponse> {
if conf.general.private && user.user().is_none() {
return None;
}
let video = if let Some(video) = library.get_video_by_id(v).await {
video
} else {
@ -33,7 +42,16 @@ pub async fn video_file(v: &str, library: &State<Library>) -> Option<DataRespons
}
#[get("/video/thumbnail?<v>")]
pub async fn video_thumbnail(v: &str, library: &State<Library>) -> Option<DataResponse> {
pub async fn video_thumbnail(
v: &str,
library: &State<Library>,
conf: &State<Config>,
user: MaybeUser,
) -> Option<DataResponse> {
if conf.general.private && user.user().is_none() {
return None;
}
let video = if let Some(video) = library.get_video_by_id(v).await {
video
} else {

View file

@ -1,5 +1,9 @@
use crate::library::Video;
use based::ui::components::{AppBar, Shell};
use based::ui::components::prelude::Avatar;
use based::ui::components::prelude::*;
use based::ui::components::{AppBar, Card, NavBar, Shell};
use based::ui::primitives::flex::Column;
use based::ui::primitives::Optional;
use based::ui::wrapper::HoverWrapper;
use based::ui::{prelude::*, UIWidget};
use based::{
@ -10,22 +14,37 @@ use based::{
use maud::{html, PreEscaped, Render};
pub async fn render_page(
shell: &Shell,
ctx: RequestContext,
content: PreEscaped<String>,
title: &str,
user: Option<User>,
) -> StringResponse {
Shell::new(
Nothing(),
Div()
.vanish()
.push(script(include_str!("../scripts/header.js")))
.push(AppBar("WatchDogs", user)),
Background(Text("").white()).color(Colors::Black),
)
.use_ui()
.render_page(content, title, ctx)
.await
shell
.extend()
.with_navbar(
NavBar("WatchDogs")
.icon(Sized(
ScreenValue::_8,
ScreenValue::_8,
Rounded(Image("/favicon").alt("Logo")).size(Size::Medium),
))
.extra(Optional(user.as_ref(), |user| {
// TODO : profile pictures
DropDown(
Avatar("", &user.username).use_initials(),
Column(vec![Link(
"/history",
Padding(Text("Video History").medium())
.x(ScreenValue::_4)
.y(ScreenValue::_2),
)
.use_htmx()]),
)
})),
)
.render_page(content, title, ctx)
.await
}
#[allow(non_snake_case)]
@ -48,7 +67,18 @@ pub fn Title<T: UIWidget + 'static>(title: T) -> PreEscaped<String> {
}
#[allow(non_snake_case)]
pub fn VerticalVideoGrid<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
pub async fn VerticalVideoGrid(videos: &mut [Video]) -> PreEscaped<String> {
let video_elements = html! {
@for mut vid in videos {
( video_element(&mut vid).await );
};
};
VerticalVideoGridRaw(video_elements)
}
#[allow(non_snake_case)]
pub fn VerticalVideoGridRaw<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
Screen::large(Grid(Nothing()).columns(GridAmount::_3).gap(ScreenValue::_6))
.on(Padding(Margin(inner).bottom(ScreenValue::_4)).all(ScreenValue::_6))
.render()
@ -141,7 +171,7 @@ pub fn video_thumbnail_with_time(video: &Video) -> PreEscaped<String> {
Position(
PositionKind::Relative,
Div()
.push(Context(Width(
.push(Context(ObjectFit::Cover(Width(
ScreenValue::full,
Height(
ScreenValue::auto,
@ -151,7 +181,7 @@ pub fn video_thumbnail_with_time(video: &Video) -> PreEscaped<String> {
.side(Side::Top),
),
),
)))
))))
.push(Context(
Position(
PositionKind::Absolute,
@ -216,3 +246,19 @@ pub async fn video_element(video: &mut Video) -> PreEscaped<String> {
)
.render()
}
#[allow(non_snake_case)]
pub fn DirectoryBadge(dir: &str) -> PreEscaped<String> {
Margin(
Padding(
Hover(Background(Cursor::Pointer.on(Nothing())).color(Purple::_600)).on(Background(
Rounded(Link(&format!("/d/{dir}"), Text(dir).white()).use_htmx()).size(Size::Full),
)
.color(Purple::_500)),
)
.x(ScreenValue::_3)
.y(ScreenValue::_2),
)
.all(ScreenValue::_2)
.render()
}

View file

@ -1,4 +1,7 @@
use based::ui::htmx::HTMXAttributes;
use based::page;
use based::request::respond_html;
use based::ui::components::prelude::InfinityScroll;
use based::ui::components::{ColoredSpinner, Shell};
use based::ui::prelude::*;
use based::{
auth::MaybeUser,
@ -7,24 +10,42 @@ use based::{
RequestContext, StringResponse,
},
};
use maud::{html, Render};
use rocket::{get, State};
use maud::{html, PreEscaped, Render};
use rocket::{get, uri, State};
use serde_json::json;
use crate::pages::components::{Title, VerticalVideoGrid};
use crate::config::Config;
use crate::library::Video;
use crate::pages::components::{Title, VerticalVideoGrid, VerticalVideoGridRaw};
use crate::{library::Library, pages::components::video_element};
use super::components::DirectoryBadge;
use super::{
api_response,
components::{render_page, video_element_wide},
};
#[macro_export]
macro_rules! check_private {
($conf:ident, $user:ident, $shell:ident, $ctx:ident) => {
if $conf.general.private && $user.user().is_none() {
return $crate::pages::index::is_private_page($shell, $ctx).await;
}
};
}
#[get("/search?<query>&<offset>")]
pub async fn search(
query: &str,
offset: Option<i64>,
library: &State<Library>,
conf: &State<Config>,
user: MaybeUser,
shell: &State<Shell>,
ctx: RequestContext,
) -> Option<serde_json::Value> {
// todo : check_private!(conf, user, shell, ctx);
const NUM_OF_RESULTS: i64 = 20;
// get start parameter for search result chunks
@ -36,32 +57,87 @@ pub async fn search(
}
#[get("/latest.json")]
pub async fn latest_api(library: &State<Library>) -> FallibleApiResponse {
pub async fn latest_api(
library: &State<Library>,
user: MaybeUser,
conf: &State<Config>,
shell: &State<Shell>,
ctx: RequestContext,
) -> FallibleApiResponse {
// todo : check_private!(conf, user, shell, ctx);
let videos = library.get_newly_added(20).await;
let vid_api = vec_to_api(&videos).await;
return Ok(serde_json::json!(vid_api));
}
#[get("/latest")]
pub async fn latest_page(
ctx: RequestContext,
library: &State<Library>,
user: MaybeUser,
) -> StringResponse {
let videos = library.get_newly_added(20).await;
#[allow(non_snake_case)]
pub async fn VideoList(videos: &[Video]) -> PreEscaped<String> {
let video_elements = html! {
@for mut vid in videos {
( video_element_wide(&mut vid).await );
};
};
Padding(video_elements).all(ScreenValue::_6).render()
}
#[get("/latest?<offset>")]
pub async fn latest_page(
ctx: RequestContext,
library: &State<Library>,
user: MaybeUser,
shell: &State<Shell>,
conf: &State<Config>,
offset: Option<u32>,
) -> StringResponse {
check_private!(conf, user, shell, ctx);
let mut videos: Vec<_> = library
.get_newly_added(offset.unwrap_or_default() as i64 + 20)
.await
.into_iter()
.skip(offset.unwrap_or_default() as usize)
.take(20)
.collect();
let has_content = !videos.is_empty();
let video_elements = VerticalVideoGrid(&mut videos).await;
if ctx.is_htmx && !ctx.htmx_redirect {
return respond_html(
Div()
.vanish()
.push(video_elements)
.push_if(has_content, || {
Width(
ScreenValue::fit,
Margin(InfinityScroll(
ColoredSpinner(Purple::_600),
&format!("/latest?offset={}", offset.unwrap_or_default() + 20),
))
.x(ScreenValue::auto),
)
})
.render()
.0,
);
}
let content = Div()
.vanish()
.push(Title("Recent videos"))
.push(Padding(video_elements).all(ScreenValue::_6))
.push(video_elements)
.push(Width(
ScreenValue::fit,
Margin(InfinityScroll(
ColoredSpinner(Purple::_600),
&format!("/latest?offset={}", offset.unwrap_or_default() + 20),
))
.x(ScreenValue::auto),
))
.render();
render_page(ctx, content, "Recent videos", user.into()).await
render_page(&shell, ctx, content, "Recent videos", user.into()).await
}
#[get("/d/<dir>")]
@ -70,7 +146,11 @@ pub async fn dir_page(
dir: &str,
library: &State<Library>,
user: MaybeUser,
shell: &State<Shell>,
conf: &State<Config>,
) -> StringResponse {
check_private!(conf, user, shell, ctx);
if dir.ends_with(".json") {
let dir_videos = library
.get_directory_videos(dir.split_once(".json").map(|x| x.0).unwrap_or_default())
@ -91,7 +171,11 @@ pub async fn dir_page(
.push(Padding(video_elements).all(ScreenValue::_6))
.render();
render_page(ctx, content, dir, user.into()).await
render_page(&shell, ctx, content, dir, user.into()).await
}
pub async fn is_private_page(shell: &Shell, ctx: RequestContext) -> StringResponse {
page!(shell, ctx, "WatchDogs", Text("This is a private instance"))
}
#[get("/")]
@ -99,7 +183,11 @@ pub async fn index_page(
ctx: RequestContext,
library: &State<Library>,
user: MaybeUser,
shell: &State<Shell>,
conf: &State<Config>,
) -> StringResponse {
check_private!(conf, user, shell, ctx);
let random_video_elements = html! {
@for mut vid in library.get_random_videos(3).await {
( video_element(&mut vid).await );
@ -115,34 +203,22 @@ pub async fn index_page(
let content = Div()
.vanish()
.push(Title("Random Videos"))
.push(VerticalVideoGrid(random_video_elements))
.push(VerticalVideoGridRaw(random_video_elements))
.push(Title(Link("/latest", "Latest Videos").use_htmx()))
.push(VerticalVideoGrid(newly_added_elements))
.push(VerticalVideoGridRaw(newly_added_elements))
.push(Title("Directories:"))
.push(
Padding(
Flex(Div().vanish().push_for_each(&directories, |dir: &_| {
Margin(
Padding(
Hover(Background(Cursor::Pointer.on(Nothing())).color(Purple::_600))
.on(Background(
Rounded(
Link(&format!("/d/{dir}"), Text(&dir).white()).use_htmx(),
)
.size(Size::Full),
)
.color(Purple::_500)),
)
.x(ScreenValue::_3)
.y(ScreenValue::_2),
)
.all(ScreenValue::_2)
}))
Flex(
Div()
.vanish()
.push_for_each(&directories, |dir: &_| DirectoryBadge(dir)),
)
.wrap(Wrap::Wrap),
)
.all(ScreenValue::_10),
)
.render();
render_page(ctx, content, "WatchDogs", user.into()).await
render_page(&shell, ctx, content, "WatchDogs", user.into()).await
}

View file

@ -1,9 +1,12 @@
use based::auth::{Sessions, User};
use based::page;
use based::request::StringResponse;
use based::ui::components::{Card, Shell};
use based::ui::prelude::*;
use based::{auth::MaybeUser, request::RequestContext};
use maud::{html, Render};
use rocket::http::CookieJar;
use rocket::State;
use rocket::{form::Form, get, http::Cookie, post, response::Redirect, FromForm};
use crate::library::history::VideoHistory;
@ -12,17 +15,32 @@ use crate::pages::components::{video_element_wide, Title};
use super::components::render_page;
#[get("/login")]
pub async fn login(ctx: RequestContext, user: MaybeUser) -> StringResponse {
let content = html!(
h2 { "Login" };
form action="/login" method="POST" {
input type="text" name="username" placeholder="Username" required;
input type="password" name="password" placeholder="Password" required;
input type="submit" value="Login";
}
);
render_page(ctx, content, "Login", user.into()).await
pub async fn login(ctx: RequestContext, user: MaybeUser, shell: &State<Shell>) -> StringResponse {
page!(
shell,
ctx,
"Login",
Margin(Width(
ScreenValue::_80,
Div().push(Text("Login")._2xl()).push(
Margin(
based::ui::primitives::input::Form::new("/login")
.method(FormMethod::POST)
.add_input(TextInput("username").placeholder("Username").required())
.add_input(
TextInput("password")
.placeholder("Password")
.required()
.password()
)
.add_input(FormSubmitButton("Login").value("Login"))
)
.top(ScreenValue::_4)
)
))
.x(ScreenValue::auto)
.top(ScreenValue::_6)
)
}
#[derive(FromForm)]
@ -49,7 +67,7 @@ pub async fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) ->
}
#[get("/history")]
pub async fn history_page(ctx: RequestContext, user: User) -> StringResponse {
pub async fn history_page(ctx: RequestContext, user: User, shell: &State<Shell>) -> StringResponse {
let video_elements = html! {
@for mut vid in user.history_of(10).await {
( video_element_wide(&mut vid).await );
@ -62,5 +80,5 @@ pub async fn history_page(ctx: RequestContext, user: User) -> StringResponse {
.push(Padding(video_elements).all(ScreenValue::_6))
.render();
render_page(ctx, content, "History", Some(user)).await
render_page(&shell, ctx, content, "History", Some(user)).await
}

View file

@ -1,3 +1,4 @@
use based::ui::components::Shell;
use based::ui::primitives::space::Fraction;
use based::ui::{prelude::*, AttrExtendable};
use based::{
@ -8,6 +9,8 @@ use based::{
use maud::{html, PreEscaped, Render};
use rocket::{get, State};
use crate::check_private;
use crate::config::Config;
use crate::library::Video;
use crate::{
library::{history::VideoHistory, Library},
@ -22,7 +25,11 @@ pub async fn watch_page(
library: &State<Library>,
v: String,
user: MaybeUser,
conf: &State<Config>,
shell: &State<Shell>,
) -> StringResponse {
check_private!(conf, user, shell, ctx);
let video = if let Some(video) = library.get_video_by_id(&v).await {
video
} else {
@ -37,7 +44,7 @@ pub async fn watch_page(
let youtube_meta = video.youtube_meta().await;
let rec = build_rec(&library, &video).await;
let content = Container(
let content =
Margin(
Screen::large(Flex(Nothing()).direction(Direction::Row)).on(
Flex(
@ -50,7 +57,7 @@ pub async fn watch_page(
Rounded(
Video().controls().autoplay().width(1080).add_src(
Source(&format!("/video/raw?v={}", video.id), Some("video/mp4".to_string()))
)
).poster(&format!("/video/thumbnail?v={}", video.id))
).size(Size::Large)
).color(Colors::Black)
))
@ -94,10 +101,10 @@ pub async fn watch_page(
rec
)
).direction(Direction::Column).gap(ScreenValue::_6))
).x(ScreenValue::_10).top(ScreenValue::_6)
).render();
).x(ScreenValue::_10).top(ScreenValue::_6).render();
render_page(
&shell,
ctx,
content,
&format!("{} - WatchDogs", video.title),
@ -123,10 +130,18 @@ pub async fn build_rec(library: &Library, video: &Video) -> PreEscaped<String> {
Margin(
Paragraph(Context(
SpaceBetween(
Flex(Div().vanish().push(Span("In ")).push(Link(
&format!("/d/{}", video.directory),
Text(&video.directory).color(&Blue::_500),
)))
Flex(
Div()
.vanish()
.push(Span("In ")._4xl().extrabold())
.push(Link(
&format!("/d/{}", video.directory),
Text(&video.directory)
.color(&Blue::_500)
._4xl()
.extrabold(),
)),
)
.group()
.justify(Justify::Center),
)

View file

@ -1,7 +0,0 @@
function stopAllVideos() {
const videos = document.querySelectorAll('video');
videos.forEach(video => {
video.pause();
});
}