aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes')
-rw-r--r--server/src/routes/mod.rs3
-rw-r--r--server/src/routes/progress.rs75
-rw-r--r--server/src/routes/ui/browser.rs21
-rw-r--r--server/src/routes/ui/home.rs67
-rw-r--r--server/src/routes/ui/node.rs101
-rw-r--r--server/src/routes/ui/player.rs1
-rw-r--r--server/src/routes/ui/sort.rs25
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.))
}),
}