aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--base/Cargo.toml1
-rw-r--r--base/src/cache.rs8
-rw-r--r--base/src/database.rs21
-rw-r--r--common/src/lib.rs2
-rw-r--r--import/src/lib.rs2
-rw-r--r--server/src/routes/ui/node.rs52
-rw-r--r--server/src/routes/ui/player.rs24
-rw-r--r--web/script/player/mod.ts13
-rw-r--r--web/style/layout.css2
10 files changed, 104 insertions, 22 deletions
diff --git a/Cargo.lock b/Cargo.lock
index aabeff6..94bead7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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);
}