diff options
| author | metamuffin <metamuffin@disroot.org> | 2023-07-31 19:53:01 +0200 | 
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2023-07-31 19:53:01 +0200 | 
| commit | aeafba7847e189313df3025e6d6f291999b57350 (patch) | |
| tree | bf7affdca28208695648bc9b18856cbb7049d1e8 /server | |
| parent | 0c651f11920350a4aa96aa24f8fe15b28390aed2 (diff) | |
| download | jellything-aeafba7847e189313df3025e6d6f291999b57350.tar jellything-aeafba7847e189313df3025e6d6f291999b57350.tar.bz2 jellything-aeafba7847e189313df3025e6d6f291999b57350.tar.zst  | |
update server to new schema
Diffstat (limited to 'server')
| -rw-r--r-- | server/src/database.rs | 6 | ||||
| -rw-r--r-- | server/src/import.rs | 68 | ||||
| -rw-r--r-- | server/src/library.rs | 195 | ||||
| -rw-r--r-- | server/src/main.rs | 7 | ||||
| -rw-r--r-- | server/src/routes/api/mod.rs | 73 | ||||
| -rw-r--r-- | server/src/routes/mod.rs | 19 | ||||
| -rw-r--r-- | server/src/routes/stream.rs | 44 | ||||
| -rw-r--r-- | server/src/routes/ui/assets.rs | 44 | ||||
| -rw-r--r-- | server/src/routes/ui/browser.rs | 31 | ||||
| -rw-r--r-- | server/src/routes/ui/home.rs | 8 | ||||
| -rw-r--r-- | server/src/routes/ui/mod.rs | 1 | ||||
| -rw-r--r-- | server/src/routes/ui/node.rs | 172 | ||||
| -rw-r--r-- | server/src/routes/ui/player.rs | 49 | 
13 files changed, 268 insertions, 449 deletions
diff --git a/server/src/database.rs b/server/src/database.rs index bfb5d47..eb88bda 100644 --- a/server/src/database.rs +++ b/server/src/database.rs @@ -5,7 +5,7 @@  */  use crate::{routes::ui::account::hash_password, CONF};  use anyhow::Context; -use jellycommon::SeekIndex; +use jellycommon::{SeekIndex, Node};  use log::info;  use serde::{Deserialize, Serialize};  use std::path::Path; @@ -16,7 +16,7 @@ pub struct Database {      pub users: Tree<String, User>,      pub invites: Tree<String, ()>, -    pub items: Tree<String, Item>, +    pub node: Tree<String, Node>,      pub seek_index: Tree<(String, usize), SeekIndex>,  } @@ -36,7 +36,7 @@ impl Database {          let r = Ok(Self {              users: Tree::open(&db, "users"),              invites: Tree::open(&db, "invites"), -            items: Tree::open(&db, "items"), +            node: Tree::open(&db, "items"),              seek_index: Tree::open(&db, "seek_index"),              db,          }); diff --git a/server/src/import.rs b/server/src/import.rs new file mode 100644 index 0000000..06d32c3 --- /dev/null +++ b/server/src/import.rs @@ -0,0 +1,68 @@ +/* +    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) 2023 metamuffin <metamuffin.org> +*/ +use crate::{database::Database, CONF}; +use anyhow::{bail, Context, Ok}; +use jellycommon::Node; +use log::info; +use std::{ffi::OsStr, fs::File, os::unix::prelude::OsStrExt, path::PathBuf}; + +pub fn import(db: &Database) -> anyhow::Result<()> { +    info!("clearing node tree"); +    db.node.clear()?; +    info!("importing..."); +    import_path(CONF.library_path.clone(), db).context("indexing")?; +    Ok(()) +} + +pub fn import_path(path: PathBuf, db: &Database) -> anyhow::Result<Vec<String>> { +    if path.is_dir() { +        let mpath = path.join("directory.json"); +        let children = path.read_dir()?.filter_map(|e| { +            let e = e.unwrap(); +            if e.path().extension() == Some(&OsStr::from_bytes(b"jelly")) +                || e.metadata().unwrap().is_dir() +            { +                Some(e.path()) +            } else { +                None +            } +        }); +        let children = children +            .map(|e| import_path(e, db)) +            .collect::<anyhow::Result<Vec<_>>>()? +            .into_iter() +            .flatten() +            .collect(); +        if mpath.exists() { +            let data: Node = +                serde_json::from_reader(File::open(mpath).context("metadata missing")?)?; + +            let identifier = path.file_name().unwrap().to_str().unwrap().to_string(); + +            db.node.insert(&identifier, &data)?; + +            Ok(vec![identifier]) +        } else { +            Ok(children) +        } +    } else if path.is_file() { +        info!("loading item {path:?}"); +        let datafile = File::open(path.clone()).context("cant load metadata")?; +        let data: Node = serde_json::from_reader(datafile).context("invalid metadata")?; +        let identifier = path +            .file_name() +            .unwrap() +            .to_str() +            .unwrap() +            .strip_suffix(".jelly") +            .unwrap() +            .to_string(); +        db.node.insert(&identifier, &data)?; +        Ok(vec![identifier]) +    } else { +        bail!("did somebody really put a fifo or socket in the library?!") +    } +} diff --git a/server/src/library.rs b/server/src/library.rs deleted file mode 100644 index 8606a6e..0000000 --- a/server/src/library.rs +++ /dev/null @@ -1,195 +0,0 @@ -/* -    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) 2023 metamuffin <metamuffin.org> -*/ -use anyhow::{anyhow, bail, Context, Ok}; -use jellycommon::{CommmonInfo, DirectoryInfo, ItemInfo}; -use log::info; -use std::{ -    ffi::OsStr, -    fs::File, -    os::unix::prelude::OsStrExt, -    path::{Path, PathBuf}, -    sync::Arc, -}; - -use crate::{routes::ui::node::AssetRole, CONF}; - -impl Library { -    pub fn open(path: &Path) -> anyhow::Result<Self> { -        Ok(Self { -            root_path: path.to_path_buf(), -            root: Node::from_path(path.to_path_buf(), PathBuf::new(), true) -                .context("indexing root")? -                .into_iter() -                .next() -                .ok_or(anyhow!("root need directory.json"))?, -        }) -    } -    pub fn nested_path(&self, path: &Path) -> anyhow::Result<Arc<Node>> { -        self.nested(path.to_str().unwrap()) -    } -    pub fn nested(&self, path: &str) -> anyhow::Result<Arc<Node>> { -        let mut n = self.root.clone(); -        if path.is_empty() { -            return Ok(n); -        } -        for seg in path.split('/') { -            n = n -                .get_directory()? -                .child_by_ident(seg) -                .ok_or(anyhow!("does not exist"))?; -        } -        Ok(n) -    } -} - -impl Node { -    pub fn get_directory(&self) -> anyhow::Result<Arc<Directory>> { -        match self { -            Node::Directory(d) => Ok(d.clone()), -            Node::Item(_) => bail!("not a directory"), -        } -    } -    pub fn get_item(&self) -> anyhow::Result<Arc<Item>> { -        match self { -            Node::Item(i) => Ok(i.clone()), -            Node::Directory(_) => bail!("not an item"), -        } -    } -    pub fn common(&self) -> &CommmonInfo { -        match self { -            Node::Directory(d) => &d.info.common, -            Node::Item(i) => &i.info.common, -        } -    } -    pub fn identifier(&self) -> &str { -        match self { -            Node::Directory(d) => &d.identifier, -            Node::Item(i) => &i.identifier, -        } -    } -    pub fn lib_path(&self) -> &PathBuf { -        match self { -            Node::Directory(d) => &d.lib_path, -            Node::Item(i) => &i.lib_path, -        } -    } - -    pub fn from_path( -        path: PathBuf, -        mut lib_path: PathBuf, -        root: bool, -    ) -> anyhow::Result<Vec<Arc<Node>>> { -        if path.is_dir() { -            let mpath = path.join("directory.json"); -            let children = path.read_dir()?.filter_map(|e| { -                let e = e.unwrap(); -                if e.path().extension() == Some(&OsStr::from_bytes(b"jelly")) -                    || e.metadata().unwrap().is_dir() -                { -                    Some(e.path()) -                } else { -                    None -                } -            }); -            if !mpath.exists() { -                info!("flattening {path:?}"); -                Ok(children -                    .map(|e| Node::from_path(e, lib_path.clone(), false)) -                    .collect::<Result<Vec<_>, _>>()? -                    .into_iter() -                    .flatten() -                    .collect()) -            } else { -                let data: DirectoryInfo = -                    serde_json::from_reader(File::open(mpath).context("metadata missing")?)?; - -                let identifier = path.file_name().unwrap().to_str().unwrap().to_string(); -                if !root { -                    lib_path = lib_path.join(identifier.clone()); -                } - -                info!("scanning directory {path:?}"); - -                let children = children -                    .map(|e| { -                        Node::from_path(e.clone(), lib_path.clone(), false) -                            .context(format!("loading {e:?}")) -                    }) -                    .collect::<anyhow::Result<Vec<_>>>()? -                    .into_iter() -                    .flatten() -                    .collect(); - -                Ok(Vec::from_iter(Some( -                    Node::Directory(Arc::new(Directory { -                        lib_path, -                        children, -                        info: data, -                        identifier, -                    })) -                    .into(), -                ))) -            } -        } else if path.is_file() { -            info!("loading item {path:?}"); -            let datafile = File::open(path.clone()).context("cant load metadata")?; -            let data: ItemInfo = serde_json::from_reader(datafile).context("invalid metadata")?; -            let identifier = path -                .file_name() -                .unwrap() -                .to_str() -                .unwrap() -                .strip_suffix(".jelly") -                .unwrap() -                .to_string(); -            Ok(Vec::from_iter(Some( -                Node::Item(Arc::new(Item { -                    fs_path: path, -                    lib_path: lib_path.join(identifier.clone()), -                    info: data, -                    identifier, -                })) -                .into(), -            ))) -        } else { -            bail!("did somebody really put a fifo or socket in the library?!") -        } -    } - -    pub fn get_asset(&self, library: &Library, role: AssetRole) -> PathBuf { -        let path = match role { -            AssetRole::Backdrop => self -                .common() -                .backdrop -                .clone() -                .or_else(|| self.common().poster.clone()), -            AssetRole::Poster => self.common().poster.clone(), -        }; -        if let Some(p) = path { -            library.root_path.join(p) -        } else { -            CONF.asset_path.join("fallback.jpeg") -        } -    } -} - -impl Item { -    pub fn path(&self) -> String { -        self.lib_path.to_str().unwrap().to_string() -    } -} - -impl Directory { -    pub fn path(&self) -> String { -        self.lib_path.to_str().unwrap().to_string() -    } -    pub fn child_by_ident(&self, i: &str) -> Option<Arc<Node>> { -        self.children -            .iter() -            .find(|e| e.identifier() == i) -            .map(|e| e.to_owned()) -    } -} diff --git a/server/src/main.rs b/server/src/main.rs index 6d7f812..a9a22cf 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -6,14 +6,13 @@  use config::{load_global_config, GlobalConfig};  use database::Database;  use jellyremuxer::RemuxerContext; -use library::Library;  use once_cell::sync::Lazy;  use rocket::launch;  use routes::build_rocket;  pub mod config;  pub mod database; -pub mod library; +pub mod import;  pub mod routes;  pub static CONF: Lazy<GlobalConfig> = Lazy::new(load_global_config); @@ -29,8 +28,8 @@ fn rocket() -> _ {      log::warn!("authentification bypass enabled");      let remuxer = RemuxerContext::new(); -    let library = Library::open(&CONF.library_path).unwrap();      let database = Database::open(&CONF.database_path).unwrap(); +    import::import(&database).unwrap();      database.create_admin(); -    build_rocket(remuxer, library, database) +    build_rocket(remuxer, database)  } diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs index af795f1..d49ecec 100644 --- a/server/src/routes/api/mod.rs +++ b/server/src/routes/api/mod.rs @@ -5,31 +5,10 @@  */  pub mod error; -use std::path::PathBuf; - -use super::ui::{ -    account::{login_logic, LoginForm}, -    node::AssetRole, -    CacheControlFile, -}; -use crate::{ -    database::Database, -    library::{Library, Node}, -    routes::{api::error::ApiResult, ui::account::session::Session}, -}; -use anyhow::Context; -use jellycommon::api::ApiNode; -use log::info; -use rocket::{ -    get, -    http::{ContentType, CookieJar}, -    post, -    response::Redirect, -    serde::json::Json, -    State, -}; +use super::ui::account::{login_logic, LoginForm}; +use crate::{database::Database, routes::api::error::ApiResult}; +use rocket::{get, http::CookieJar, post, response::Redirect, serde::json::Json, State};  use serde_json::{json, Value}; -use tokio::fs::File;  #[get("/api")]  pub fn r_api_root() -> Redirect { @@ -50,49 +29,3 @@ pub fn r_api_account_login(      login_logic(jar, database, &data.username, &data.password)?;      Ok(json!({ "ok": true }))  } - -#[get("/api/assets/node/<path..>?<role>")] -pub async fn r_api_assets_node( -    _sess: Session, -    path: PathBuf, -    role: AssetRole, -    library: &State<Library>, -) -> ApiResult<(ContentType, CacheControlFile)> { -    let node = library -        .nested_path(&path) -        .context("retrieving library node")?; -    let path = node.get_asset(library, role); -    info!("loading asset from {path:?}"); -    let ext = path.extension().unwrap().to_str().unwrap(); -    Ok(( -        ContentType::from_extension(ext).unwrap(), -        CacheControlFile::new(File::open(path).await?).await, -    )) -} - -#[get("/api/node/<path..>")] -pub fn r_api_node( -    _sess: Session, -    path: PathBuf, -    library: &State<Library>, -) -> ApiResult<Json<ApiNode>> { -    let node = library -        .nested_path(&path) -        .context("retrieving library node")?; - -    Ok(Json(match node.as_ref() { -        Node::Directory(d) => ApiNode::Directory { -            identifier: d.identifier.clone(), -            info: d.info.clone(), -            children: d -                .children -                .iter() -                .map(|c| c.identifier().to_string()) -                .collect::<Vec<_>>(), -        }, -        Node::Item(i) => ApiNode::Item { -            identifier: i.identifier.clone(), -            info: i.info.clone(), -        }, -    })) -} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 8d50c2e..52918d9 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -3,11 +3,8 @@      which is licensed under the GNU Affero General Public License (version 3); see /COPYING.      Copyright (C) 2023 metamuffin <metamuffin.org>  */ -use crate::{database::Database, library::Library, routes::ui::error::MyResult, CONF}; -use api::{ -    error::r_api_catch, r_api_account_login, r_api_assets_node, r_api_node, r_api_root, -    r_api_version, -}; +use crate::{database::Database, routes::ui::error::MyResult, CONF}; +use api::{error::r_api_catch, r_api_account_login, r_api_root, r_api_version};  use jellyremuxer::RemuxerContext;  use rocket::{      catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, get, http::Header, routes, Build, @@ -25,10 +22,11 @@ use ui::{          r_account_register, r_account_register_post,          settings::{r_account_settings, r_account_settings_post},      }, +    assets::r_item_assets,      browser::r_all_items,      error::r_catch,      home::{r_home, r_home_unpriv}, -    node::{r_item_assets, r_library_node}, +    node::r_library_node,      player::r_player,      style::{r_assets_font, r_assets_js, r_assets_style},  }; @@ -44,18 +42,13 @@ macro_rules! uri {      };  } -pub fn build_rocket( -    remuxer: RemuxerContext, -    library: Library, -    database: Database, -) -> Rocket<Build> { +pub fn build_rocket(remuxer: RemuxerContext, database: Database) -> Rocket<Build> {      rocket::build()          .configure(Config {              secret_key: SecretKey::derive_from(CONF.cookie_key.as_bytes()),              ..Default::default()          })          .manage(remuxer) -        .manage(library)          .manage(database)          .attach(AdHoc::on_response("set server header", |_req, res| {              res.set_header(Header::new("server", "jellything")); @@ -92,8 +85,6 @@ pub fn build_rocket(                  r_account_settings_post,                  r_api_version,                  r_api_account_login, -                r_api_node, -                r_api_assets_node,                  r_api_root,              ],          ) diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index b2b708b..2583cb1 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -4,8 +4,9 @@      Copyright (C) 2023 metamuffin <metamuffin.org>  */  use super::ui::{account::session::Session, error::MyError}; -use crate::library::Library; -use anyhow::{anyhow, Context, Result}; +use crate::{database::Database, CONF}; +use anyhow::{anyhow, Result}; +use jellycommon::MediaSource;  use jellyremuxer::RemuxerContext;  use log::{debug, info, warn};  use rocket::{ @@ -15,17 +16,14 @@ use rocket::{      response::{self, Responder},      Request, Response, State,  }; -use std::{ -    ops::{Deref, Range}, -    path::{Path, PathBuf}, -}; +use std::ops::{Deref, Range};  use tokio::io::{duplex, DuplexStream};  use tokio_util::io::SyncIoBridge; -pub fn stream_uri(path: &Path, tracks: &[u64], webm: bool) -> String { +pub fn stream_uri(id: &str, tracks: &[u64], webm: bool) -> String {      format!(          "/stream/{}?tracks={}&webm={}", -        path.to_str().unwrap(), +        id,          tracks              .iter()              .map(|v| format!("{v}")) @@ -35,16 +33,27 @@ pub fn stream_uri(path: &Path, tracks: &[u64], webm: bool) -> String {      )  } -#[get("/stream/<path..>?<tracks>&<webm>")] +#[get("/stream/<id>?<tracks>&<webm>")]  pub fn r_stream(      _sess: Session, -    path: PathBuf, +    id: String,      webm: Option<bool>,      tracks: String,      remuxer: &State<RemuxerContext>, -    library: &State<Library>, +    db: &State<Database>,      range: Option<RequestRange>,  ) -> Result<StreamResponse, MyError> { +    let node = db.node.get(&id)?.ok_or(anyhow!("node does not exist"))?; +    let source = node +        .private +        .source +        .ok_or(anyhow!("item does not contain media"))?; + +    let source_tracks = match source { +        MediaSource::Local { tracks } => tracks, +        _ => Err(anyhow!("todo"))?, +    }; +      info!(          "stream request (range={})",          range @@ -52,12 +61,9 @@ pub fn r_stream(              .map(|r| r.to_cr_hv())              .unwrap_or(format!("none"))      ); +      let (a, b) = duplex(4096); -    let path = path.to_str().unwrap().to_string(); -    let item = library -        .nested(&path) -        .context("retrieving library node")? -        .get_item()?; +      let remuxer = remuxer.deref().clone();      let tracks = tracks          .split(',') @@ -74,13 +80,13 @@ pub fn r_stream(          None => 0..(isize::MAX as usize),      }; -    let path_base = library.root_path.clone();      tokio::task::spawn_blocking(move || {          if let Err(e) = remuxer.generate_into(              b,              urange, -            path_base, -            item.info.clone(), +            CONF.library_path.clone(), +            node.public, +            source_tracks,              tracks,              webm.unwrap_or(false),          ) { diff --git a/server/src/routes/ui/assets.rs b/server/src/routes/ui/assets.rs new file mode 100644 index 0000000..2c0b85a --- /dev/null +++ b/server/src/routes/ui/assets.rs @@ -0,0 +1,44 @@ +/* +    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) 2023 metamuffin <metamuffin.org> +*/ +use crate::{ +    database::Database, +    routes::ui::{account::session::Session, error::MyError, CacheControlFile}, +    CONF, +}; +use anyhow::anyhow; +use log::info; +use rocket::{get, http::ContentType, FromFormField, State, UriDisplayQuery}; +use std::{path::PathBuf, str::FromStr}; +use tokio::fs::File; + +#[derive(FromFormField, UriDisplayQuery)] +pub enum AssetRole { +    Poster, +    Backdrop, +} + +#[get("/item_assets/<id>?<role>")] +pub async fn r_item_assets( +    _sess: Session, +    id: String, +    role: AssetRole, +    db: &State<Database>, +) -> Result<(ContentType, CacheControlFile), MyError> { +    let node = db.node.get(&id)?.ok_or(anyhow!("node does not exist"))?; +    let path = CONF.asset_path.join( +        match role { +            AssetRole::Backdrop => node.private.backdrop, +            AssetRole::Poster => node.private.poster, +        } +        .unwrap_or_else(|| PathBuf::from_str("fallback.jpeg").unwrap()), +    ); +    info!("loading asset from {path:?}"); +    let ext = path.extension().unwrap().to_str().unwrap(); +    Ok(( +        ContentType::from_extension(ext).unwrap(), +        CacheControlFile::new(File::open(path).await?).await, +    )) +} diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs index 30eb3f2..767e411 100644 --- a/server/src/routes/ui/browser.rs +++ b/server/src/routes/ui/browser.rs @@ -3,35 +3,24 @@      which is licensed under the GNU Affero General Public License (version 3); see /COPYING.      Copyright (C) 2023 metamuffin <metamuffin.org>  */ -use super::{account::session::Session, error::MyError, layout::DynLayoutPage, node::PosterCard}; -use crate::library::{Library, Node}; +use super::{account::session::Session, error::MyError, layout::DynLayoutPage}; +use crate::database::Database;  use rocket::{get, State}; -use std::collections::VecDeque;  #[get("/items")] -pub fn r_all_items(_sess: Session, library: &State<Library>) -> Result<DynLayoutPage<'_>, MyError> { -    let mut dirs = VecDeque::from_iter(Some(library.root.get_directory().unwrap())); -    let mut items = Vec::new(); -    while let Some(d) = dirs.pop_front() { -        for e in &d.children { -            match e.as_ref() { -                Node::Directory(d) => dirs.push_back(d.clone()), -                Node::Item(i) => items.push(i.clone()), -            } -        } -    } +pub fn r_all_items(_sess: Session, db: &State<Database>) -> Result<DynLayoutPage<'_>, MyError> {      Ok(super::layout::LayoutPage {          title: "All Items".to_owned(),          content: markup::new! {              .page.dir {                  h1 { "All Items" } -                ul.directorylisting { @for item in &items { -                    li {@PosterCard { -                        wide: false, dir: false, -                        path: item.lib_path.clone(), -                        title: &item.info.title -                    }} -                }} +                // ul.directorylisting { @for item in &items { +                //     li {@PosterCard { +                //         wide: false, dir: false, +                //         path: item.lib_path.clone(), +                //         title: &item.info.title +                //     }} +                // }}              }          },          ..Default::default() diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index cdde478..0b85e89 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -5,20 +5,20 @@  */  use super::{account::session::Session, layout::LayoutPage};  use crate::{ -    library::Library, -    routes::ui::{error::MyResult, layout::DynLayoutPage, node::NodePage}, +    database::Database, +    routes::ui::{error::MyResult, layout::DynLayoutPage},      CONF,  };  use rocket::{get, State};  use tokio::fs::read_to_string;  #[get("/")] -pub fn r_home(_sess: Session, library: &State<Library>) -> DynLayoutPage { +pub fn r_home(_sess: Session, _db: &State<Database>) -> DynLayoutPage {      LayoutPage {          title: "Home".to_string(),          content: markup::new! {              p { "Welcome to " @CONF.brand  } -            @NodePage { node: &library.root } +            // @NodePage { node: &db }          },          ..Default::default()      } diff --git a/server/src/routes/ui/mod.rs b/server/src/routes/ui/mod.rs index f566f6d..5fad2b7 100644 --- a/server/src/routes/ui/mod.rs +++ b/server/src/routes/ui/mod.rs @@ -29,6 +29,7 @@ pub mod layout;  pub mod node;  pub mod player;  pub mod style; +pub mod assets;  pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 4d599dc..0ae0d9e 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -1,163 +1,141 @@  /* -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) 2023 metamuffin <metamuffin.org> +    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) 2023 metamuffin <metamuffin.org>  */ -use super::error::MyError; -use super::player::player_uri; -use super::CacheControlFile; -use crate::uri; +use super::{assets::rocket_uri_macro_r_item_assets, error::MyError, player::player_uri};  use crate::{ -    library::{Directory, Item, Library, Node}, +    database::Database,      routes::ui::{          account::session::Session, +        assets::AssetRole,          layout::{DynLayoutPage, LayoutPage},      }, +    uri,  }; -use anyhow::Context; -use jellycommon::DirectoryKind; -use log::info; -use rocket::{get, http::ContentType, State}; -use rocket::{FromFormField, UriDisplayQuery}; -use std::{path::PathBuf, sync::Arc}; -use tokio::fs::File; +use anyhow::{anyhow, Context}; +use jellycommon::{Node, NodeKind}; +use rocket::{get, State}; -#[get("/library/<path..>")] +#[get("/library/<id>")]  pub async fn r_library_node(      _sess: Session, -    path: PathBuf, -    library: &State<Library>, +    id: String, +    db: &State<Database>,  ) -> Result<DynLayoutPage<'_>, MyError> { -    let node = library -        .nested_path(&path) -        .context("retrieving library node")?; +    let node = db +        .node +        .get(&id) +        .context("retrieving library node")? +        .ok_or(anyhow!("node does not exist"))?; + +    let children = node +        .public +        .children +        .iter() +        .map(|c| { +            Ok(( +                c.to_owned(), +                db.node.get(c)?.ok_or(anyhow!("child does not exist"))?, +            )) +        }) +        .collect::<anyhow::Result<Vec<_>>>()? +        .into_iter() +        .collect(); +      Ok(LayoutPage { -        title: node.common().title.to_string(), -        show_back: node.get_item().is_ok(), +        title: node.public.title.to_string(), +        show_back: matches!(node.public.kind, NodeKind::Movie | NodeKind::Episode),          content: markup::new! { -            @NodePage { node: &node } +            @NodePage { node: &node, id: &id, children: &children }          },          ..Default::default()      })  }  markup::define! { -    NodePage<'a>(node: &'a Arc<Node>) { -        @match node.as_ref() { -            Node::Directory(dir) => { @match dir.info.kind { -                DirectoryKind::Series => { @SeriesPage { dir } } -                _ => { @DirectoryPage { dir } } -            } } -            Node::Item(item) => { @ItemPage { item } } +    NodePage<'a>(id: &'a str, node: &'a Node, children: &'a Vec<(String,Node)>) { +        @match node.public.kind { +            NodeKind::Collection | NodeKind::Show | NodeKind::Season => { @DirectoryPage { node, id, children } } +            NodeKind::Series => { @SeriesPage { node, children, id } } +            NodeKind::Movie | NodeKind::Episode => { @ItemPage { node, id } }          }      } -    NodeCard<'a>(node: &'a Arc<Node>) { -        @match node.as_ref() { -            Node::Directory(dir) => {@PosterCard { -                wide: !matches!(dir.info.kind, DirectoryKind::Series | DirectoryKind::Season), -                dir: true, -                path: dir.lib_path.clone(), -                title: &dir.info.title -            }} -            Node::Item(item) => {@PosterCard { -                wide: false, dir: false, -                path: item.lib_path.clone(), -                title: &item.info.title -            }} +    NodeCard<'a>(id: &'a str, node: &'a Node) { +        @PosterCard { +            wide: matches!(node.public.kind, NodeKind::Collection), +            dir: !matches!(node.public.kind, NodeKind::Movie | NodeKind::Episode), +            id, +            title: &node.public.title          }      } -    DirectoryPage<'a>(dir: &'a Arc<Directory>) { +    DirectoryPage<'a>(id: &'a str, node: &'a Node, children: &'a Vec<(String,Node)>) {          div.page.dir { -            h1 { @dir.info.title } -            @if let Some(parent) = dir.lib_path.parent() { -                a.dirup[href=uri!(r_library_node(&parent))] { "Go up" } -            } +            h1 { @node.public.title } +            // @if let Some(parent) = node.lib_path.parent() { +            //     a.dirup[href=uri!(r_library_node(&parent))] { "Go up" } +            // }              ul.directorylisting { -                @for node in &dir.children { -                    li { @NodeCard { node } } +                @for (id, node) in children.iter() { +                    li { @NodeCard { id, node } }                  }              }          }      } -    PosterCard<'a>(path: PathBuf, title: &'a str, wide: bool, dir: bool) { +    PosterCard<'a>(id: &'a str, title: &'a str, wide: bool, dir: bool) {          div[class=if *wide {"card wide poster"} else {"card poster"}] {              div.banner { -                a[href=uri!(r_library_node(path))] { -                    img[src=uri!(r_item_assets(path, AssetRole::Poster))]; +                a[href=uri!(r_library_node(id))] { +                    img[src=uri!(r_item_assets(id, AssetRole::Poster))];                  }                  @if *dir { -                    div.hoverdir { a[href=&uri!(r_library_node(path))] { "Open" } } +                    div.hoverdir { a[href=&uri!(r_library_node(id))] { "Open" } }                  } else { -                    div.hoveritem { a[href=&player_uri(path)] { "▶" } } +                    div.hoveritem { a[href=&player_uri(id)] { "▶" } }                  }              }              p.title { -                a[href=uri!(r_library_node(path))] { +                a[href=uri!(r_library_node(id))] {                      @title                  }              }          }      } -    ItemPage<'a>(item: &'a Arc<Item>) { +    ItemPage<'a>(id: &'a str, node: &'a Node) {          // TODO different image here -        img.backdrop[src=uri!(r_item_assets(&item.lib_path, AssetRole::Backdrop))]; +        img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop))];          div.page.item {              div.banner { -                img[src=uri!(r_item_assets(&item.lib_path, AssetRole::Poster))]; +                img[src=uri!(r_item_assets(id, AssetRole::Poster))];              }              div.title { -                h1 { @item.info.title } +                h1 { @node.public.title }                  // TODO release date, duration, ratings -                a.play[href=&player_uri(&item.lib_path)] { "Watch now" } +                a.play[href=&player_uri(id)] { "Watch now" }              }              div.details { -                h3 { @item.info.tagline } -                p { @item.info.description } +                h3 { @node.public.tagline } +                p { @node.public.description }              }          }      } -    SeriesPage<'a>(dir: &'a Arc<Directory>) { +    SeriesPage<'a>(id: &'a str, node: &'a Node, children: &'a Vec<(String,Node)>) {          // TODO different image here -        img.backdrop[src=uri!(r_item_assets(&dir.lib_path, AssetRole::Backdrop))]; +        img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop))];          div.page.item {              div.banner { -                img[src=uri!(r_item_assets(&dir.lib_path, AssetRole::Poster))]; +                img[src=uri!(r_item_assets(id, AssetRole::Poster))];              }              div.title { -                h1 { @dir.info.title } +                h1 { @node.public.title }              }              div.details { -                h3 { @dir.info.tagline } -                p { @dir.info.description } +                h3 { @node.public.tagline } +                p { @node.public.description }              } -            ol { @for ep in &dir.children { -                li { a[href=uri!(r_library_node(ep.lib_path()))] { @ep.common().title } } +            ol { @for (id, c) in children.iter() { +                li { a[href=uri!(r_library_node(id))] { @c.public.title } }              } }          }      }  } - -#[derive(FromFormField, UriDisplayQuery)] -pub enum AssetRole { -    Poster, -    Backdrop, -} - -#[get("/item_assets/<path..>?<role>")] -pub async fn r_item_assets( -    _sess: Session, -    path: PathBuf, -    role: AssetRole, -    library: &State<Library>, -) -> Result<(ContentType, CacheControlFile), MyError> { -    let node = library -        .nested_path(&path) -        .context("retrieving library node")?; -    let path = node.get_asset(library, role); -    info!("loading asset from {path:?}"); -    let ext = path.extension().unwrap().to_str().unwrap(); -    Ok(( -        ContentType::from_extension(ext).unwrap(), -        CacheControlFile::new(File::open(path).await?).await, -    )) -} diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index d5cb685..0e87749 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -5,27 +5,24 @@  */  use super::{account::session::Session, layout::LayoutPage};  use crate::{ -    library::{Item, Library}, +    database::Database,      routes::{          stream::stream_uri,          ui::{ +            assets::{rocket_uri_macro_r_item_assets, AssetRole},              error::MyResult,              layout::DynLayoutPage, -            node::{rocket_uri_macro_r_item_assets, AssetRole},          },      },      uri,  }; -use jellycommon::SourceTrackKind; +use anyhow::anyhow; +use jellycommon::{Node, SourceTrackKind};  use markup::DynRender;  use rocket::{get, FromForm, State}; -use std::{ -    path::{Path, PathBuf}, -    sync::Arc, -}; -pub fn player_uri(path: &Path) -> String { -    format!("/player/{}", path.to_str().unwrap()) +pub fn player_uri(id: &str) -> String { +    format!("/player/{}", id)  }  #[derive(FromForm, Default, Clone, Debug)] @@ -36,14 +33,14 @@ pub struct PlayerConfig {      pub webm: bool,  } -#[get("/player/<path..>?<conf..>", rank = 4)] +#[get("/player/<id>?<conf..>", rank = 4)]  pub fn r_player(      _sess: Session, -    library: &State<Library>, -    path: PathBuf, +    db: &State<Database>, +    id: String,      conf: PlayerConfig,  ) -> MyResult<DynLayoutPage<'_>> { -    let item = library.nested_path(&path)?.get_item()?; +    let item = db.node.get(&id)?.ok_or(anyhow!("node does not exist"))?;      let tracks = None          .into_iter()          .chain(conf.v.into_iter()) @@ -51,27 +48,35 @@ pub fn r_player(          .chain(conf.s.into_iter())          .collect::<Vec<_>>(); +    let conf = player_conf(item.clone(), !tracks.is_empty())?;      Ok(LayoutPage { -        title: item.info.title.to_owned(), +        title: item.public.title.to_owned(),          class: Some("player"),          content: markup::new! {              @if tracks.is_empty() { -                img.backdrop[src=uri!(r_item_assets(&item.lib_path, AssetRole::Backdrop)).to_string()]; +                img.backdrop[src=uri!(r_item_assets(&id, AssetRole::Backdrop)).to_string()];              } else { -                video[src=stream_uri(&item.lib_path, &tracks, true), controls]{} +                video[src=stream_uri(&id, &tracks, true), controls]{}              } -            @player_conf(item.clone(), !tracks.is_empty()) +            @conf          },          show_back: true,          ..Default::default()      })  } -pub fn player_conf<'a>(item: Arc<Item>, playing: bool) -> DynRender<'a> { +pub fn player_conf<'a>(item: Node, playing: bool) -> anyhow::Result<DynRender<'a>> {      let mut audio_tracks = vec![];      let mut video_tracks = vec![];      let mut sub_tracks = vec![]; -    for (tid, track) in item.info.tracks.clone() { +    let tracks = item +        .public +        .media +        .clone() +        .ok_or(anyhow!("node does not have media"))? +        .tracks +        .clone(); +    for (tid, track) in tracks.into_iter().enumerate() {          match &track.kind {              SourceTrackKind::Audio { .. } => audio_tracks.push((tid, track)),              SourceTrackKind::Video { .. } => video_tracks.push((tid, track)), @@ -79,9 +84,9 @@ pub fn player_conf<'a>(item: Arc<Item>, playing: bool) -> DynRender<'a> {          }      } -    markup::new! { +    Ok(markup::new! {          form.playerconf[method = "GET", action = ""] { -            h2 { "Select tracks for " @item.info.title } +            h2 { "Select tracks for " @item.public.title }              fieldset.video {                  legend { "Video" } @@ -115,5 +120,5 @@ pub fn player_conf<'a>(item: Arc<Item>, playing: bool) -> DynRender<'a> {              input[type="submit", value=if playing { "Change tracks" } else { "Start playback" }];          } -    } +    })  }  |