This commit is contained in:
parent
2d2f7c5522
commit
4a4ae89b1b
6 changed files with 238 additions and 22 deletions
|
@ -1,5 +1,6 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use func::is_video_file;
|
use func::is_video_file;
|
||||||
|
@ -96,7 +97,7 @@ impl Library {
|
||||||
|
|
||||||
pub async fn get_video_by_id(&self, id: &str) -> Option<Video> {
|
pub async fn get_video_by_id(&self, id: &str) -> Option<Video> {
|
||||||
sqlx::query_as("SELECT * FROM videos WHERE id = $1")
|
sqlx::query_as("SELECT * FROM videos WHERE id = $1")
|
||||||
.bind(id)
|
.bind(uuid::Uuid::from_str(id).unwrap())
|
||||||
.fetch_optional(&self.conn)
|
.fetch_optional(&self.conn)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
|
@ -76,7 +76,9 @@ async fn launch() -> _ {
|
||||||
pages::index::channel_page,
|
pages::index::channel_page,
|
||||||
pages::yt::yt_tags,
|
pages::yt::yt_tags,
|
||||||
pages::yt::yt_tag_page,
|
pages::yt::yt_tag_page,
|
||||||
pages::yt::yt_channel_page
|
pages::yt::yt_channel_page,
|
||||||
|
pages::index::index_page,
|
||||||
|
pages::watch::watch_page
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.attach(cors)
|
.attach(cors)
|
||||||
|
|
|
@ -1,20 +1,66 @@
|
||||||
|
use core::num;
|
||||||
|
|
||||||
use maud::{html, PreEscaped};
|
use maud::{html, PreEscaped};
|
||||||
|
|
||||||
use crate::library::Video;
|
use crate::library::Video;
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
http::{ContentType, Status},
|
||||||
|
request::{self, FromRequest, Request},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HTMX {
|
||||||
|
is_htmx: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for HTMX {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
rocket::outcome::Outcome::Success(HTMX {
|
||||||
|
is_htmx: !req
|
||||||
|
.headers()
|
||||||
|
.get("HX-Request")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.is_empty(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shell(content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
pub fn shell(content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
||||||
html! {
|
html! {
|
||||||
html {
|
html {
|
||||||
head {
|
head {
|
||||||
title { (title) };
|
title { (title) };
|
||||||
|
script src="https://cdn.tailwindcss.com" {};
|
||||||
|
script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous" {};
|
||||||
|
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||||
};
|
};
|
||||||
body {
|
body class="bg-black text-white" {
|
||||||
(content)
|
div id="main_content" {
|
||||||
|
(content)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_page(
|
||||||
|
htmx: HTMX,
|
||||||
|
content: PreEscaped<String>,
|
||||||
|
title: &str,
|
||||||
|
) -> (Status, (ContentType, String)) {
|
||||||
|
if !htmx.is_htmx {
|
||||||
|
(
|
||||||
|
Status::Ok,
|
||||||
|
(ContentType::HTML, shell(content, title).into_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Status::Ok, (ContentType::HTML, content.into_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn loading_spinner() -> PreEscaped<String> {
|
pub fn loading_spinner() -> PreEscaped<String> {
|
||||||
html! {
|
html! {
|
||||||
style {
|
style {
|
||||||
|
@ -36,21 +82,81 @@ pub fn search_bar(query: &str) -> PreEscaped<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn video_element(video: &mut Video) -> PreEscaped<String> {
|
fn format_seconds_to_hhmmss(seconds: f64) -> String {
|
||||||
|
let total_seconds = seconds as u64;
|
||||||
|
let hours = total_seconds / 3600;
|
||||||
|
let minutes = (total_seconds % 3600) / 60;
|
||||||
|
let seconds = total_seconds % 60;
|
||||||
|
if hours != 0 {
|
||||||
|
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
format!("{:02}:{:02}", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_date(date: &chrono::NaiveDate) -> String {
|
||||||
|
// TODO : Implement
|
||||||
|
date.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_number(num: i64) -> String {
|
||||||
|
// TODO : Implement
|
||||||
|
num.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn video_element_wide(video: &mut Video) -> PreEscaped<String> {
|
||||||
html!(
|
html!(
|
||||||
@let desc = video.youtube_meta().await.map(|x| x.description);
|
a href=(format!("/watch?v={}", video.id)) class="flex items-center w-full p-4 bg-gray-900 shadow-lg rounded-lg overflow-hidden mb-2 mt-2" {
|
||||||
@let video_id = video.id;
|
div class="flex-shrink-0" {
|
||||||
article class="container-fluid" style="margin: 50px; cursor: pointer;" {
|
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) alt="Video Thumbnail" class="w-48 h-32 object-cover rounded-md";
|
||||||
a href=(format!("/watch?v={video_id}")) style="text-decoration:none !important;" {
|
};
|
||||||
img style="width: 350px;" width="480" src=(format!("/video/thumbnail?v={video_id}"));
|
|
||||||
div style="padding: 10px;" {
|
div class="flex flex-col flex-grow ml-4" {
|
||||||
h2 style="margin: 0; font-size: 18px;" { (video.title) };
|
h3 class="text-lg font-semibold truncate mb-1" {
|
||||||
@if !desc.as_ref().map(|x| x.is_empty()).unwrap_or(true) {
|
( video.title )
|
||||||
p style="margin: 0; color: grey; font-size: 14px;margin-top: 10px;" { (desc.unwrap().chars().take(200).chain("...".to_string().chars()).take(203).collect::<String>()) };
|
};
|
||||||
|
|
||||||
|
@if let Some(meta) = video.youtube_meta().await {
|
||||||
|
div class="text-sm text-gray-400 mb-2" {
|
||||||
|
span class="font-medium" { ( meta.uploader_name ) }
|
||||||
|
span { " - " }
|
||||||
|
span { ( format_date(&meta.upload_date) ) }
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="text-sm text-gray-400" {
|
||||||
|
span { ( format_number(meta.views) ) }
|
||||||
|
span { " views" }
|
||||||
|
span { " - " }
|
||||||
|
span class="ml-2 bg-black text-white text-xs px-2 py-1 rounded-sm opacity-90" {
|
||||||
|
(( format_seconds_to_hhmmss(video.duration) ))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn video_element(video: &mut Video) -> PreEscaped<String> {
|
||||||
|
html!(
|
||||||
|
a href=(format!("/watch?v={}", video.id)) class="max-w-sm mx-auto p-4 max-h-60 aspect-video" {
|
||||||
|
div class="bg-gray-900 shadow-lg rounded-lg overflow-hidden" {
|
||||||
|
div class="relative" {
|
||||||
|
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) alt="Video Thumbnail" class="w-full h-auto object-cover aspect-video";
|
||||||
|
span class="absolute bottom-2 right-2 bg-black text-white text-xs px-2 py-1 rounded-sm opacity-90" {
|
||||||
|
(( format_seconds_to_hhmmss(video.duration) ))
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
div class="bg-gray-900 shadow-lg rounded-lg overflow-hidden" {
|
||||||
|
div class="p-4" {
|
||||||
|
h3 class="text-lg font-semibold truncate" {
|
||||||
|
( video.title )
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use rocket::{get, State};
|
use rocket::{
|
||||||
|
get,
|
||||||
|
http::{ContentType, Status},
|
||||||
|
State,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::library::Library;
|
use crate::{library::Library, pages::components::video_element};
|
||||||
|
|
||||||
use super::vec_to_api;
|
use super::{
|
||||||
|
api_response,
|
||||||
|
components::{render_page, video_element_wide, HTMX},
|
||||||
|
vec_to_api,
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/search?<query>&<offset>")]
|
#[get("/search?<query>&<offset>")]
|
||||||
pub async fn search(
|
pub async fn search(
|
||||||
|
@ -23,13 +31,49 @@ pub async fn search(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/d/<dir>")]
|
#[get("/d/<dir>")]
|
||||||
pub async fn channel_page(dir: &str, library: &State<Library>) -> Option<serde_json::Value> {
|
pub async fn channel_page(
|
||||||
let mut dir_videos = library.get_directory_videos(dir).await;
|
htmx: HTMX,
|
||||||
|
dir: &str,
|
||||||
|
library: &State<Library>,
|
||||||
|
) -> (Status, (ContentType, String)) {
|
||||||
|
if dir.ends_with(".json") {
|
||||||
|
let dir_videos = library
|
||||||
|
.get_directory_videos(dir.split_once(".json").map(|x| x.0).unwrap_or_default())
|
||||||
|
.await;
|
||||||
|
return api_response(&json!(vec_to_api(&dir_videos).await));
|
||||||
|
}
|
||||||
|
|
||||||
Some(json!(vec_to_api(&mut dir_videos).await))
|
let dir_videos = library.get_directory_videos(dir).await;
|
||||||
|
|
||||||
|
let content = html!(
|
||||||
|
h1 class="text-center text-4xl font-extrabold leading-tight mt-4 mb-2" { (dir) };
|
||||||
|
div class="p-6" {
|
||||||
|
@for mut vid in dir_videos {
|
||||||
|
( video_element_wide(&mut vid).await );
|
||||||
|
};
|
||||||
|
};
|
||||||
|
);
|
||||||
|
|
||||||
|
render_page(htmx, content, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn index_page(library: &State<Library>) -> String {
|
pub async fn index_page(htmx: HTMX, library: &State<Library>) -> (Status, (ContentType, String)) {
|
||||||
unimplemented!()
|
let content = html!(
|
||||||
|
h1 class="text-center text-4xl font-extrabold leading-tight mt-4" { "Random Videos" };
|
||||||
|
div class="grid grid-cols-3 gap-6 p-6" {
|
||||||
|
@for mut vid in library.get_random_videos(3).await {
|
||||||
|
( video_element(&mut vid).await );
|
||||||
|
};
|
||||||
|
};
|
||||||
|
h1 class="text-center text-4xl font-extrabold leading-tight mt-8" { "Directories:" };
|
||||||
|
div class="flex p-10" {
|
||||||
|
@for dir in library.get_directories().await {
|
||||||
|
a class="px-3 py-2 m-2 bg-purple-500 text-white rounded-full cursor-pointer hover:bg-purple-600" href=(format!("/d/{dir}")) { (dir) };
|
||||||
|
br;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
);
|
||||||
|
|
||||||
|
render_page(htmx, content, "WatchDogs")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
use rocket::http::{ContentType, Status};
|
||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
pub mod watch;
|
||||||
pub mod yt;
|
pub mod yt;
|
||||||
|
|
||||||
/// A trait to generate a Model API representation in JSON format.
|
/// A trait to generate a Model API representation in JSON format.
|
||||||
|
@ -19,3 +22,10 @@ pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> {
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn api_response(json: &serde_json::Value) -> (Status, (ContentType, String)) {
|
||||||
|
(
|
||||||
|
Status::Ok,
|
||||||
|
(ContentType::JSON, serde_json::to_string(json).unwrap()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
53
src/pages/watch.rs
Normal file
53
src/pages/watch.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use maud::html;
|
||||||
|
use rocket::{
|
||||||
|
get,
|
||||||
|
http::{ContentType, Status},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
library::{self, Library},
|
||||||
|
pages::components::video_element,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
components::{render_page, HTMX},
|
||||||
|
vec_to_api,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[get("/watch?<v>")]
|
||||||
|
pub async fn watch_page(
|
||||||
|
htmx: HTMX,
|
||||||
|
library: &State<Library>,
|
||||||
|
v: String,
|
||||||
|
) -> (Status, (ContentType, String)) {
|
||||||
|
let video = if let Some(video) = library.get_video_by_id(&v).await {
|
||||||
|
video
|
||||||
|
} else {
|
||||||
|
// TODO : Error handling
|
||||||
|
library.get_video_by_youtube_id(&v).await.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = html!(
|
||||||
|
main class="container mx-auto mt-6 flex flex-col lg:flex-row gap-6" {
|
||||||
|
div class="lg:w-2/3 mt-10" {
|
||||||
|
div class="bg-black aspect-video rounded-lg overflow-hidden" {
|
||||||
|
video
|
||||||
|
controls
|
||||||
|
autoplay
|
||||||
|
class="w-full h-full" {
|
||||||
|
source src=(format!("/video/raw?v={}", video.id)) {
|
||||||
|
"Your browser does not support the video"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
div class="p-4 bg-stone-900 rounded-lg shadow-lg mt-8" {
|
||||||
|
h2 class="text-2xl font-semibold" { (video.title) };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
);
|
||||||
|
|
||||||
|
render_page(htmx, content, &format!("{} - WatchDogs", video.title))
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue