From bfc5552a8eba07897c2ed626b49c085d97fdfa0d Mon Sep 17 00:00:00 2001 From: metamuffin Date: Thu, 30 Jan 2025 16:45:06 +0100 Subject: external ids and urls --- base/src/database.rs | 22 +++++++++++++------ import/src/lib.rs | 23 +++++++++----------- import/src/matroska.rs | 14 +++++++++--- server/src/routes/external_compat.rs | 41 ++++++++++++++++++++++++++++++++++++ server/src/routes/mod.rs | 4 ++++ 5 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 server/src/routes/external_compat.rs 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> { + 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> { 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, pub infojson: Option, } -pub(crate) fn matroska_metadata(path: &Path) -> Result> { +pub(crate) fn matroska_metadata(path: &Path) -> Result>> { 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> { } } } - 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 +*/ +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?")] +pub fn r_ext_youtube_watch(_session: Session, db: &State, v: &str) -> MyResult { + 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/")] +pub fn r_ext_youtube_channel( + _session: Session, + db: &State, + id: &str, +) -> MyResult { + 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 r_api_account_login, r_api_root, r_api_asset_token_raw, + r_ext_youtube_watch, + r_ext_youtube_channel, ], ) } -- cgit v1.2.3-70-g09d2