use axum::{ extract::{Form, Path, Query, State}, http::StatusCode, response::{IntoResponse, Redirect, Response}, }; use serde::Deserialize; use sqlx::{query, query_as, SqlitePool}; use super::templates::{AddNewWatchPage, GetWatchPage, SearchWatchesPage}; use crate::{ db_id::DbId, util::{empty_string_as_none, year_to_epoch}, AuthContext, MyWatchesPage, ShowKind, Watch, WatchQuest, }; //-************************************************************************ // Constants //-************************************************************************ const GET_SAVED_WATCHES_QUERY: &str = "select * from watches left join witch_watch on $1 = witch_watch.witch and watches.id = witch_watch.watch"; const GET_WATCH_QUERY: &str = "select * from watches where id = $1"; const ADD_WATCH_QUERY: &str = "insert into watches (id, title, kind, release_date, metadata_url, added_by) values ($1, $2, $3, $4, $5, $6)"; const ADD_WITCH_WATCH_QUERY: &str = "insert into witch_watch (id, witch, watch, public, watched) values ($1, $2, $3, $4, $5)"; const EMPTY_SEARCH_QUERY_STRUCT: SearchQuery = SearchQuery { title: None, kind: None, year: None, search: None, }; //-************************************************************************ // Error types for Watch creation //-************************************************************************ #[Error] pub struct WatchAddError(#[from] WatchAddErrorKind); #[Error] #[non_exhaustive] pub enum WatchAddErrorKind { UnknownDBError, NotSignedIn, } impl IntoResponse for WatchAddError { fn into_response(self) -> Response { match &self.0 { WatchAddErrorKind::UnknownDBError => { (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() } WatchAddErrorKind::NotSignedIn => ( StatusCode::OK, "Ope, you need to sign in first!".to_string(), ) .into_response(), } } } //-************************************************************************ // Types for receiving arguments from forms //-************************************************************************ #[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] pub struct SearchQuery { #[serde(default, deserialize_with = "empty_string_as_none")] pub search: Option, #[serde(default, deserialize_with = "empty_string_as_none")] pub title: Option, #[serde(default, deserialize_with = "empty_string_as_none")] pub kind: Option, #[serde(default, deserialize_with = "empty_string_as_none")] pub year: Option, } // kinda the main form? #[derive(Debug, Default, Deserialize, PartialEq, Eq)] pub struct PostAddNewWatch { pub title: String, #[serde(default)] pub private: bool, pub kind: ShowKind, #[serde(default, deserialize_with = "empty_string_as_none")] pub year: Option, // need a date-picker or something #[serde(default, deserialize_with = "empty_string_as_none")] pub metadata_url: Option, #[serde(default)] pub watched_already: bool, } #[derive(Debug, Default, Deserialize, PartialEq, Eq)] pub struct PostAddExistingWatch { pub id: String, pub public: bool, pub watched_already: bool, } //-************************************************************************ // handlers //-************************************************************************ pub async fn get_add_new_watch(auth: AuthContext) -> impl IntoResponse { AddNewWatchPage { user: auth.current_user, } } /// Add a Watch to your watchlist (side effects system-add) pub async fn post_add_new_watch( auth: AuthContext, State(pool): State, Form(form): Form, ) -> Result { if let Some(user) = auth.current_user { { let watch_id = DbId::new(); let witch_watch_id = DbId::new(); let release_date = year_to_epoch(form.year.as_deref()); let watch = Watch { id: watch_id, title: form.title, kind: form.kind, metadata_url: form.metadata_url, length: None, release_date, added_by: user.id, }; let quest = WatchQuest { id: witch_watch_id, user: user.id, watch: watch_id, is_public: !form.private, already_watched: form.watched_already, }; add_new_watch_impl(&pool, &watch, Some(quest)).await?; let location = format!("/watch/{watch_id}"); Ok(Redirect::to(&location)) } } else { Err(WatchAddErrorKind::NotSignedIn.into()) } } pub(crate) async fn add_new_watch_impl( db_pool: &SqlitePool, watch: &Watch, quest: Option, ) -> Result<(), WatchAddError> { let mut tx = db_pool .begin() .await .map_err(|_| WatchAddErrorKind::UnknownDBError)?; query(ADD_WATCH_QUERY) .bind(watch.id) .bind(&watch.title) .bind(watch.kind) .bind(watch.release_date) .bind(&watch.metadata_url) .bind(watch.added_by) .execute(&mut tx) .await .map_err(|err| { tracing::error!("Got error: {err}"); WatchAddErrorKind::UnknownDBError })?; if let Some(quest) = quest { query(ADD_WITCH_WATCH_QUERY) .bind(quest.id) .bind(quest.user) .bind(quest.watch) .bind(quest.is_public) .bind(quest.already_watched) .execute(&mut tx) .await .map_err(|err| { tracing::error!("Got error: {err}"); WatchAddErrorKind::UnknownDBError })?; } tx.commit().await.map_err(|err| { tracing::error!("Got error: {err}"); WatchAddErrorKind::UnknownDBError })?; Ok(()) } /// Add a Watch to your watchlist by selecting it with a checkbox pub async fn post_add_watch_quest( _auth: AuthContext, State(_pool): State, Form(_form): Form, ) -> impl IntoResponse { todo!() } pub async fn add_watch_quest_impl(pool: &SqlitePool, quest: &WatchQuest) -> Result<(), ()> { query(ADD_WITCH_WATCH_QUERY) .bind(quest.id) .bind(quest.user) .bind(quest.watch) .bind(quest.is_public) .bind(quest.already_watched) .execute(pool) .await .map_err(|err| { tracing::error!("Got error: {err}"); })?; Ok(()) } /// A single Watch pub async fn get_watch( auth: AuthContext, watch: Option>, State(pool): State, ) -> impl IntoResponse { let id = if let Some(Path(id)) = watch { id } else { "".to_string() }; let id = id.trim(); let id = DbId::from_string(id).unwrap_or_default(); let q = query_as(GET_WATCH_QUERY).bind(id); let watch: Option = q.fetch_one(&pool).await.ok(); GetWatchPage { watch, user: auth.current_user, } } /// everything the user has saved pub async fn get_watches(auth: AuthContext, State(pool): State) -> impl IntoResponse { let user = auth.current_user; let watches: Vec = if (user).is_some() { query_as(GET_SAVED_WATCHES_QUERY) .bind(user.as_ref().unwrap().id) .fetch_all(&pool) .await .unwrap_or_default() } else { vec![] }; MyWatchesPage { watches, user } } pub async fn get_search_watch( auth: AuthContext, State(pool): State, search: Query, ) -> impl IntoResponse { let user = auth.current_user; let (search_string, qstring) = if search.0 != EMPTY_SEARCH_QUERY_STRUCT { let s = search.0; let q = if let Some(title) = &s.title { title } else if let Some(search) = &s.search { search } else { "" }; (format!("{s:?}"), format!("%{}%", q.trim())) } else { ("".to_string(), "%".to_string()) }; // until tantivy search let watches: Vec = query_as("select * from watches where title like ?") .bind(&qstring) .fetch_all(&pool) .await .unwrap(); SearchWatchesPage { watches, user, search: search_string, } }