mango-client 0.2.2

Fetch data from selfhosted mango instances
Documentation
use std::error::Error;
use std::fmt;
use crate::opds::{OpdsClient, Link};
use image;
use image::imageops::FilterType;
use image::DynamicImage;

fn to_owned(borrowed: &Option<&str>) -> Option<String> {
    match borrowed {
        Some(val) => Some(String::from(val.to_owned())),
        None => None
    }
}

fn find_link<'a>(links: &'a Vec<Link>, filter_rel: Option<&str>, filter_type: Option<&str>) -> Result<&'a Link, LinkNotFoundError> {
    for link in links.iter() {
        if filter_rel.is_some() && link.rel != filter_rel.clone().unwrap() {
            continue;
        }

        if filter_type.is_some() &&
            ( link.link_type.is_none() || link.link_type.clone().unwrap() != filter_type.clone().unwrap() ) {
            continue;
        }

        return Ok(link);
    }

    //Err(error_text.unwrap_or("find_link did not find a No suitable Link found with find_link!").into_error())
    Err(LinkNotFoundError {
        filter_rel: to_owned(&filter_rel),
        filter_type: to_owned(&filter_type),
        candidates: links.clone()
    })
}

pub struct MangoClient {
    opds_client: OpdsClient
}


impl MangoClient {
    pub fn new(opds_client: OpdsClient) -> MangoClient {
        MangoClient { opds_client }
    }

    pub async fn library(&self) -> Result<Library, Box<dyn Error>> {
        let result = self.opds_client.get("/opds").await?;

        let mut library_entries: Vec<LibraryEntry> = Vec::new();

        for entry in result.entries.unwrap().iter() {
            library_entries.push(LibraryEntry {
                title: entry.title.clone(),
                book_path: find_link(&entry.links, Some("subsection"), None)?.href.clone()
            });
        }

        Ok(Library {
            title: result.title,
            entries: library_entries
        })
    }

    pub async fn book(&self, library_entry: &LibraryEntry) -> Result<Book, Box<dyn Error>> {
        let result = self.opds_client.get(&library_entry.book_path).await?;

        let mut chapters: Vec<BookChapter> = Vec::new();

        for entry in result.entries.unwrap().iter() {
            chapters.push(BookChapter {
                title: entry.title.clone(),
                thumbnail_url: format!("{}{}", self.opds_client.base_url(), find_link(&entry.links,
                                                                                      Some("http://opds-spec.org/image/thumbnail"),
                                                                                      None)?.href),
                download_url: format!("{}{}", self.opds_client.base_url(), find_link(&entry.links,
                                                                                     Some("http://opds-spec.org/acquisition"),
                                                                                     Some("application/vnd.comicbook+zip"))?.href),
            })
        }

        Ok(Book {
            title: result.title.clone(),
            chapters
        })
    }

    /// Important: If this client has never called library() or book() before,
    /// this will return an html page prompting you to login instead!
    /// When using the opds api, a session cookie gets set that is necessary,
    /// for downloading typical resource. The authentication header will not suffice!
    pub async fn download_raw_thumbnail(&self, chapter: &BookChapter) -> Result<Vec<u8>, Box<dyn Error>> {
        self.opds_client.get_resource(&chapter.thumbnail_url).await
    }

    /// Warning: This method will be at least 30-45 times slower
    /// in debug mode, no matter how beefy a PC you possess!
    /// Example I had (with a Ryzen 3700X):
    ///  - Debug: Download (2s) -> Load Image (1-2s) -> Resize Image (1-3s)
    ///  - Release: Download (2s) -> Load Image (1ms-60ms) -> Resize Image (15ms-40ms)
    pub async fn download_thumbnail(&self, chapter: &BookChapter, max_size: Option<(u16, u16)>) -> Result<DynamicImage, Box<dyn Error>> {
        let data = self.download_raw_thumbnail(&chapter).await?;
        
        let mut img = image::load_from_memory(&data)?;
        if let Some(max_size) = max_size {
            img = img.resize(max_size.0 as u32, max_size.1 as u32, FilterType::CatmullRom);
        }
        
        Ok(img)
    }

    /// Important: If this client has never called library() or book() before,
    /// this will return an html page prompting you to login instead!
    /// When using the opds api, a session cookie gets set that is necessary,
    /// for downloading typical resource. The authentication header will not suffice!
    pub async fn download_chapter(&self, chapter: &BookChapter) -> Result<Vec<u8>, Box<dyn Error>> {
        self.opds_client.get_resource(&chapter.download_url).await
    }
}

#[derive(Debug, Clone)]
pub struct Library {
    pub title: String,
    pub entries: Vec<LibraryEntry>
}

#[derive(Debug, Clone)]
pub struct LibraryEntry {
    pub title: String,
    book_path: String,
}

#[derive(Debug, Clone)]
pub struct Book {
    pub title: String,
    pub chapters: Vec<BookChapter>
}

#[derive(Debug, Clone)]
pub struct BookChapter {
    pub title: String,
    pub thumbnail_url: String,
    pub download_url: String
}


#[derive(Debug)]
pub struct LinkNotFoundError {
    pub filter_type: Option<String>,
    pub filter_rel: Option<String>,
    pub candidates: Vec<Link>
}

impl Error for LinkNotFoundError { }

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