/* 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) 2024 metamuffin */ use anyhow::Result; use jellycommon::user::CreateSessionParams; use log::{debug, info}; use reqwest::{ header::{HeaderMap, HeaderValue}, Client, }; use stream::StreamSpec; use tokio::io::AsyncWriteExt; pub use jellycommon::*; #[derive(Debug, Clone)] pub struct Instance { pub host: String, pub tls: bool, } impl Instance { pub fn new(host: String, tls: bool) -> Self { Self { host, tls } } pub fn base(&self) -> String { format!( "{}://{}", if self.tls { "https" } else { "http" }, self.host ) } pub async fn login(self, data: CreateSessionParams) -> anyhow::Result { let session_token = Client::builder() .build()? .post(format!("{}/api/create_session", self.base())) .json(&data) .send() .await? .json() .await?; let mut headers = HeaderMap::new(); headers.insert( "Cookie", HeaderValue::from_str(&format!("session={session_token}")).unwrap(), ); headers.insert("Accept", HeaderValue::from_static("application/json")); Ok(Session { instance: self, session_token, client: Client::builder().default_headers(headers).build().unwrap(), }) } } pub struct Session { client: Client, instance: Instance, session_token: String, } pub trait UnpinWrite: tokio::io::AsyncWrite + std::marker::Unpin {} impl UnpinWrite for T {} impl Session { fn session_param(&self) -> String { format!("session={}", self.session_token) } pub async fn node(&self, id: &str) -> Result { debug!("downloading node {id:?}"); Ok(self .client .get(format!("{}/n/{id}", self.instance.base())) .send() .await? .error_for_status()? .json() .await?) } pub async fn node_extended(&self, id: &str) -> Result { debug!("downloading extended node {id:?}"); Ok(self .client .get(format!("{}/n/{id}/extended", self.instance.base())) .send() .await? .error_for_status()? .json() .await?) } pub async fn node_asset( &self, writer: impl UnpinWrite, id: &str, role: AssetRole, width: usize, ) -> Result<()> { debug!("downloading asset {role:?} for {id:?}"); self.download_url( writer, format!( "{}/n/{id}/asset?role={}&width={width}", self.instance.base(), role.as_str() ), ) .await } pub async fn node_thumbnail( &self, writer: impl UnpinWrite, id: &str, width: usize, time: f64, ) -> Result<()> { debug!("downloading thumbnail for {id:?} at {time}s"); self.download_url( writer, format!( "{}/n/{id}/thumbnail?t={time}&width={width}", self.instance.base(), ), ) .await } pub async fn asset(&self, writer: impl UnpinWrite, token: &str, width: usize) -> Result<()> { debug!("downloading asset {token:?} (w={width})"); self.download_url( writer, format!("{}/asset/{token}?width={width}", self.instance.base(),), ) .await } pub async fn stream( &self, writer: impl UnpinWrite, id: &str, stream_spec: &StreamSpec, ) -> Result<()> { self.download_url(writer, self.stream_url(id, stream_spec)) .await } pub fn stream_url(&self, id: &str, stream_spec: &StreamSpec) -> String { format!( "{}/n/{}/stream?{}&{}", self.instance.base(), id, stream_spec.to_query(), self.session_param() ) } pub async fn download_url(&self, mut writer: impl UnpinWrite, url: String) -> Result<()> { let mut r = self.client.get(url).send().await?.error_for_status()?; while let Some(chunk) = r.chunk().await? { writer.write_all(&chunk).await?; } Ok(()) } pub async fn reimport(&self) -> Result<()> { // TODO error handling info!("reimport"); self.client .post(format!("{}/admin/import", self.instance.base())) .send() .await?; info!("done"); Ok(()) } }