aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--base/src/database.rs22
-rw-r--r--import/src/lib.rs23
-rw-r--r--import/src/matroska.rs14
-rw-r--r--server/src/routes/external_compat.rs41
-rw-r--r--server/src/routes/mod.rs4
5 files changed, 82 insertions, 22 deletions
diff --git a/base/src/database.rs b/base/src/database.rs
index cd8b2bc..28cafaa 100644
--- a/base/src/database.rs
+++ b/base/src/database.rs
@@ -56,11 +56,13 @@ impl Database {
{
// this creates all tables such that read operations on them do not fail.
let txn = r.inner.begin_write()?;
- drop(txn.open_table(T_INVITE)?);
- drop(txn.open_table(T_USER)?);
- drop(txn.open_table(T_USER_NODE)?);
- drop(txn.open_table(T_NODE)?);
- drop(txn.open_table(T_IMPORT_FILE_MTIME)?);
+ txn.open_table(T_INVITE)?;
+ txn.open_table(T_USER)?;
+ txn.open_table(T_USER_NODE)?;
+ txn.open_table(T_NODE)?;
+ txn.open_table(T_NODE_CHILDREN)?;
+ txn.open_table(T_NODE_EXTERNAL_ID)?;
+ txn.open_table(T_IMPORT_FILE_MTIME)?;
txn.commit()?;
}
@@ -81,7 +83,15 @@ impl Database {
Ok(None)
}
}
-
+ pub fn get_node_external_id(&self, platform: &str, eid: &str) -> Result<Option<NodeID>> {
+ let txn = self.inner.begin_read()?;
+ let t_node_external_id = txn.open_table(T_NODE_EXTERNAL_ID)?;
+ if let Some(id) = t_node_external_id.get((platform, eid))? {
+ Ok(Some(NodeID(id.value())))
+ } else {
+ Ok(None)
+ }
+ }
pub fn get_node_children(&self, id: NodeID) -> Result<Vec<NodeID>> {
let txn = self.inner.begin_read()?;
let t_node_children = txn.open_table(T_NODE_CHILDREN)?;
diff --git a/import/src/lib.rs b/import/src/lib.rs
index a22551e..c841294 100644
--- a/import/src/lib.rs
+++ b/import/src/lib.rs
@@ -10,13 +10,12 @@ use jellycommon::{
Chapter, LocalTrack, MediaInfo, Node, NodeID, NodeKind, Rating, SourceTrack, SourceTrackKind,
TrackSource,
};
-use log::info;
use matroska::matroska_metadata;
use rayon::iter::{ParallelDrainRange, ParallelIterator};
use std::{
collections::HashMap,
fs::File,
- io::{BufReader, Read},
+ io::BufReader,
mem::swap,
path::{Path, PathBuf},
sync::LazyLock,
@@ -170,9 +169,10 @@ fn import_file(db: &Database, path: &Path) -> Result<()> {
.to_owned(),
);
node.external_ids
- .insert("youtube".to_string(), data.channel_id);
+ .insert("youtube:channel".to_string(), data.channel_id);
if let Some(uid) = data.uploader_id {
- node.external_ids.insert("youtube".to_string(), uid);
+ node.external_ids
+ .insert("youtube:channel-name".to_string(), uid);
}
node.description = Some(data.description);
if let Some(followers) = data.channel_follower_count {
@@ -185,19 +185,15 @@ fn import_file(db: &Database, path: &Path) -> Result<()> {
_ => (),
}
- let mut magic = [0; 4];
- File::open(path)?.read_exact(&mut magic).ok();
- if matches!(magic, [0x1A, 0x45, 0xDF, 0xA3]) {
- import_media_file(db, path, parent).context("media file")?;
- }
+ import_media_file(db, path, parent).context("media file")?;
Ok(())
}
fn import_media_file(db: &Database, path: &Path, parent: NodeID) -> Result<()> {
- info!("reading media file {path:?}");
-
- let m = (*matroska_metadata(path)?).to_owned();
+ let Some(m) = (*matroska_metadata(path)?).to_owned() else {
+ return Ok(());
+ };
let info = m.info.ok_or(anyhow!("no info"))?;
let tracks = m.tracks.ok_or(anyhow!("no tracks"))?;
@@ -251,7 +247,8 @@ fn import_media_file(db: &Database, path: &Path, parent: NodeID) -> Result<()> {
node.release_date =
Some(infojson::parse_upload_date(date).context("parsing upload date")?);
}
- node.external_ids.insert("youtube".to_string(), infojson.id);
+ node.external_ids
+ .insert("youtube:video".to_string(), infojson.id);
node.ratings.insert(
Rating::YoutubeViews,
infojson.view_count.unwrap_or_default() as f64,
diff --git a/import/src/matroska.rs b/import/src/matroska.rs
index bb8d927..6a33420 100644
--- a/import/src/matroska.rs
+++ b/import/src/matroska.rs
@@ -16,6 +16,7 @@ use jellybase::{
cache::{cache_file, cache_memory},
};
use jellycommon::Asset;
+use log::info;
use std::{
fs::File,
io::{BufReader, ErrorKind, Read, Write},
@@ -32,8 +33,15 @@ pub(crate) struct MatroskaMetadata {
pub tags: Option<Tags>,
pub infojson: Option<YVideo>,
}
-pub(crate) fn matroska_metadata(path: &Path) -> Result<Arc<MatroskaMetadata>> {
+pub(crate) fn matroska_metadata(path: &Path) -> Result<Arc<Option<MatroskaMetadata>>> {
cache_memory(&["mkmeta-v1", path.to_string_lossy().as_ref()], || {
+ let mut magic = [0; 4];
+ File::open(path)?.read_exact(&mut magic).ok();
+ if !matches!(magic, [0x1A, 0x45, 0xDF, 0xA3]) {
+ return Ok(None);
+ }
+
+ info!("reading media file {path:?}");
let mut file = BufReader::new(File::open(path)?);
let mut file = file.by_ref().take(u64::MAX);
@@ -100,13 +108,13 @@ pub(crate) fn matroska_metadata(path: &Path) -> Result<Arc<MatroskaMetadata>> {
}
}
}
- Ok(MatroskaMetadata {
+ Ok(Some(MatroskaMetadata {
chapters,
cover,
info,
infojson,
tags,
tracks,
- })
+ }))
})
}
diff --git a/server/src/routes/external_compat.rs b/server/src/routes/external_compat.rs
new file mode 100644
index 0000000..7babfa5
--- /dev/null
+++ b/server/src/routes/external_compat.rs
@@ -0,0 +1,41 @@
+/*
+ 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 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::{get, response::Redirect, State};
+
+#[get("/watch?<v>")]
+pub fn r_ext_youtube_watch(_session: Session, db: &State<Database>, v: &str) -> MyResult<Redirect> {
+ if v.len() != 11 {
+ Err(anyhow!("video id length incorrect"))?
+ }
+ let Some(id) = db.get_node_external_id("youtube:video", v)? else {
+ Err(anyhow!("element not found"))?
+ };
+ let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?;
+ Ok(Redirect::to(rocket::uri!(r_library_node(&node.slug))))
+}
+
+#[get("/channel/<id>")]
+pub fn r_ext_youtube_channel(
+ _session: Session,
+ db: &State<Database>,
+ id: &str,
+) -> MyResult<Redirect> {
+ let Some(id) = (if id.starts_with("UC") {
+ db.get_node_external_id("youtube:channel", id)?
+ } else if id.starts_with("@") {
+ db.get_node_external_id("youtube:channel-name", id)?
+ } else {
+ Err(anyhow!("unknown channel id format"))?
+ }) else {
+ Err(anyhow!("channel not found"))?
+ };
+ let node = db.get_node(id)?.ok_or(anyhow!("node missing"))?;
+ Ok(Redirect::to(rocket::uri!(r_library_node(&node.slug))))
+}
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 600d544..5fb9b26 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -7,6 +7,7 @@ use self::playersync::{r_streamsync, PlayersyncChannels};
use crate::{database::Database, routes::ui::error::MyResult};
use api::{r_api_account_login, r_api_asset_token_raw, r_api_root, r_api_version};
use base64::Engine;
+use external_compat::{r_ext_youtube_channel, r_ext_youtube_watch};
use jellybase::{federation::Federation, CONF, SECRETS};
use log::warn;
use rand::random;
@@ -48,6 +49,7 @@ use userdata::{
};
pub mod api;
+pub mod external_compat;
pub mod playersync;
pub mod stream;
pub mod ui;
@@ -140,6 +142,8 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build>
r_api_account_login,
r_api_root,
r_api_asset_token_raw,
+ r_ext_youtube_watch,
+ r_ext_youtube_channel,
],
)
}