diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | base/Cargo.toml | 1 | ||||
-rw-r--r-- | base/src/cache.rs | 8 | ||||
-rw-r--r-- | base/src/database.rs | 21 | ||||
-rw-r--r-- | common/src/lib.rs | 2 | ||||
-rw-r--r-- | import/src/lib.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 52 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 24 | ||||
-rw-r--r-- | web/script/player/mod.ts | 13 | ||||
-rw-r--r-- | web/style/layout.css | 2 |
10 files changed, 104 insertions, 22 deletions
@@ -1712,6 +1712,7 @@ dependencies = [ "anyhow", "base64", "bincode", + "humansize", "jellyclient", "jellycommon", "log", diff --git a/base/Cargo.toml b/base/Cargo.toml index e897404..6474e25 100644 --- a/base/Cargo.toml +++ b/base/Cargo.toml @@ -19,6 +19,7 @@ redb = "2.4.0" tantivy = "0.22.0" serde_json = "1.0.138" aes-gcm-siv = "0.11.1" +humansize = "2.1.3" [features] db_json = [] diff --git a/base/src/cache.rs b/base/src/cache.rs index d6aa15f..0b28e1b 100644 --- a/base/src/cache.rs +++ b/base/src/cache.rs @@ -44,7 +44,8 @@ pub fn cache_location(seed: &[&str]) -> (usize, CachePath) { d.update(b"\0"); } let d = d.finalize(); - let n = d[0] as usize | ((d[1] as usize) << 8) | ((d[2] as usize) << 16) | ((d[3] as usize) << 24); + let n = + d[0] as usize | ((d[1] as usize) << 8) | ((d[2] as usize) << 16) | ((d[3] as usize) << 24); let fname = base64::engine::general_purpose::URL_SAFE.encode(d); let fname = &fname[..22]; let fname = format!("{}-{}", seed[0], fname); // about 128 bits @@ -269,5 +270,8 @@ pub fn cleanup_cache() { CACHE_IN_MEMORY_SIZE.fetch_sub(reduction, Ordering::Relaxed); drop(g); - info!("done"); + info!( + "done, {} freed", + humansize::format_size(reduction, humansize::DECIMAL) + ); } diff --git a/base/src/database.rs b/base/src/database.rs index 32f1464..c3ca5d4 100644 --- a/base/src/database.rs +++ b/base/src/database.rs @@ -35,6 +35,7 @@ const T_INVITE: TableDefinition<&str, ()> = TableDefinition::new("invite"); const T_NODE: TableDefinition<[u8; 32], Ser<Node>> = TableDefinition::new("node"); const T_NODE_CHILDREN: TableDefinition<([u8; 32], [u8; 32]), ()> = TableDefinition::new("node_children"); +const T_TAG_NODE: TableDefinition<(&str, [u8; 32]), ()> = TableDefinition::new("tag_node"); const T_NODE_EXTERNAL_ID: TableDefinition<(&str, &str), [u8; 32]> = TableDefinition::new("node_external_id"); const T_IMPORT_FILE_MTIME: TableDefinition<&[u8], u64> = TableDefinition::new("import_file_mtime"); @@ -109,6 +110,14 @@ impl Database { .map(|r| r.map(|r| NodeID(r.0.value().1))) .collect::<Result<Vec<_>, StorageError>>()?) } + pub fn get_tag_nodes(&self, tag: &str) -> Result<Vec<NodeID>> { + let txn = self.inner.begin_read()?; + let t_tag_node = txn.open_table(T_TAG_NODE)?; + Ok(t_tag_node + .range((tag, NodeID::MIN.0)..(tag, NodeID::MAX.0))? + .map(|r| r.map(|r| NodeID(r.0.value().1))) + .collect::<Result<Vec<_>, StorageError>>()?) + } pub fn get_nodes_modified_since(&self, since: u64) -> Result<Vec<NodeID>> { let txn = self.inner.begin_read()?; let t_node_mtime = txn.open_table(T_NODE_MTIME)?; @@ -171,6 +180,7 @@ impl Database { let mut t_node_mtime = txn.open_table(T_NODE_MTIME)?; let mut t_node_children = txn.open_table(T_NODE_CHILDREN)?; let mut t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?; + let mut t_tag_node = txn.open_table(T_TAG_NODE)?; let mut node = t_node.get(id.0)?.map(|v| v.value().0).unwrap_or_default(); let mut dh_before = HashWriter(DefaultHasher::new()); @@ -189,9 +199,18 @@ impl Database { for (pl, eid) in &node.external_ids { t_node_external_id.insert((pl.as_str(), eid.as_str()), id.0)?; } + for tag in &node.tags { + t_tag_node.insert((tag.as_str(), id.0), ())?; + } t_node.insert(&id.0, Ser(node))?; t_node_mtime.insert(&id.0, time)?; - drop((t_node, t_node_mtime, t_node_children, t_node_external_id)); + drop(( + t_node, + t_node_mtime, + t_node_children, + t_node_external_id, + t_tag_node, + )); txn.set_durability(Durability::Eventual); txn.commit()?; Ok(()) diff --git a/common/src/lib.rs b/common/src/lib.rs index 4480db5..003a798 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -49,6 +49,8 @@ pub struct Node { pub ratings: BTreeMap<Rating, f64>, pub federated: Option<String>, #[serde(default)] + pub tags: BTreeSet<String>, + #[serde(default)] pub people: BTreeMap<PeopleGroup, Vec<Appearance>>, #[serde(default)] pub external_ids: BTreeMap<String, String>, diff --git a/import/src/lib.rs b/import/src/lib.rs index 3ea42f1..78a99c3 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -429,6 +429,8 @@ fn import_media_file( .map(|u| clean_uploader_name(u).to_owned())) .or(node.subtitle.clone()); + node.tags.extend(infojson.tags.unwrap_or_default()); + if let Some(desc) = infojson.description { node.description = Some(desc) } diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 9deffbb..76ecd82 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -36,7 +36,7 @@ use jellycommon::{ Chapter, MediaInfo, Node, NodeID, NodeKind, PeopleGroup, Rating, SourceTrackKind, Visibility, }; use rocket::{get, serde::json::Json, Either, State}; -use std::{fmt::Write, sync::Arc}; +use std::{cmp::Reverse, collections::BTreeMap, fmt::Write, sync::Arc}; /// This function is a stub and only useful for use in the uri! macro. #[get("/n/<id>")] @@ -74,6 +74,9 @@ pub async fn r_library_node_filter<'a>( Vec::new() }; + let mut similar = get_similar_media(&node, db, &session)?; + + similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); children.retain(|(n, _)| n.visibility >= Visibility::Reduced); parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); @@ -98,13 +101,38 @@ pub async fn r_library_node_filter<'a>( Either::Left(LayoutPage { title: node.title.clone().unwrap_or_default(), content: markup::new! { - @NodePage { node: &node, udata: &udata, children: &children, parents: &parents, filter: &filter } + @NodePage { node: &node, udata: &udata, children: &children, parents: &parents, filter: &filter, player: false, similar: &similar } }, ..Default::default() }) }) } +pub fn get_similar_media( + node: &Node, + db: &Database, + session: &Session, +) -> Result<Vec<(Arc<Node>, NodeUserData)>> { + let this_id = NodeID::from_slug(&node.slug); + let mut ranking = BTreeMap::<NodeID, usize>::new(); + for tag in &node.tags { + let nodes = db.get_tag_nodes(tag)?; + let weight = 1_000_000 / nodes.len(); + for n in nodes { + if n != this_id { + *ranking.entry(n).or_default() += weight; + } + } + } + let mut ranking = ranking.into_iter().collect::<Vec<_>>(); + ranking.sort_by_key(|(_, k)| Reverse(*k)); + ranking + .into_iter() + .take(32) + .map(|(pid, _)| db.get_node_with_userdata(pid, &session)) + .collect::<anyhow::Result<Vec<_>>>() +} + markup::define! { NodeCard<'a>(node: &'a Node, udata: &'a NodeUserData) { @let cls = format!("node card poster {}", aspect_class(node.kind)); @@ -151,12 +179,12 @@ markup::define! { } } } - NodePage<'a>(node: &'a Node, udata: &'a NodeUserData, children: &'a [(Arc<Node>, NodeUserData)], parents: &'a [(Arc<Node>, NodeUserData)], filter: &'a NodeFilterSort) { - @if !matches!(node.kind, NodeKind::Collection) { + NodePage<'a>(node: &'a Node, udata: &'a NodeUserData, children: &'a [(Arc<Node>, NodeUserData)], parents: &'a [(Arc<Node>, NodeUserData)], similar: &'a [(Arc<Node>, NodeUserData)], filter: &'a NodeFilterSort, player: bool) { + @if !matches!(node.kind, NodeKind::Collection) && !player { img.backdrop[src=uri!(r_item_backdrop(&node.slug, Some(2048))), loading="lazy"]; } .page.node { - @if !matches!(node.kind, NodeKind::Collection) { + @if !matches!(node.kind, NodeKind::Collection) && !player { @let cls = format!("bigposter {}", aspect_class(node.kind)); div[class=cls] { img[src=uri!(r_item_poster(&node.slug, Some(2048))), loading="lazy"]; } } @@ -248,10 +276,24 @@ markup::define! { }} } } + @if !node.tags.is_empty() { + details { + summary { "Tags" } + ol { @for tag in &node.tags { + li { @tag } + }} + } + } } @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) { @NodeFilterSortForm { f: filter } } + @if !similar.is_empty() { + h2 { "Similar Media" } + ul.children.hlist {@for (node, udata) in similar.iter() { + li { @NodeCard { node, udata } } + }} + } @match node.kind { NodeKind::Show | NodeKind::Series | NodeKind::Season => { ol { @for (node, udata) in children.iter() { diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 2cc2dd4..d2a8236 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -6,6 +6,7 @@ use super::{ account::session::{token, Session}, layout::LayoutPage, + node::{get_similar_media, DatabaseNodeUserDataExt}, }; use crate::{ database::Database, @@ -19,7 +20,7 @@ use jellybase::CONF; use jellycommon::{ stream::{StreamContainer, StreamSpec}, user::{PermissionSet, PlayerKind}, - Node, NodeID, SourceTrackKind, TrackID, + Node, NodeID, SourceTrackKind, TrackID, Visibility, }; use markup::DynRender; use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; @@ -59,28 +60,39 @@ fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: & #[get("/n/<id>/player?<conf..>", rank = 4)] pub fn r_player( - sess: Session, + session: Session, db: &State<Database>, id: NodeID, conf: PlayerConfig, ) -> MyResult<Either<DynLayoutPage<'_>, Redirect>> { - let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; + let (node, _udata) = db.get_node_with_userdata(id, &session)?; + + let mut parents = node + .parents + .iter() + .map(|pid| db.get_node_with_userdata(*pid, &session)) + .collect::<anyhow::Result<Vec<_>>>()?; + + let mut similar = get_similar_media(&node, db, &session)?; + + similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); + parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); let native_session = |action: &str| { Ok(Either::Right(Redirect::temporary(jellynative_url( action, conf.t.unwrap_or(0.), - &sess.user.native_secret, + &session.user.native_secret, &id.to_string(), &token::create( - sess.user.name, + session.user.name, PermissionSet::default(), // TODO chrono::Duration::hours(24), ), )))) }; - match conf.kind.unwrap_or(sess.user.player_preference) { + match conf.kind.unwrap_or(session.user.player_preference) { PlayerKind::Browser => (), PlayerKind::Native => { return native_session("player-v2"); diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index af62cde..e8cde94 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -18,10 +18,10 @@ globalThis.addEventListener("DOMContentLoaded", () => { if (globalThis.location.search.search("nojsp") != -1) return if (!globalThis.MediaSource) return alert("Media Source Extension API required") const node_id = globalThis.location.pathname.split("/")[2]; - const main = document.getElementById("main")!; - document.getElementsByTagName("footer")[0].remove() + document.getElementById("player")?.remove(); + document.getElementsByClassName("playerconf").item(0)?.remove() globalThis.dispatchEvent(new Event("navigationrequiresreload")) - initialize_player(main, node_id) + document.getElementById("main")!.prepend(initialize_player(node_id)) } }) @@ -50,9 +50,7 @@ function get_query_start_time() { return x } -function initialize_player(el: HTMLElement, node_id: string) { - el.innerHTML = "" // clear the body - +function initialize_player(node_id: string): HTMLElement { const logger = new Logger<string>(s => e("p", s)) const start_time = get_query_start_time() ?? 0 // TODO get_continue_time(ndata.userdata.watched); const player = new Player(`/n/${encodeURIComponent(node_id)}/stream`, `/n/${encodeURIComponent(node_id)}/poster`, start_time, logger) @@ -253,7 +251,6 @@ function initialize_player(el: HTMLElement, node_id: string) { popups, controls, ) - el.append(pel) controls.onmouseenter = () => idle_inhibit.value = true controls.onmouseleave = () => idle_inhibit.value = false @@ -296,6 +293,8 @@ function initialize_player(el: HTMLElement, node_id: string) { k.preventDefault() }) send_player_progress(node_id, player) + + return pel } function screenshot_video(video: HTMLVideoElement) { diff --git a/web/style/layout.css b/web/style/layout.css index a86f76d..a14c86e 100644 --- a/web/style/layout.css +++ b/web/style/layout.css @@ -53,7 +53,7 @@ h1, h2, h3, h4 { h1 { font-weight: bold; } -p, span, a, td, th, label, input, legend, pre, summary { +p, span, a, td, th, label, input, legend, pre, summary, li { color: var(--font); } |