Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ rand_core = "0.6.4"
rand_xoshiro = "0.6.0"
percent-encoding = "2.3.1"
chrono = { version = "0.4.38", features = ["serde", "clock"], default-features = false }
attohttpc = { version = "0.28.0", features = ["basic-auth", "compress", "tls-rustls-webpki-roots"], default-features = false}
url = "2.5.4"
1 change: 1 addition & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod settings;
pub mod font;
pub mod context;
pub mod gesture;
pub mod opds;

pub use anyhow;
pub use fxhash;
Expand Down
224 changes: 224 additions & 0 deletions crates/core/src/opds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use std::fmt::Display;
use std::{fs::File, io::Write, path::PathBuf, str::FromStr};

use anyhow::{format_err, Error};
use attohttpc::Response;
use url::{Position, Url};

use crate::document::html::xml::XmlParser;
use crate::helpers::decode_entities;
use crate::settings::OpdsSettings;

#[derive(PartialEq, Debug, Clone)]
pub enum MimeType {
Epub,
Cbz,
Pdf,
OpdsCatalog,
OpdsEntry,
Other(String),
}

impl Display for MimeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match *self {
MimeType::Epub => "epub".to_string(),
MimeType::Cbz => "cbz".to_string(),
MimeType::Pdf => "pdf".to_string(),
MimeType::OpdsCatalog => "xml".to_string(),
MimeType::OpdsEntry => "xml".to_string(),
MimeType::Other(ref s) => s.to_string(),
};
write!(f, "{}", str)
}
}

impl FromStr for MimeType {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"application/epub+zip" => Ok(MimeType::Epub),
"application/x-cbz" => Ok(MimeType::Cbz),
"application/pdf" => Ok(MimeType::Pdf),
"application/atom+xml;profile=opds-catalog" => Ok(MimeType::OpdsCatalog),
"application/atom+xml;type=entry;profile=opds-catalog" => Ok(MimeType::OpdsEntry),
_ => Ok(MimeType::Other(s.to_string())),
}
}
}

#[derive(PartialEq, Debug, Clone)]
pub enum LinkType {
Acquisition,
Cover,
Thumbnail,
Sample,
OpenAccess,
Borrow,
Buy,
Subscribe,
Subsection,
Other(String),
}

impl FromStr for LinkType {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"http://opds-spec.org/acquisition" => Ok(LinkType::Acquisition),
"http://opds-spec.org/image" => Ok(LinkType::Cover),
"http://opds-spec.org/image/thumbnail" => Ok(LinkType::Thumbnail),
"http://opds-spec.org/acquisition/sample" => Ok(LinkType::Sample),
"http://opds-spec.org/acquisition/preview" => Ok(LinkType::Sample),
"http://opds-spec.org/acquisition/open-access" => Ok(LinkType::OpenAccess),
"http://opds-spec.org/acquisition/borrow" => Ok(LinkType::Borrow),
"http://opds-spec.org/acquisition/buy" => Ok(LinkType::Buy),
"http://opds-spec.org/acquisition/subscribe" => Ok(LinkType::Subscribe),
"subsection" => Ok(LinkType::Subsection),
_ => Ok(LinkType::Other(s.to_string())),
}
}
}

#[derive(Default, Debug, Clone)]
pub struct Feed {
pub title: String,
pub entries: Vec<Entry>,
// pub links: Vec<Link>,
}

#[derive(Default, Debug, Clone)]
pub struct Entry {
pub id: String,
pub title: String,
pub author: Option<String>,
pub links: Vec<Link>,
}

#[derive(Default, Debug, Clone)]
pub struct Link {
pub rel: Option<LinkType>,
pub href: Option<String>,
pub mime_type: Option<MimeType>,
}

#[derive(Debug, Clone)]
pub struct OpdsFetcher {
pub settings: OpdsSettings,
pub root_url: Url,
pub base_url: Url,
}

impl OpdsFetcher {
pub fn new(settings: OpdsSettings) -> Result<OpdsFetcher, Error> {
let root_url = Url::parse(&settings.url)?;
let base_url = Url::parse(&root_url[..Position::BeforePath])?;

Ok(OpdsFetcher {
settings: settings,
root_url: root_url,
base_url: base_url,
})
}

pub fn download_relative(&self, path: &str, file_path: &PathBuf) -> Result<File, Error> {
let full_url = Url::join(&self.base_url, path)?;
let mut file = File::create(&file_path)?;
let response: Response = self.request(&full_url)?;
//TODO check success
let bytes = response.bytes()?;
let _ = file.write(&bytes);
return Ok(file);
}

pub fn home(&self) -> Result<Feed, Error> {
let response = self.request(&self.root_url)?;
//TODO check success
return OpdsFetcher::parse_feed(response);
}

pub fn pull_relative(&self, path: &str) -> Result<Feed, Error> {
let full_url = Url::join(&self.base_url, path)?;
let response = self.request(&full_url)?;
//TODO check success
return OpdsFetcher::parse_feed(response);
}

fn parse_feed(response: Response) -> Result<Feed, Error> {
let body = response.text()?;
let root = XmlParser::new(&body).parse();
//println!("{:?}", body);
let feed = root
.root()
.find("feed")
.ok_or_else(|| format_err!("feed is missing"))?;
let mut entries = Vec::new();
let mut title = String::new();
for child in feed.children() {
if child.tag_name() == Some("title") {
title = decode_entities(&child.text()).into_owned();
}
if child.tag_name() == Some("entry") {
let mut find_id = None;
let mut find_title = None;
let mut author = None;
let mut links = Vec::new();
for entry_child in child.children() {
if entry_child.tag_name() == Some("id") {
find_id = Some(entry_child.text());
}
if entry_child.tag_name() == Some("title") {
find_title = Some(decode_entities(&entry_child.text()).into_owned());
}
if entry_child.tag_name() == Some("author") {
if let Some(name) = entry_child.find("name") {
author = Some(decode_entities(&name.text()).into_owned());
}
}
if entry_child.tag_name() == Some("link") {
let rel = entry_child
.attribute("rel")
.map(|s| LinkType::from_str(s).ok())
.flatten();
let href = entry_child.attribute("href").map(String::from);
let mime_type = entry_child
.attribute("type")
.map(|s| MimeType::from_str(s).ok())
.flatten();
links.push(Link {
rel: rel,
href: href,
mime_type: mime_type,
});
}
}
//TODO error
if let (Some(id), Some(title)) = (find_id, find_title) {
let entry = Entry {
id: id,
title: title,
author: author,
links: links,
};
//println!("{:#?}", entry);
entries.push(entry);
}
}
}
Ok(Feed {
title: title,
entries: entries,
})
}

fn request(&self, url: &Url) -> Result<Response, Error> {
let mut request_builder = attohttpc::get(url);
if let Some(username) = self.settings.username.clone() {
request_builder = request_builder.basic_auth(username, self.settings.password.clone());
}
let response = request_builder.send()?;
return Ok(response);
}
}
32 changes: 32 additions & 0 deletions crates/core/src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub struct Settings {
pub calculator: CalculatorSettings,
pub battery: BatterySettings,
pub frontlight_levels: LightLevels,
pub opds: Vec<OpdsSettings>
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -489,6 +490,28 @@ impl Default for BatterySettings {
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct OpdsSettings {
pub name: String,
pub url: String,
pub username: Option<String>,
pub password: Option<String>,
pub download_location: PathBuf,
}

impl Default for OpdsSettings {
fn default() -> Self {
OpdsSettings {
name: "Unnamed".to_string(),
url: String::default(),
username: None,
password: None,
download_location: PathBuf::default()
}
}
}

impl Default for Settings {
fn default() -> Self {
Settings {
Expand Down Expand Up @@ -551,6 +574,15 @@ impl Default for Settings {
battery: BatterySettings::default(),
frontlight_levels: LightLevels::default(),
frontlight_presets: Vec::new(),
opds: vec![
OpdsSettings {
name: "Project Gutenberg".to_string(),
url: "https://m.gutenberg.org/ebooks.opds/".to_string(),
username: None,
password: None,
download_location: PathBuf::from(INTERNAL_CARD_ROOT)
}
],
}
}
}
Loading