diff options
author | metamuffin <metamuffin@disroot.org> | 2023-12-16 01:08:15 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-12-16 01:08:15 +0100 |
commit | af99c406af8ee47bee38708cf23e86af826e41ba (patch) | |
tree | 23498a36f813454c2edea46906f812d929ba792e /server/src/routes | |
parent | 21b58037c69798e922c5512ea5380943781558ff (diff) | |
download | jellything-af99c406af8ee47bee38708cf23e86af826e41ba.tar jellything-af99c406af8ee47bee38708cf23e86af826e41ba.tar.bz2 jellything-af99c406af8ee47bee38708cf23e86af826e41ba.tar.zst |
watch progress and some draft ui
Diffstat (limited to 'server/src/routes')
-rw-r--r-- | server/src/routes/mod.rs | 3 | ||||
-rw-r--r-- | server/src/routes/progress.rs | 75 | ||||
-rw-r--r-- | server/src/routes/ui/browser.rs | 21 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 67 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 101 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 1 | ||||
-rw-r--r-- | server/src/routes/ui/sort.rs | 25 |
7 files changed, 187 insertions, 106 deletions
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 9211a1e..e95d714 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -8,7 +8,7 @@ use api::{r_api_account_login, r_api_node_raw, r_api_root, r_api_version}; use base64::Engine; use jellybase::CONF; use log::warn; -use progress::r_player_progress; +use progress::{r_player_progress, r_player_watched}; use rand::random; use rocket::{ catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, get, http::Header, routes, Build, @@ -95,6 +95,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> r_stream, r_player, r_player_progress, + r_player_watched, r_account_login, r_account_login_post, r_account_register, diff --git a/server/src/routes/progress.rs b/server/src/routes/progress.rs index 170ed97..0ba5b31 100644 --- a/server/src/routes/progress.rs +++ b/server/src/routes/progress.rs @@ -1,28 +1,71 @@ -use std::collections::HashMap; - use super::ui::{account::session::Session, error::MyResult}; +use crate::routes::ui::node::rocket_uri_macro_r_library_node; +use anyhow::anyhow; use jellybase::database::Database; -use rocket::{post, State}; +use jellycommon::user::WatchedState; +use rocket::{post, response::Redirect, State}; + +#[post("/n/<id>/watched?<state>")] +pub async fn r_player_watched( + session: Session, + db: &State<Database>, + id: &str, + state: bool, +) -> MyResult<Redirect> { + db.node + .get(&id.to_string())? + .ok_or(anyhow!("node does not exist"))?; + + let key = (session.user.name.clone(), id.to_owned()); + + db.user_node.fetch_and_update(&key, |t| { + let mut t = t.unwrap_or_default(); + if state { + t.watched = WatchedState::Watched + } else { + t.watched = WatchedState::None + } + Some(t) + })?; + + Ok(Redirect::found(rocket::uri!(r_library_node(id)))) +} #[post("/n/<id>/progress?<t>")] pub async fn r_player_progress( session: Session, db: &State<Database>, id: &str, - t: Option<f64>, + t: f64, ) -> MyResult<()> { - db.user_progess.fetch_and_update(&session.user.name, |p| { - let mut m = p.unwrap_or_else(|| HashMap::new()); - if let Some(t) = t { - m.insert(id.to_string(), t); - } else { - m.remove(&id.to_string()); - } - if m.is_empty() { - None - } else { - Some(m) - } + db.node + .get(&id.to_string())? + .ok_or(anyhow!("node does not exist"))?; + + let key = (session.user.name.clone(), id.to_owned()); + db.user_node.fetch_and_update(&key, |d| { + let mut d = d.unwrap_or_default(); + d.watched = match d.watched { + WatchedState::None | WatchedState::Progress(_) => WatchedState::Progress(t), + WatchedState::Watched => WatchedState::Watched, + }; + Some(d) })?; + + // db.user_progess.fetch_and_update(&session.user.name, |p| { + // let mut m = p.unwrap_or_else(|| HashMap::new()); + // if let Some(t) = t { + // if m.len() < 16 { + // m.insert(id.to_string(), t); + // } + // } else { + // m.remove(&id.to_string()); + // } + // if m.is_empty() { + // None + // } else { + // Some(m) + // } + // })?; Ok(()) } diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs index d2c24bc..509f242 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/routes/ui/browser.rs @@ -12,7 +12,7 @@ use super::{ }; use crate::{database::Database, uri}; use anyhow::Context; -use jellycommon::NodePublic; +use jellycommon::{user::NodeUserData, NodePublic}; use rocket::{get, State}; /// This function is a stub and only useful for use in the uri! macro. @@ -21,7 +21,7 @@ pub fn r_all_items() {} #[get("/items?<page>&<filter..>")] pub fn r_all_items_filter( - _sess: Session, + sess: Session, db: &State<Database>, page: Option<usize>, filter: NodeFilterSort, @@ -29,11 +29,18 @@ pub fn r_all_items_filter( let mut items = db .node .iter() - .map(|e| e.context("listing")) + .map(|e| { + let (i, n) = e.context("listing")?; + let u = db + .user_node + .get(&(sess.user.name.clone(), i.clone()))? + .unwrap_or_default(); + Ok((i, n, u)) + }) .collect::<anyhow::Result<Vec<_>>>()? .into_iter() - .map(|(k, n)| (k, n.public)) - .collect::<Vec<(String, NodePublic)>>(); + .map(|(k, n, u)| (k, n.public, u)) + .collect::<Vec<(String, NodePublic, NodeUserData)>>(); filter_and_sort_nodes(&filter, &mut items); @@ -50,8 +57,8 @@ pub fn r_all_items_filter( .page.dir { h1 { "All Items" } @NodeFilterSortForm { f: &filter } - ul.children { @for (id, node) in &items[from..to] { - li {@NodeCard { id, node: &node }} + ul.children { @for (id, node, udata) in &items[from..to] { + li {@NodeCard { id, node, udata }} }} p.pagecontrols { span.current { "Page " @{page + 1} " of " @max_page " " } diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index 3f1f0d0..bcbc847 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -3,7 +3,11 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -use super::{account::session::Session, layout::LayoutPage, node::NodeCard}; +use super::{ + account::session::Session, + layout::LayoutPage, + node::{DatabaseNodeUserDataExt, NodeCard}, +}; use crate::{ database::Database, routes::ui::{error::MyResult, layout::DynLayoutPage}, @@ -11,7 +15,10 @@ use crate::{ use anyhow::Context; use chrono::{Datelike, Utc}; use jellybase::CONF; -use jellycommon::NodePublic; +use jellycommon::{ + user::{NodeUserData, WatchedState}, + NodePublic, +}; use rocket::{get, State}; use tokio::fs::read_to_string; @@ -20,11 +27,18 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { let mut items = db .node .iter() - .map(|e| e.context("listing")) + .map(|e| { + let (i, n) = e.context("listing")?; + let u = db + .user_node + .get(&(sess.user.name.clone(), i.clone()))? + .unwrap_or_default(); + Ok((i, n, u)) + }) .collect::<anyhow::Result<Vec<_>>>()? .into_iter() - .map(|(k, n)| (k, n.public)) - .collect::<Vec<(String, NodePublic)>>(); + .map(|(k, n, u)| (k, n.public, u)) + .collect::<Vec<(String, NodePublic, NodeUserData)>>(); let random = (0..16) .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone())) @@ -37,17 +51,12 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { .public .children .into_iter() - .map(|n| { - Ok(( - n.clone(), - db.node.get(&n)?.context("child does not exist")?.public, - )) - }) + .map(|n| db.get_node_with_userdata(&n, &sess)) .collect::<anyhow::Result<Vec<_>>>()? .into_iter() .collect::<Vec<_>>(); - items.sort_by_key(|(_, n)| { + items.sort_by_key(|(_, n, _)| { n.release_date .map(|d| -d.naive_utc().timestamp()) .unwrap_or(i64::MAX) @@ -59,20 +68,10 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { .map(|k| k.to_owned()) .collect::<Vec<_>>(); - let continue_watching = db - .user_progess - .get(&sess.user.name)? - .unwrap_or_default() - .into_iter() - .map(|(n, p)| { - Ok(( - n.clone(), - db.node.get(&n)?.context("child does not exist")?.public, - p, - )) - }) - .collect::<anyhow::Result<Vec<_>>>()? - .into_iter() + let continue_watching = items + .iter() + .filter(|(_, _, u)| matches!(u.watched, WatchedState::Progress(_))) + .map(|k| k.to_owned()) .collect::<Vec<_>>(); Ok(LayoutPage { @@ -80,22 +79,22 @@ pub fn r_home(sess: Session, db: &State<Database>) -> MyResult<DynLayoutPage> { content: markup::new! { p { "Welcome back " @sess.user.display_name } h2 { "Explore " @CONF.brand } - .homelist { ul {@for (id, node) in &toplevel { - li { @NodeCard { id, node } } + .homelist { ul {@for (id, node, udata) in &toplevel { + li { @NodeCard { id, node, udata } } }}} @if !continue_watching.is_empty() { h2 { "Continue Watching" } - .homelist { ul {@for (id, node, _p) in &continue_watching { - li { @NodeCard { id, node } } + .homelist { ul {@for (id, node, udata) in &continue_watching { + li { @NodeCard { id, node, udata } } }}} } h2 { "Latest Releases" } - .homelist { ul {@for (id, node) in &latest { - li { @NodeCard { id, node } } + .homelist { ul {@for (id, node, udata) in &latest { + li { @NodeCard { id, node, udata } } }}} h2 { "Today's Picks" } - .homelist { ul {@for (id, node) in &random { - li { @NodeCard { id, node } } + .homelist { ul {@for (id, node, udata) in &random { + li { @NodeCard { id, node, udata } } }}} p.error { "TODO: recently added" } p.error { "TODO: best rating" } diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index cc31de2..2932bf3 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -12,6 +12,7 @@ use crate::{ database::Database, routes::{ api::AcceptJson, + progress::rocket_uri_macro_r_player_watched, ui::{ account::session::Session, assets::AssetRole, @@ -21,9 +22,12 @@ use crate::{ }, uri, }; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, Context, Result}; use jellybase::permission::NodePermissionExt; -use jellycommon::{MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind}; +use jellycommon::{ + user::{NodeUserData, WatchedState}, + MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind, +}; use rocket::{get, serde::json::Json, Either, State}; /// This function is a stub and only useful for use in the uri! macro. @@ -48,6 +52,11 @@ pub async fn r_library_node_filter<'a>( .ok_or(anyhow!("node does not exist"))? .public; + let udata = db + .user_node + .get(&(session.user.name.clone(), id.to_string()))? + .unwrap_or_default(); + if *aj { return Ok(Either::Right(Json(node))); } @@ -55,34 +64,24 @@ pub async fn r_library_node_filter<'a>( let mut children = node .children .iter() - .map(|c| { - Ok(( - c.to_owned(), - db.node - .get(c)? - .ok_or(anyhow!("child does not exist: {c}"))? - .public, - )) - }) + .map(|c| Ok(db.get_node_with_userdata(c, &session)?)) .collect::<anyhow::Result<Vec<_>>>()? .into_iter() .collect(); filter_and_sort_nodes(&filter, &mut children); - // node.media.unwrap().tracks[0]. - Ok(Either::Left(LayoutPage { title: node.title.to_string(), content: markup::new! { - @NodePage { node: &node, id: &id, children: &children, filter: &filter } + @NodePage { node: &node, id: &id, udata: &udata, children: &children, filter: &filter } }, ..Default::default() })) } markup::define! { - NodeCard<'a>(id: &'a str, node: &'a NodePublic) { + NodeCard<'a>(id: &'a str, node: &'a NodePublic, udata: &'a NodeUserData) { @let cls = format!("node card poster {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"}); div[class=cls] { .poster { @@ -93,22 +92,8 @@ markup::define! { @if !(matches!(node.kind, NodeKind::Collection | NodeKind::Channel)) { a.play.icon[href=&uri!(r_player(id, PlayerConfig::default()))] { "play_arrow" } } - @Props { node } + @Props { node, udata } } - // .inner { - // a[href=uri!(r_library_node(id))] { - // img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))]; - // } - // div.details { - // h3 { @node.title } - // p.description { @node.description } - // @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { - // a[href=&uri!(r_library_node(id))] { "Open" } - // } else { - // a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" } - // } - // } - // } } div.title { a[href=uri!(r_library_node(id))] { @@ -117,7 +102,7 @@ markup::define! { } } } - NodePage<'a>(id: &'a str, node: &'a NodePublic, children: &'a Vec<(String, NodePublic)>, filter: &'a NodeFilterSort) { + NodePage<'a>(id: &'a str, node: &'a NodePublic, udata: &'a NodeUserData, children: &'a Vec<(String, NodePublic, NodeUserData)>, filter: &'a NodeFilterSort) { @if !matches!(node.kind, NodeKind::Collection) { img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop, Some(2048)))]; } @@ -128,9 +113,22 @@ markup::define! { .title { h1 { @node.title } @if node.media.is_some() { a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" }} + @match udata.watched { + WatchedState::None | + WatchedState::Progress(_) => { + form.mark_watched[method="POST", action=uri!(r_player_watched(id, true))] { + input[type="submit", value="Mark Watched"]; + } + } + WatchedState::Watched => { + form.mark_unwatched[method="POST", action=uri!(r_player_watched(id, false))] { + input[type="submit", value="Mark Unwatched"]; + } + } + } } .details { - @Props { node } + @Props { node, udata } h3 { @node.tagline } @if let Some(description) = &node.description { p { @for line in description.lines() { @line br; } } @@ -154,12 +152,12 @@ markup::define! { } @match node.kind { NodeKind::Collection | NodeKind::Channel => { - ul.children {@for (id, node) in children.iter() { - li { @NodeCard { id, node } } + ul.children {@for (id, node, udata) in children.iter() { + li { @NodeCard { id, node, udata } } }} } NodeKind::Series => { - ol { @for (id, c) in children.iter() { + ol { @for (id, c, _) in children.iter() { li { a[href=uri!(r_library_node(id))] { @c.title } } }} } @@ -168,7 +166,7 @@ markup::define! { } } - Props<'a>(node: &'a NodePublic) { + Props<'a>(node: &'a NodePublic, udata: &'a NodeUserData) { .props { @if let Some(m) = &node.media { p { @format_duration(m.duration) } @@ -193,6 +191,11 @@ markup::define! { @if let Some(f) = &node.federated { p.federation { @f } } + @match udata.watched { + WatchedState::None => {} + WatchedState::Progress(x) => { p.progress { "Watched up to " @format_duration(x) } } + WatchedState::Watched => { p.watched { "Watched" } } + } } } } @@ -212,6 +215,32 @@ pub fn format_duration(mut d: f64) -> String { s } +pub trait DatabaseNodeUserDataExt { + fn get_node_with_userdata( + &self, + id: &str, + session: &Session, + ) -> Result<(String, NodePublic, NodeUserData)>; +} +impl DatabaseNodeUserDataExt for Database { + fn get_node_with_userdata( + &self, + id: &str, + session: &Session, + ) -> Result<(String, NodePublic, NodeUserData)> { + Ok(( + id.to_owned(), + self.node + .get(&id.to_owned())? + .ok_or(anyhow!("node does not exist: {id}"))? + .public, + self.user_node + .get(&(session.user.name.to_owned(), id.to_owned()))? + .unwrap_or_default(), + )) + } +} + trait MediaInfoExt { fn resolution_name(&self) -> &'static str; } diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index d3a3342..177a5f6 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -29,7 +29,6 @@ pub struct PlayerConfig { pub a: Option<TrackID>, pub v: Option<TrackID>, pub s: Option<TrackID>, - pub webm: bool, } #[get("/n/<id>/player?<conf..>", rank = 4)] diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs index 10ba356..d8a44b2 100644 --- a/server/src/routes/ui/sort.rs +++ b/server/src/routes/ui/sort.rs @@ -1,4 +1,4 @@ -use jellycommon::{helpers::SortAnyway, NodeKind, NodePublic, Rating}; +use jellycommon::{helpers::SortAnyway, user::NodeUserData, NodeKind, NodePublic, Rating}; use rocket::{ http::uri::fmt::{Query, UriDisplay}, FromForm, FromFormField, UriDisplayQuery, @@ -79,8 +79,11 @@ enum SortOrder { #[field(value = "descending")] Descending, } -pub fn filter_and_sort_nodes(f: &NodeFilterSort, nodes: &mut Vec<(String, NodePublic)>) { - nodes.retain(|(_id, node)| { +pub fn filter_and_sort_nodes( + f: &NodeFilterSort, + nodes: &mut Vec<(String, NodePublic, NodeUserData)>, +) { + nodes.retain(|(_id, node, _udata)| { let mut o = true; if let Some(prop) = &f.filter_kind { for (p, _) in FilterProperty::ALL { @@ -109,25 +112,25 @@ pub fn filter_and_sort_nodes(f: &NodeFilterSort, nodes: &mut Vec<(String, NodePu if let Some(sort_prop) = &f.sort_by { match sort_prop { SortProperty::ReleaseDate => { - nodes.sort_by_key(|(_, n)| n.release_date.expect("asserted above")) + nodes.sort_by_key(|(_, n, _)| n.release_date.expect("asserted above")) } - SortProperty::Title => nodes.sort_by(|(_, a), (_, b)| a.title.cmp(&b.title)), - SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(_, n)| { + SortProperty::Title => nodes.sort_by(|(_, a, _), (_, b, _)| a.title.cmp(&b.title)), + SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(_, n, _)| { SortAnyway(*n.ratings.get(&Rating::RottenTomatoes).unwrap_or(&0.)) }), - SortProperty::RatingMetacritic => nodes.sort_by_cached_key(|(_, n)| { + SortProperty::RatingMetacritic => nodes.sort_by_cached_key(|(_, n, _)| { SortAnyway(*n.ratings.get(&Rating::Metacritic).unwrap_or(&0.)) }), - SortProperty::RatingImdb => nodes.sort_by_cached_key(|(_, n)| { + SortProperty::RatingImdb => nodes.sort_by_cached_key(|(_, n, _)| { SortAnyway(*n.ratings.get(&Rating::Imdb).unwrap_or(&0.)) }), - SortProperty::RatingYoutubeViews => nodes.sort_by_cached_key(|(_, n)| { + SortProperty::RatingYoutubeViews => nodes.sort_by_cached_key(|(_, n, _)| { SortAnyway(*n.ratings.get(&Rating::YoutubeViews).unwrap_or(&0.)) }), - SortProperty::RatingYoutubeLikes => nodes.sort_by_cached_key(|(_, n)| { + SortProperty::RatingYoutubeLikes => nodes.sort_by_cached_key(|(_, n, _)| { SortAnyway(*n.ratings.get(&Rating::YoutubeLikes).unwrap_or(&0.)) }), - SortProperty::RatingYoutubeFollowers => nodes.sort_by_cached_key(|(_, n)| { + SortProperty::RatingYoutubeFollowers => nodes.sort_by_cached_key(|(_, n, _)| { SortAnyway(*n.ratings.get(&Rating::YoutubeFollowers).unwrap_or(&0.)) }), } |