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::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use func::is_video_file;
|
||||
|
@ -96,7 +97,7 @@ impl Library {
|
|||
|
||||
pub async fn get_video_by_id(&self, id: &str) -> Option<Video> {
|
||||
sqlx::query_as("SELECT * FROM videos WHERE id = $1")
|
||||
.bind(id)
|
||||
.bind(uuid::Uuid::from_str(id).unwrap())
|
||||
.fetch_optional(&self.conn)
|
||||
.await
|
||||
.unwrap()
|
||||
|
|
|
@ -76,7 +76,9 @@ async fn launch() -> _ {
|
|||
pages::index::channel_page,
|
||||
pages::yt::yt_tags,
|
||||
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)
|
||||
|
|
|
@ -1,20 +1,66 @@
|
|||
use core::num;
|
||||
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
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> {
|
||||
html! {
|
||||
html {
|
||||
head {
|
||||
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 {
|
||||
(content)
|
||||
body class="bg-black text-white" {
|
||||
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> {
|
||||
html! {
|
||||
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!(
|
||||
@let desc = video.youtube_meta().await.map(|x| x.description);
|
||||
@let video_id = video.id;
|
||||
article class="container-fluid" style="margin: 50px; cursor: pointer;" {
|
||||
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;" {
|
||||
h2 style="margin: 0; font-size: 18px;" { (video.title) };
|
||||
@if !desc.as_ref().map(|x| x.is_empty()).unwrap_or(true) {
|
||||
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>()) };
|
||||
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" {
|
||||
div class="flex-shrink-0" {
|
||||
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) alt="Video Thumbnail" class="w-48 h-32 object-cover rounded-md";
|
||||
};
|
||||
|
||||
div class="flex flex-col flex-grow ml-4" {
|
||||
h3 class="text-lg font-semibold truncate mb-1" {
|
||||
( video.title )
|
||||
};
|
||||
|
||||
@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 rocket::{get, State};
|
||||
use rocket::{
|
||||
get,
|
||||
http::{ContentType, Status},
|
||||
State,
|
||||
};
|
||||
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>")]
|
||||
pub async fn search(
|
||||
|
@ -23,13 +31,49 @@ pub async fn search(
|
|||
}
|
||||
|
||||
#[get("/d/<dir>")]
|
||||
pub async fn channel_page(dir: &str, library: &State<Library>) -> Option<serde_json::Value> {
|
||||
let mut dir_videos = library.get_directory_videos(dir).await;
|
||||
pub async fn channel_page(
|
||||
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("/")]
|
||||
pub async fn index_page(library: &State<Library>) -> String {
|
||||
unimplemented!()
|
||||
pub async fn index_page(htmx: HTMX, library: &State<Library>) -> (Status, (ContentType, String)) {
|
||||
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 components;
|
||||
pub mod index;
|
||||
pub mod watch;
|
||||
pub mod yt;
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
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