history feature
Some checks failed
ci/woodpecker/push/build Pipeline failed

This commit is contained in:
JMARyA 2024-12-22 20:19:52 +01:00
parent 95c4cf6fe1
commit a0e7c5d3c1
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
8 changed files with 111 additions and 23 deletions

2
Cargo.lock generated
View file

@ -158,7 +158,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "based"
version = "0.1.0"
source = "git+https://git.hydrar.de/jmarya/based#291949b8c65e90aaacead7d108f77366c742125a"
source = "git+https://git.hydrar.de/jmarya/based#a89c5661208585b2a9f09d5ee337ad1c60ea9a49"
dependencies = [
"bcrypt",
"chrono",

View file

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS video_history (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
video_id UUID NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
FOREIGN KEY (username) REFERENCES users (username) ON DELETE CASCADE,
FOREIGN KEY (video_id) REFERENCES videos (id) ON DELETE CASCADE
);

43
src/library/history.rs Normal file
View file

@ -0,0 +1,43 @@
use based::{auth::User, get_pg};
use super::Video;
pub trait VideoHistory {
async fn last_video(&self) -> Option<uuid::Uuid>;
async fn insert_history(&self, vid: uuid::Uuid);
async fn history_of(&self, n: i64) -> Vec<Video>;
}
impl VideoHistory for User {
async fn last_video(&self) -> Option<uuid::Uuid> {
let res: Option<(uuid::Uuid,)> = sqlx::query_as("SELECT video_id FROM video_history WHERE username = $1 ORDER BY timestamp DESC LIMIT 1")
.bind(&self.username)
.fetch_optional(get_pg!())
.await.unwrap();
res.map(|x| x.0)
}
async fn insert_history(&self, vid: uuid::Uuid) {
if let Some(prev) = self.last_video().await {
if prev == vid {
return;
}
}
sqlx::query("INSERT INTO video_history (username, video_id) VALUES ($1, $2)")
.bind(&self.username)
.bind(vid)
.execute(get_pg!())
.await
.unwrap();
}
async fn history_of(&self, n: i64) -> Vec<Video> {
sqlx::query_as("SELECT v.* FROM video_history vh JOIN videos v ON vh.video_id = v.id WHERE vh.username = $1 ORDER BY vh.timestamp DESC LIMIT $2")
.bind(&self.username)
.bind(n)
.fetch_all(get_pg!())
.await.unwrap()
}
}

View file

@ -1,3 +1,5 @@
use based::auth::User;
use based::get_pg;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
@ -9,6 +11,7 @@ pub use video::Video;
use crate::meta;
mod func;
pub mod history;
mod video;
#[derive(Debug, Clone)]

View file

@ -66,7 +66,8 @@ async fn launch() -> _ {
pages::index::index_page,
pages::watch::watch_page,
pages::user::login,
pages::user::login_post
pages::user::login_post,
pages::user::history_page
],
)
.attach(cors)

View file

@ -13,30 +13,38 @@ pub async fn render_page(
title: &str,
user: Option<User>,
) -> StringResponse {
based::page::render_page(content, title, ctx, &based::page::Shell::new(
html! {
script src="https://cdn.tailwindcss.com" {};
script src="/assets/htmx.min.js" {};
meta name="viewport" content="width=device-width, initial-scale=1.0";
},
html! {
header class="bg-gray-800 text-white shadow-md py-2" {
(script(include_str!("../scripts/header.js")));
based::page::render_page(
content,
title,
ctx,
&based::page::Shell::new(
html! {
script src="https://cdn.tailwindcss.com" {};
script src="/assets/htmx.min.js" {};
meta name="viewport" content="width=device-width, initial-scale=1.0";
},
html! {
header class="bg-gray-800 text-white shadow-md py-2" {
(script(include_str!("../scripts/header.js")));
div class="flex justify-between px-6" {
a href="/" class="flex items-center space-x-2" {
img src="/favicon" alt="Logo" class="w-10 h-10 rounded-md";
span class="font-semibold text-xl" { "WatchDogs" };
};
div class="flex justify-between px-6" {
a href="/" class="flex items-center space-x-2" {
img src="/favicon" alt="Logo" class="w-10 h-10 rounded-md";
span class="font-semibold text-xl" { "WatchDogs" };
};
@if user.is_some() {
p { (user.unwrap().username) };
};
@if user.is_some() {
p { (user.unwrap().username) };
};
};
};
};
}, Some(String::from("bg-black text-white")))).await
};
},
Some(String::from("bg-black text-white")),
),
)
.await
}
pub fn loading_spinner() -> PreEscaped<String> {

View file

@ -5,6 +5,9 @@ use maud::html;
use rocket::http::CookieJar;
use rocket::{form::Form, get, http::Cookie, post, response::Redirect, FromForm};
use crate::library::history::VideoHistory;
use crate::pages::components::video_element_wide;
use super::components::render_page;
#[get("/login")]
@ -43,3 +46,17 @@ pub async fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) ->
Some(Redirect::to("/"))
}
#[get("/history")]
pub async fn history_page(ctx: RequestContext, user: User) -> StringResponse {
let content = html! {
h1 class="text-center text-4xl font-extrabold leading-tight mt-4 mb-2" { "History" };
div class="p-6" {
@for mut vid in user.history_of(10).await {
( video_element_wide(&mut vid).await );
};
};
};
render_page(ctx, content, "History", Some(user)).await
}

View file

@ -6,7 +6,10 @@ use based::{
use maud::html;
use rocket::{get, State};
use crate::{library::Library, pages::components::video_element_wide};
use crate::{
library::{history::VideoHistory, Library},
pages::components::video_element_wide,
};
use super::components::render_page;
@ -24,6 +27,10 @@ pub async fn watch_page(
library.get_video_by_youtube_id(&v).await.unwrap()
};
if let Some(user) = user.user() {
user.insert_history(video.id).await;
}
let content = html!(
main class="container mx-auto mt-6 flex flex-col lg:flex-row gap-6" {
div class="lg:w-10/12 mt-10" {