checkpoint merge from watches
This commit is contained in:
commit
5ded14d958
6 changed files with 111 additions and 61 deletions
|
@ -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,
|
||||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -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(),
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue