book feed
This commit is contained in:
parent
603c2fbe48
commit
12341d01a6
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -193,6 +193,7 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1310,6 +1311,7 @@ dependencies = [
|
|||||||
"hashlink",
|
"hashlink",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7,6 +7,7 @@ members = [
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = "1.0.200"
|
serde = "1.0.200"
|
||||||
thiserror = "1.0.59"
|
thiserror = "1.0.59"
|
||||||
|
time = { version = "0.3.36", features = ["macros", "serde", "formatting", "parsing" ] }
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -7,6 +7,7 @@ license = { workspace = true }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
r2d2 = "0.8.10"
|
r2d2 = "0.8.10"
|
||||||
r2d2_sqlite = "0.24.0"
|
r2d2_sqlite = "0.24.0"
|
||||||
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
rusqlite = { version = "0.31.0", features = ["bundled", "time"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
time = { workspace = true }
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use rusqlite::{named_params, Connection, Row};
|
use rusqlite::{named_params, Connection, Row};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
error::DataStoreError,
|
error::DataStoreError,
|
||||||
@ -12,6 +13,9 @@ pub struct Book {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
pub uuid: String,
|
||||||
|
pub last_modified: OffsetDateTime,
|
||||||
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Book {
|
impl Book {
|
||||||
@ -21,6 +25,9 @@ impl Book {
|
|||||||
title: row.get(1)?,
|
title: row.get(1)?,
|
||||||
sort: row.get(2)?,
|
sort: row.get(2)?,
|
||||||
path: row.get(3)?,
|
path: row.get(3)?,
|
||||||
|
uuid: row.get(4)?,
|
||||||
|
last_modified: row.get(5)?,
|
||||||
|
description: row.get(6)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +40,8 @@ impl Book {
|
|||||||
let pagination = Pagination::new("sort", cursor, limit, *sort_order);
|
let pagination = Pagination::new("sort", cursor, limit, *sort_order);
|
||||||
pagination.paginate(
|
pagination.paginate(
|
||||||
conn,
|
conn,
|
||||||
"SELECT id, title, sort, path FROM books",
|
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \
|
||||||
|
FROM books LEFT JOIN comments ON books.id = comments.book",
|
||||||
&[],
|
&[],
|
||||||
Self::from_row,
|
Self::from_row,
|
||||||
)
|
)
|
||||||
@ -49,8 +57,9 @@ impl Book {
|
|||||||
let pagination = Pagination::new("books.sort", cursor, limit, sort_order);
|
let pagination = Pagination::new("books.sort", cursor, limit, sort_order);
|
||||||
pagination.paginate(
|
pagination.paginate(
|
||||||
conn,
|
conn,
|
||||||
"SELECT books.id, books.title, books.sort, books.path FROM books \
|
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text FROM books \
|
||||||
INNER JOIN books_authors_link ON books.id = books_authors_link.book \
|
INNER JOIN books_authors_link ON books.id = books_authors_link.book \
|
||||||
|
LEFT JOIN comments ON books.id = comments.book \
|
||||||
WHERE books_authors_link.author = (:author_id) AND",
|
WHERE books_authors_link.author = (:author_id) AND",
|
||||||
&[(":author_id", &author_id)],
|
&[(":author_id", &author_id)],
|
||||||
Self::from_row,
|
Self::from_row,
|
||||||
@ -59,9 +68,10 @@ impl Book {
|
|||||||
|
|
||||||
pub fn series_books(conn: &Connection, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
pub fn series_books(conn: &Connection, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT books.id, books.title, books.sort, books.path FROM series \
|
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text FROM series \
|
||||||
INNER JOIN books_series_link ON series.id = books_series_link.series \
|
INNER JOIN books_series_link ON series.id = books_series_link.series \
|
||||||
INNER JOIN books ON books.id = books_series_link.book \
|
INNER JOIN books ON books.id = books_series_link.book \
|
||||||
|
LEFT JOIN comments ON books.id = comments.book \
|
||||||
WHERE books_series_link.series = (:id) \
|
WHERE books_series_link.series = (:id) \
|
||||||
ORDER BY books.series_index",
|
ORDER BY books.series_index",
|
||||||
)?;
|
)?;
|
||||||
@ -72,7 +82,8 @@ impl Book {
|
|||||||
|
|
||||||
pub fn recents(conn: &Connection, limit: u64) -> Result<Vec<Self>, DataStoreError> {
|
pub fn recents(conn: &Connection, limit: u64) -> Result<Vec<Self>, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, title, sort, path FROM books ORDER BY timestamp DESC LIMIT (:limit)",
|
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \
|
||||||
|
FROM books LEFT JOIN comments ON books.id = comments.book ORDER BY books.timestamp DESC LIMIT (:limit)"
|
||||||
)?;
|
)?;
|
||||||
let params = named_params! { ":limit": limit };
|
let params = named_params! { ":limit": limit };
|
||||||
let iter = stmt.query_map(params, Self::from_row)?;
|
let iter = stmt.query_map(params, Self::from_row)?;
|
||||||
@ -80,7 +91,10 @@ impl Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn scalar_book(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
pub fn scalar_book(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
||||||
let mut stmt = conn.prepare("SELECT id, title, sort, path FROM books WHERE id = (:id)")?;
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \
|
||||||
|
FROM books LEFT JOIN comments WHERE books.id = (:id)",
|
||||||
|
)?;
|
||||||
let params = named_params! { ":id": id };
|
let params = named_params! { ":id": id };
|
||||||
Ok(stmt.query_row(params, Self::from_row)?)
|
Ok(stmt.query_row(params, Self::from_row)?)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use time::error::Parse;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("data store error")]
|
#[error("data store error")]
|
||||||
@ -9,6 +10,8 @@ pub enum DataStoreError {
|
|||||||
SqliteError(rusqlite::Error),
|
SqliteError(rusqlite::Error),
|
||||||
#[error("connection error")]
|
#[error("connection error")]
|
||||||
ConnectionError(#[from] r2d2::Error),
|
ConnectionError(#[from] r2d2::Error),
|
||||||
|
#[error("failed to parse datetime")]
|
||||||
|
DateTimeError(#[from] Parse),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<rusqlite::Error> for DataStoreError {
|
impl From<rusqlite::Error> for DataStoreError {
|
||||||
|
@ -6,7 +6,7 @@ use super::{
|
|||||||
pagination::{Pagination, SortOrder},
|
pagination::{Pagination, SortOrder},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Series {
|
pub struct Series {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -15,7 +15,7 @@ serde_json = "1.0.116"
|
|||||||
serde_with = "3.8.1"
|
serde_with = "3.8.1"
|
||||||
tera = "1.19.1"
|
tera = "1.19.1"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
time = { version = "0.3.36", features = ["macros", "serde", "formatting"] }
|
time = { workspace = true }
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
|
@ -1,21 +1,39 @@
|
|||||||
use std::{collections::HashMap, path::Path};
|
use std::{collections::HashMap, fmt::Display, path::Path};
|
||||||
|
|
||||||
use calibre_db::data::{
|
use calibre_db::data::{
|
||||||
author::Author as DbAuthor, book::Book as DbBook, series::Series as DbSeries,
|
author::Author as DbAuthor, book::Book as DbBook, series::Series as DbSeries,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash)]
|
||||||
|
pub struct Format(pub String);
|
||||||
|
pub type Formats = HashMap<Format, String>;
|
||||||
|
|
||||||
|
impl Display for Format {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self.0.as_ref() {
|
||||||
|
"pdf" => write!(f, "pdf"),
|
||||||
|
"epub" => write!(f, "epub"),
|
||||||
|
_ => write!(f, "unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
pub uuid: String,
|
||||||
|
pub last_modified: OffsetDateTime,
|
||||||
|
pub description: Option<String>,
|
||||||
pub author: DbAuthor,
|
pub author: DbAuthor,
|
||||||
pub series: Option<(DbSeries, f64)>,
|
pub series: Option<(DbSeries, f64)>,
|
||||||
pub formats: HashMap<String, String>,
|
pub formats: Formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Book {
|
impl Book {
|
||||||
@ -23,20 +41,23 @@ impl Book {
|
|||||||
db_book: &DbBook,
|
db_book: &DbBook,
|
||||||
db_series: Option<(DbSeries, f64)>,
|
db_series: Option<(DbSeries, f64)>,
|
||||||
author: DbAuthor,
|
author: DbAuthor,
|
||||||
formats: HashMap<String, String>,
|
formats: Formats,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: db_book.id,
|
id: db_book.id,
|
||||||
title: db_book.title.clone(),
|
title: db_book.title.clone(),
|
||||||
sort: db_book.sort.clone(),
|
sort: db_book.sort.clone(),
|
||||||
path: db_book.path.clone(),
|
path: db_book.path.clone(),
|
||||||
|
uuid: db_book.uuid.clone(),
|
||||||
|
description: db_book.description.clone(),
|
||||||
|
last_modified: db_book.last_modified,
|
||||||
author: author.clone(),
|
author: author.clone(),
|
||||||
series: db_series.map(|x| (x.0, x.1)),
|
series: db_series.map(|x| (x.0, x.1)),
|
||||||
formats,
|
formats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn formats(book: &DbBook, library_path: &Path) -> HashMap<String, String> {
|
fn formats(book: &DbBook, library_path: &Path) -> Formats {
|
||||||
let book_path = library_path.join(&book.path);
|
let book_path = library_path.join(&book.path);
|
||||||
let mut formats = HashMap::new();
|
let mut formats = HashMap::new();
|
||||||
|
|
||||||
@ -48,7 +69,10 @@ impl Book {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(format) = format {
|
if let Some(format) = format {
|
||||||
formats.insert(format, entry.file_name().to_string_lossy().to_string());
|
formats.insert(
|
||||||
|
Format(format),
|
||||||
|
entry.file_name().to_string_lossy().to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,11 @@ use poem::{
|
|||||||
IntoResponse,
|
IntoResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError};
|
use crate::{
|
||||||
|
app_state::AppState,
|
||||||
|
data::book::{Book, Format},
|
||||||
|
handlers::error::HandlerError,
|
||||||
|
};
|
||||||
|
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
@ -19,18 +23,14 @@ pub async fn handler(
|
|||||||
.scalar_book(id)
|
.scalar_book(id)
|
||||||
.map_err(HandlerError::DataError)?;
|
.map_err(HandlerError::DataError)?;
|
||||||
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
|
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
|
||||||
let format: &str = format.as_str();
|
let format = Format(format);
|
||||||
let file_name = book.formats.get(format).ok_or(NotFoundError)?;
|
let file_name = book.formats.get(&format).ok_or(NotFoundError)?;
|
||||||
let file_path = state.config.library_path.join(book.path).join(file_name);
|
let file_path = state.config.library_path.join(book.path).join(file_name);
|
||||||
let mut file = File::open(file_path).map_err(|_| NotFoundError)?;
|
let mut file = File::open(file_path).map_err(|_| NotFoundError)?;
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
file.read_to_end(&mut data).map_err(|_| NotFoundError)?;
|
file.read_to_end(&mut data).map_err(|_| NotFoundError)?;
|
||||||
let content_type = match format {
|
let content_type = format.0;
|
||||||
"pdf" => "application/pdf",
|
|
||||||
"epub" => "application/epub+zip",
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(data
|
Ok(data
|
||||||
.with_content_type(content_type)
|
.with_content_type(content_type)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use calibre_db::data::pagination::SortOrder;
|
||||||
use poem::{
|
use poem::{
|
||||||
handler,
|
handler,
|
||||||
web::{Data, WithContentType},
|
web::{Data, WithContentType},
|
||||||
@ -9,6 +10,7 @@ use time::OffsetDateTime;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::AppState,
|
app_state::AppState,
|
||||||
|
data::book::Book,
|
||||||
handlers::error::HandlerError,
|
handlers::error::HandlerError,
|
||||||
opds::{
|
opds::{
|
||||||
author::Author, content::Content, entry::Entry, feed::Feed, link::Link,
|
author::Author, content::Content, entry::Entry, feed::Feed, link::Link,
|
||||||
@ -16,22 +18,72 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[handler]
|
fn create_feed(
|
||||||
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> {
|
now: OffsetDateTime,
|
||||||
let now = OffsetDateTime::now_utc();
|
self_link: Link,
|
||||||
|
mut additional_links: Vec<Link>,
|
||||||
|
entries: Vec<Entry>,
|
||||||
|
) -> Feed {
|
||||||
let author = Author {
|
let author = Author {
|
||||||
name: "Thallian".to_string(),
|
name: "Thallian".to_string(),
|
||||||
uri: "https://code.vanwa.ch/shu/rusty-library".to_string(),
|
uri: "https://code.vanwa.ch/shu/rusty-library".to_string(),
|
||||||
email: None,
|
email: None,
|
||||||
};
|
};
|
||||||
let home_link = Link {
|
let mut links = vec![
|
||||||
href: "/opds".to_string(),
|
Link {
|
||||||
|
href: "/opds".to_string(),
|
||||||
|
media_type: MediaType::Navigation,
|
||||||
|
rel: Relation::Start,
|
||||||
|
title: Some("Home".to_string()),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
self_link,
|
||||||
|
];
|
||||||
|
links.append(&mut additional_links);
|
||||||
|
|
||||||
|
Feed {
|
||||||
|
title: "rusty-library".to_string(),
|
||||||
|
id: "rusty:catalog".to_string(),
|
||||||
|
updated: now,
|
||||||
|
icon: "favicon.ico".to_string(),
|
||||||
|
author,
|
||||||
|
links,
|
||||||
|
entries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn books(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> {
|
||||||
|
let books: Vec<Book> = state
|
||||||
|
.calibre
|
||||||
|
.books(u32::MAX.into(), None, &SortOrder::ASC)
|
||||||
|
.map(|x| {
|
||||||
|
x.iter()
|
||||||
|
.filter_map(|y| Book::full_book(y, &state))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.map_err(HandlerError::DataError)?;
|
||||||
|
|
||||||
|
let entries: Vec<Entry> = books.into_iter().map(Entry::from).collect();
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
let self_link = Link {
|
||||||
|
href: "/opds/books".to_string(),
|
||||||
media_type: MediaType::Navigation,
|
media_type: MediaType::Navigation,
|
||||||
rel: Relation::Start,
|
rel: Relation::Myself,
|
||||||
title: Some("Home".to_string()),
|
title: None,
|
||||||
count: None,
|
count: None,
|
||||||
};
|
};
|
||||||
|
let feed = create_feed(now, self_link, vec![], entries);
|
||||||
|
let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
|
||||||
|
|
||||||
|
Ok(xml.with_content_type("application/atom+xml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
let self_link = Link {
|
let self_link = Link {
|
||||||
href: "/opds".to_string(),
|
href: "/opds".to_string(),
|
||||||
media_type: MediaType::Navigation,
|
media_type: MediaType::Navigation,
|
||||||
@ -47,6 +99,7 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<Stri
|
|||||||
media_type: MediaType::Text,
|
media_type: MediaType::Text,
|
||||||
content: "Index of all books".to_string(),
|
content: "Index of all books".to_string(),
|
||||||
},
|
},
|
||||||
|
author: None,
|
||||||
links: vec![Link {
|
links: vec![Link {
|
||||||
href: "/opds/books".to_string(),
|
href: "/opds/books".to_string(),
|
||||||
media_type: MediaType::Navigation,
|
media_type: MediaType::Navigation,
|
||||||
@ -56,15 +109,7 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<Stri
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
let feed = Feed {
|
let feed = create_feed(now, self_link, vec![], vec![books_entry]);
|
||||||
title: "rusty-library".to_string(),
|
|
||||||
id: "rusty:catalog".to_string(),
|
|
||||||
updated: now,
|
|
||||||
icon: "favicon.ico".to_string(),
|
|
||||||
author,
|
|
||||||
links: vec![home_link, self_link],
|
|
||||||
entries: vec![books_entry],
|
|
||||||
};
|
|
||||||
let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
|
let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
|
||||||
|
|
||||||
Ok(xml.with_content_type("application/atom+xml"))
|
Ok(xml.with_content_type("application/atom+xml"))
|
||||||
|
@ -43,6 +43,7 @@ pub async fn run(config: Config) -> Result<(), std::io::Error> {
|
|||||||
let app = Route::new()
|
let app = Route::new()
|
||||||
.at("/", get(handlers::recents::handler))
|
.at("/", get(handlers::recents::handler))
|
||||||
.at("/opds", get(handlers::opds::handler))
|
.at("/opds", get(handlers::opds::handler))
|
||||||
|
.at("/opds/books", get(handlers::opds::books))
|
||||||
.at("/books", get(handlers::books::handler_init))
|
.at("/books", get(handlers::books::handler_init))
|
||||||
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
||||||
.at("/series", get(handlers::series::handler_init))
|
.at("/series", get(handlers::series::handler_init))
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use super::{content::Content, link::Link};
|
use crate::data::book::Book;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
author::Author, content::Content, link::Link, media_type::MediaType, relation::Relation,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename = "entry")]
|
#[serde(rename = "entry")]
|
||||||
@ -11,10 +15,46 @@ pub struct Entry {
|
|||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
pub updated: OffsetDateTime,
|
pub updated: OffsetDateTime,
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
|
pub author: Option<Author>,
|
||||||
#[serde(rename = "link")]
|
#[serde(rename = "link")]
|
||||||
pub links: Vec<Link>,
|
pub links: Vec<Link>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Book> for Entry {
|
||||||
|
fn from(value: Book) -> Self {
|
||||||
|
let author = Author {
|
||||||
|
name: value.clone().author.name,
|
||||||
|
uri: format!("/opds/authors/{}", value.author.id),
|
||||||
|
email: None,
|
||||||
|
};
|
||||||
|
let mut links = vec![Link {
|
||||||
|
href: format!("/cover/{}", value.id),
|
||||||
|
media_type: MediaType::Jpeg,
|
||||||
|
rel: Relation::Image,
|
||||||
|
title: None,
|
||||||
|
count: None,
|
||||||
|
}];
|
||||||
|
let mut format_links: Vec<Link> = value
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.map(|(key, val)| Link::from((&value, (key, val.as_str()))))
|
||||||
|
.collect();
|
||||||
|
links.append(&mut format_links);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
title: value.title.clone(),
|
||||||
|
id: format!("urn:uuid:{}", value.uuid),
|
||||||
|
updated: value.last_modified,
|
||||||
|
content: Content {
|
||||||
|
media_type: MediaType::Html,
|
||||||
|
content: value.description.clone().unwrap_or("".to_string()),
|
||||||
|
},
|
||||||
|
author: Some(author),
|
||||||
|
links,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use quick_xml::se::to_string;
|
use quick_xml::se::to_string;
|
||||||
@ -33,6 +73,7 @@ mod tests {
|
|||||||
media_type: MediaType::Text,
|
media_type: MediaType::Text,
|
||||||
content: "All authors".to_string(),
|
content: "All authors".to_string(),
|
||||||
},
|
},
|
||||||
|
author: None,
|
||||||
links: vec![
|
links: vec![
|
||||||
Link {
|
Link {
|
||||||
href: "/opds".to_string(),
|
href: "/opds".to_string(),
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::data::book::{Book, Format};
|
||||||
|
|
||||||
use super::{media_type::MediaType, relation::Relation};
|
use super::{media_type::MediaType, relation::Relation};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@ -19,6 +21,20 @@ pub struct Link {
|
|||||||
pub count: Option<u64>,
|
pub count: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<(&Book, (&Format, &str))> for Link {
|
||||||
|
fn from(value: (&Book, (&Format, &str))) -> Self {
|
||||||
|
let format = value.1 .0.clone();
|
||||||
|
let media_type: MediaType = format.into();
|
||||||
|
Self {
|
||||||
|
href: format!("/book/{}/{}", value.0.id, value.1 .0),
|
||||||
|
media_type,
|
||||||
|
rel: media_type.into(),
|
||||||
|
title: Some(value.1 .0 .0.clone()),
|
||||||
|
count: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use quick_xml::se::to_string;
|
use quick_xml::se::to_string;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
#[derive(Debug, SerializeDisplay)]
|
use crate::data::book::Format;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, SerializeDisplay)]
|
||||||
pub enum MediaType {
|
pub enum MediaType {
|
||||||
Acquisition,
|
Acquisition,
|
||||||
Epub,
|
Epub,
|
||||||
@ -11,6 +13,16 @@ pub enum MediaType {
|
|||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Format> for MediaType {
|
||||||
|
fn from(value: Format) -> Self {
|
||||||
|
match value.0.as_ref() {
|
||||||
|
"epub" => MediaType::Epub,
|
||||||
|
"pdf" => MediaType::Pdf,
|
||||||
|
_ => MediaType::Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for MediaType {
|
impl std::fmt::Display for MediaType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
|
use super::media_type::MediaType;
|
||||||
|
|
||||||
#[derive(Debug, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
Image,
|
Image,
|
||||||
@ -7,6 +9,21 @@ pub enum Relation {
|
|||||||
Start,
|
Start,
|
||||||
Subsection,
|
Subsection,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
Acquisition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MediaType> for Relation {
|
||||||
|
fn from(value: MediaType) -> Self {
|
||||||
|
match value {
|
||||||
|
MediaType::Acquisition => Relation::Acquisition,
|
||||||
|
MediaType::Epub => Relation::Acquisition,
|
||||||
|
MediaType::Html => Relation::Acquisition,
|
||||||
|
MediaType::Jpeg => Relation::Image,
|
||||||
|
MediaType::Navigation => Relation::Myself,
|
||||||
|
MediaType::Pdf => Relation::Acquisition,
|
||||||
|
MediaType::Text => Relation::Acquisition,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Relation {
|
impl std::fmt::Display for Relation {
|
||||||
@ -17,6 +34,7 @@ impl std::fmt::Display for Relation {
|
|||||||
Relation::Start => write!(f, "start"),
|
Relation::Start => write!(f, "start"),
|
||||||
Relation::Subsection => write!(f, "subsection"),
|
Relation::Subsection => write!(f, "subsection"),
|
||||||
Relation::Thumbnail => write!(f, "http://opds-spec.org/image/thumbnail"),
|
Relation::Thumbnail => write!(f, "http://opds-spec.org/image/thumbnail"),
|
||||||
|
Relation::Acquisition => write!(f, "http://opds-spec.org/acquisition"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user