diff options
author | metamuffin <metamuffin@disroot.org> | 2025-03-03 18:15:47 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-03-03 18:15:47 +0100 |
commit | bf84be508aa415b45a51fc0fe007a0879f7bfab7 (patch) | |
tree | 6a98c9a534dbb5eed899d0f9ff6f6499dddfd0db | |
parent | 26d3a70b0be2809177076e155f987e18e2b2ceb2 (diff) | |
download | jellything-bf84be508aa415b45a51fc0fe007a0879f7bfab7.tar jellything-bf84be508aa415b45a51fc0fe007a0879f7bfab7.tar.bz2 jellything-bf84be508aa415b45a51fc0fe007a0879f7bfab7.tar.zst |
nodepage in player and tags
-rw-r--r-- | base/src/database.rs | 21 | ||||
-rw-r--r-- | common/src/lib.rs | 2 | ||||
-rw-r--r-- | common/src/stream.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 | 36 | ||||
-rw-r--r-- | web/script/player/mod.ts | 13 | ||||
-rw-r--r-- | web/style/layout.css | 2 |
8 files changed, 102 insertions, 28 deletions
diff --git a/base/src/database.rs b/base/src/database.rs index 407db29..2909498 100644 --- a/base/src/database.rs +++ b/base/src/database.rs @@ -34,6 +34,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"); @@ -105,6 +106,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)?; @@ -164,6 +173,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()); @@ -182,9 +192,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 ce333eb..7685027 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -50,6 +50,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/common/src/stream.rs b/common/src/stream.rs index 3e227e1..1c285b3 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -1,9 +1,9 @@ -use bincode::{Decode, Encode}; /* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ +use bincode::{Decode, Encode}; #[cfg(feature = "rocket")] use rocket::{FromForm, FromFormField, UriDisplayQuery}; use serde::{Deserialize, Serialize}; diff --git a/import/src/lib.rs b/import/src/lib.rs index 3226a0a..f7c047e 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -432,6 +432,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 2f28f74..c2188a8 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -6,12 +6,14 @@ use super::{ account::session::{token, Session}, layout::LayoutPage, + node::{get_similar_media, DatabaseNodeUserDataExt, NodePage}, + sort::NodeFilterSort, }; use crate::{ database::Database, routes::{ stream::rocket_uri_macro_r_stream, - ui::{assets::rocket_uri_macro_r_item_backdrop, error::MyResult, layout::DynLayoutPage}, + ui::{error::MyResult, layout::DynLayoutPage}, }, uri, }; @@ -20,7 +22,7 @@ use jellybase::{permission::PermissionSetExt, CONF}; use jellycommon::{ stream::{StreamFormat, StreamSpec}, user::{PermissionSet, PlayerKind, UserPermission}, - Node, NodeID, SourceTrackKind, TrackID, + Node, NodeID, SourceTrackKind, TrackID, Visibility, }; use markup::DynRender; use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; @@ -59,12 +61,23 @@ 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| { let perm = [ @@ -73,22 +86,22 @@ pub fn r_player( UserPermission::StreamFormat(StreamFormat::Fragment), ]; for perm in &perm { - sess.user.permissions.assert(perm)?; + session.user.permissions.assert(perm)?; } 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(perm.map(|e| (e, true)).into()), 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"); @@ -117,11 +130,8 @@ pub fn r_player( title: node.title.to_owned().unwrap_or_default(), class: Some("player"), content: markup::new! { - @if playing { - video[src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} - } else { - img.backdrop[src=uri!(r_item_backdrop(&node.slug, Some(2048))).to_string()]; - } + video[id="player", src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} + @NodePage { children: &[], parents: &parents, filter: &NodeFilterSort::default(), node: &node, udata: &udata, player: true, similar: &similar } @conf }, })) diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index 15c37da..53f13bd 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)) } }) @@ -37,9 +37,7 @@ function toggle_fullscreen() { } -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 player = new Player(node_id, logger) const show_stats = new OVar(false); @@ -238,7 +236,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 @@ -281,6 +278,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); } |