checkpoint merge from watches

This commit is contained in:
Joe Ardent 2023-06-18 12:06:33 -07:00
commit 5ded14d958
6 changed files with 111 additions and 61 deletions

View File

@ -18,7 +18,7 @@ create table if not exists witches (
-- table of things to watch -- table of things to watch
create table if not exists watches ( create table if not exists watches (
id blob not null primary key, id blob not null primary key,
typ int not null, -- enum for movie or tv show or whatev kind int not null, -- enum for movie or tv show or whatev
title text not null, title text not null,
metadata_url text, -- possible url for imdb or other metadata-esque site to show the user metadata_url text, -- possible url for imdb or other metadata-esque site to show the user
length int, length int,

View File

@ -38,9 +38,7 @@ pub async fn app(db_pool: sqlx::SqlitePool, session_secret: &[u8]) -> axum::Rout
use generic_handlers::{handle_slash, handle_slash_redir}; use generic_handlers::{handle_slash, handle_slash_redir};
use login::{get_login, get_logout, post_login, post_logout}; use login::{get_login, get_logout, post_login, post_logout};
use signup::{get_create_user, handle_signup_success, post_create_user}; use signup::{get_create_user, handle_signup_success, post_create_user};
use watches::handlers::{ use watches::handlers::{get_search_watch, get_watches, post_add_watch};
get_search_watch, get_watches, post_add_watch, post_search_watch, put_add_watch,
};
axum::Router::new() axum::Router::new()
.route("/", get(handle_slash).post(handle_slash)) .route("/", get(handle_slash).post(handle_slash))
@ -51,13 +49,8 @@ pub async fn app(db_pool: sqlx::SqlitePool, session_secret: &[u8]) -> axum::Rout
.route("/watches", get(get_watches)) .route("/watches", get(get_watches))
.route("/watch", get(get_watch)) .route("/watch", get(get_watch))
.route("/watch/:id", get(get_watch)) .route("/watch/:id", get(get_watch))
.route("/search", get(get_search_watch).post(post_search_watch)) .route("/search", get(get_search_watch))
.route( .route("/add", get(get_search_watch).post(post_add_watch))
"/add",
get(get_search_watch)
.put(put_add_watch)
.post(post_add_watch),
)
.fallback(handle_slash_redir) .fallback(handle_slash_redir)
.layer(middleware::from_fn_with_state( .layer(middleware::from_fn_with_state(
db_pool.clone(), db_pool.clone(),

View File

@ -3,22 +3,29 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use serde::Deserialize; use serde::{de, Deserialize, Deserializer};
use sqlx::{query_as, SqlitePool}; use sqlx::{query_as, SqlitePool};
use uuid::Uuid; use uuid::Uuid;
use super::templates::{GetSearchWatches, GetWatch}; use super::templates::{GetSearchWatches, GetWatch};
use crate::{AuthContext, GetWatches, ShowKind, User, Watch}; use crate::{AuthContext, GetWatches, ShowKind, Watch};
//-************************************************************************ //-************************************************************************
// Constants // Constants
//-************************************************************************ //-************************************************************************
const GET_WATCHES_QUERY: &str = const GET_SAVED_WATCHES_QUERY: &str =
"select * from watches left join witch_watch on $1 = witch_watch.witch and watches.id = witch_watch.watch"; "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 GET_WATCH_QUERY: &str = "select * from watches where id = $1";
const EMPTY_SEARCH_QUERY: SearchQuery = SearchQuery {
title: None,
kind: None,
year: None,
search: None,
};
//-************************************************************************ //-************************************************************************
// Error types for Watch creation // Error types for Watch creation
//-************************************************************************ //-************************************************************************
@ -46,39 +53,39 @@ impl IntoResponse for WatchAddError {
// Types for receiving arguments from search queries // Types for receiving arguments from search queries
//-************************************************************************ //-************************************************************************
#[derive(Debug, Default, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
pub struct SimpleSearchQuery { pub struct SearchQuery {
search: String, #[serde(default, deserialize_with = "empty_string_as_none")]
} pub search: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
#[derive(Debug, Default, Clone, Deserialize)]
pub struct FullSearchQuery {
pub title: Option<String>, pub title: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub kind: Option<String>, pub kind: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub year: Option<i64>, pub year: Option<i64>,
} }
#[derive(Debug, Clone, Deserialize)] // kinda the main form?
pub enum SearchQuery { #[derive(Debug, Default, Deserialize, PartialEq, Eq)]
Full(FullSearchQuery), pub struct PostAddWatch {
Simple(SimpleSearchQuery), pub id: Option<String>, // maybe this already exists
} pub title: String,
pub kind: Option<ShowKind>,
impl Default for SearchQuery { pub release_date: Option<String>, // need a date-picker or something
fn default() -> Self { pub metadata_url: Option<String>,
SearchQuery::Simple(SimpleSearchQuery::default())
}
} }
//-************************************************************************ //-************************************************************************
// handlers // handlers
//-************************************************************************ //-************************************************************************
/// Add a new Watch to the whole system (also adds to your watchlist) /// Add a Watch to your watchlist (side effects system-add if missing)
pub async fn put_add_watch() {} pub async fn post_add_watch(
auth: AuthContext,
/// Add a Watch to your watchlist State(pool): State<SqlitePool>,
pub async fn post_add_watch() {} Form(form): Form<PostAddWatch>,
) -> impl IntoResponse {
}
/// A single Watch /// A single Watch
pub async fn get_watch( pub async fn get_watch(
@ -92,7 +99,7 @@ pub async fn get_watch(
"".to_string() "".to_string()
}; };
let id = id.trim(); let id = id.trim();
let id = Uuid::try_parse(id).unwrap_or_default();
let watch: Option<Watch> = query_as(GET_WATCH_QUERY) let watch: Option<Watch> = query_as(GET_WATCH_QUERY)
.bind(id) .bind(id)
.fetch_one(&pool) .fetch_one(&pool)
@ -109,7 +116,7 @@ pub async fn get_watch(
pub async fn get_watches(auth: AuthContext, State(pool): State<SqlitePool>) -> impl IntoResponse { pub async fn get_watches(auth: AuthContext, State(pool): State<SqlitePool>) -> impl IntoResponse {
let user = auth.current_user; let user = auth.current_user;
let watches: Vec<Watch> = if (user).is_some() { let watches: Vec<Watch> = if (user).is_some() {
query_as(GET_WATCHES_QUERY) query_as(GET_SAVED_WATCHES_QUERY)
.bind(user.as_ref().unwrap().id) .bind(user.as_ref().unwrap().id)
.fetch_all(&pool) .fetch_all(&pool)
.await .await
@ -123,27 +130,47 @@ pub async fn get_watches(auth: AuthContext, State(pool): State<SqlitePool>) -> i
pub async fn get_search_watch( pub async fn get_search_watch(
auth: AuthContext, auth: AuthContext,
State(_pool): State<SqlitePool>, State(pool): State<SqlitePool>,
search: Option<Query<SearchQuery>>, search: Query<SearchQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
use SearchQuery::*;
let search = match search {
Some(Query(Simple(SimpleSearchQuery { search }))) => search,
Some(Query(Full(q))) => {
// obviously this is dumb
format!("{q:?}")
}
None => "".to_owned(),
};
let search = search.trim().to_string();
let user = auth.current_user; let user = auth.current_user;
let search = if search.0 != EMPTY_SEARCH_QUERY {
let s = search.0;
format!("{s:?}")
} else {
"".to_string()
};
// until tantivy search
let watches: Vec<Watch> = query_as("select * from watches")
.fetch_all(&pool)
.await
.unwrap_or_default();
GetSearchWatches { GetSearchWatches {
watches: vec![], watches,
user, user,
search, search,
} }
} }
pub async fn post_search_watch() {} //-************************************************************************
// helper fns
//-************************************************************************
/// Serde deserialization decorator to map empty Strings to None,
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let opt = Option::<String>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => std::str::FromStr::from_str(s)
.map_err(de::Error::custom)
.map(Some),
}
}

View File

@ -7,7 +7,7 @@ pub mod templates;
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type,
)] )]
#[repr(i32)] #[repr(i64)]
pub enum ShowKind { pub enum ShowKind {
Movie = 0, Movie = 0,
Series = 1, Series = 1,
@ -18,7 +18,14 @@ pub enum ShowKind {
impl std::fmt::Display for ShowKind { impl std::fmt::Display for ShowKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
todo!() let repr = match self {
Self::Movie => "movie",
Self::Series => "series",
Self::LimitedSeries => "limited series",
Self::Short => "short form",
Self::Unknown => "unknown",
};
write!(f, "{repr}")
} }
} }
@ -28,8 +35,8 @@ impl Default for ShowKind {
} }
} }
impl From<i32> for ShowKind { impl From<i64> for ShowKind {
fn from(value: i32) -> Self { fn from(value: i64) -> Self {
match value { match value {
0 => Self::Movie, 0 => Self::Movie,
1 => Self::Series, 1 => Self::Series,
@ -59,7 +66,7 @@ pub struct Watch {
pub kind: ShowKind, pub kind: ShowKind,
pub title: String, pub title: String,
pub metadata_url: Option<String>, pub metadata_url: Option<String>,
pub length: Option<i32>, pub length: Option<i64>,
pub release_date: Option<i64>, pub release_date: Option<i64>,
added_by: Uuid, // this shouldn't be exposed to randos added_by: Uuid, // this shouldn't be exposed to randos
created_at: i64, created_at: i64,

View File

@ -1,7 +1,7 @@
use askama::Template; use askama::Template;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{OptionalOptionalUser, User, Watch}; use crate::{OptionalOptionalUser, ShowKind, User, Watch};
#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)]
#[template(path = "get_watches.html")] #[template(path = "get_watches.html")]

View File

@ -16,12 +16,35 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<p> <h2>Simple Search</h2>
<div class="simplesearch">
<form action="/search" enctype="application/x-www-form-urlencoded" method="get"> <form action="/search" enctype="application/x-www-form-urlencoded" method="get">
<label for="search">Looking for something else to watch?</label> <label for="search">Looking for something else to watch?</label>
<input type="text" name="search" id="search"></br> <input type="text" name="search" id="search"></br>
<input type="submit" value="Let's go!">
</div>
<h2>Fussy Search</h2>
<div class="fullsearch">
<label for="title">Title</label>
<input type="text" name="title" id="title"></br>
<label for="year">Release Year</label>
<input type="text" name="year" id="year"></br>
<label for="kind">Type</label>
<select id="kind" name="kind">
<option value="">Unknown</option>
<option value="0">Movie</option>
<option value="1">Series</option>
<option value="2">Limited Series</option>
<option value="3">Short</option>
<option value="4">Other</option>
</select>
<input type="submit" value="Let's go!"> <input type="submit" value="Let's go!">
</form> </form>
</p>
</div>
{% endblock %} {% endblock %}