mango-client 0.2.2

Fetch data from selfhosted mango instances
Documentation
use std::error::Error;
use strerror::prelude::*;
use reqwest;
use reqwest::header::AUTHORIZATION;
use base64;
use serde;
use serde::Deserialize;
use serde_xml_rs;
use std::fmt;
use bytes::Bytes;


///
/// Some semi-accurate implementation of opds.
/// Works for Mango (https://github.com/hkalexling/Mango) and may work
/// for other opds supported services, but is not guaranteed.
#[derive(Debug)]
pub struct OpdsClient {
    base_url: String,
    client: reqwest::Client,
    authorization_value: String
}


impl OpdsClient {
    pub fn new(base_url: &str, username: &str, password: &str) -> OpdsClient {
        let mut base_url_no_trailing_slash = base_url.to_owned();

        // Remove trailing slash
        if base_url_no_trailing_slash.ends_with("/") {
            base_url_no_trailing_slash.pop();
        }

        let auth_base64 = base64::encode(format!("{}:{}", username, password).as_bytes());

        let client = reqwest::ClientBuilder::new().cookie_store(true).build().expect("Failed to build client");
        OpdsClient {
            base_url: base_url_no_trailing_slash,
            client,
            authorization_value: format!("Basic {}", auth_base64)
        }
    }

    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    pub async fn get(&self, path: &str) -> Result<OpdsEntry, Box<dyn Error>> {
        // Get response
        let response = self.client.get(&format!("{}{}", self.base_url, path).replace("//", "/"))
            .header(AUTHORIZATION, &self.authorization_value)
            .send().await?.error_for_status()?;
        
        let raw_xml = response.text().await?;

        if ! path.starts_with("/") {
            return Err(Box::new("Only paths starting with / are currently supported.".into_error()));
        }

        // Parse xml
        match serde_xml_rs::from_str(&raw_xml) {
            Ok(entry) => Ok(entry),
            Err(e) => Err(Box::new(OpdsEntryParsingError{
                xml: raw_xml,
                source_error: Box::new(e)
            }))
        }
    }

    pub async fn get_resource(&self, url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
        let response = self.client.get(url)
            .header(AUTHORIZATION, &self.authorization_value)
            .send().await?.error_for_status()?;
        
        let data: Bytes = response.bytes().await?;
        Ok(data.to_vec())
    }
}


#[derive(Deserialize, Debug)]
pub struct OpdsEntry {
    pub title: String,
    pub id: String,
    pub author: Option<Author>,
    #[serde(rename = "link")]
    pub links: Vec<Link>,
    #[serde(rename = "entry")]
    pub entries: Option<Vec<OpdsEntry>>
}


#[derive(Deserialize, Debug)]
pub struct Author {
    pub name: String,
    pub uri: String
}


#[derive(Deserialize, Debug, Clone)]
pub struct Link {
    #[serde(rename="type")]
    pub link_type: Option<String>,
    pub rel: String,
    pub href: String
}

#[derive(Debug)]
struct OpdsEntryParsingError {
    source_error: Box<dyn Error>,
    xml: String
}

impl Error for OpdsEntryParsingError {
    /*fn cause(&self) -> Option<&dyn Error> {
        // TODO: Get this to work
        Some(&self.source_error)
    }*/
}

impl fmt::Display for OpdsEntryParsingError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:#?}", self)
    }
}