use crate::{fs, models, new_page_stream, Client, SerieID, NEXT_DATA_SELECTOR};
use eyre::{bail, ensure, eyre, Result, WrapErr};
use futures::Stream;
use image::DynamicImage;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
use std::{
fmt,
path::{Path, PathBuf},
str::FromStr,
};
use url::Url;
pub static EPISODE_TITLE_PREFIX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^#\d+ ").expect("invalid episode title prefix"));
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum AccessType {
Free,
TemporaryFree,
WaitUntilFree,
Paywalled,
Paid,
}
impl FromStr for AccessType {
type Err = eyre::Report;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(match &value[..2] {
"FR" => Self::Free,
"RD" => Self::TemporaryFree,
"WF" => Self::WaitUntilFree,
"PM" => Self::Paywalled,
"AB" => Self::Paid,
_ => bail!("{value} is not a valid access type"),
})
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)]
pub enum MediaType {
#[serde(rename = "E")]
Episode,
#[serde(rename = "V")]
Volume,
}
impl fmt::Display for MediaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Episode => "episode",
Self::Volume => "volume",
}
)
}
}
impl FromStr for MediaType {
type Err = eyre::Report;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(match value {
"episode" => Self::Episode,
"volume" => Self::Volume,
_ => bail!("{value} is not a valid media type"),
})
}
}
#[derive(Debug)]
pub struct Media {
title: String,
id: MediaID,
serie_id: SerieID,
number: u16,
access: AccessType,
page_count: u16,
}
impl Media {
pub fn id(&self) -> MediaID {
self.id
}
pub fn number(&self) -> u16 {
self.number
}
pub fn title(&self) -> &str {
&self.title
}
pub fn page_count(&self) -> u16 {
self.page_count
}
pub fn is_available(&self) -> bool {
matches!(
self.access,
AccessType::Free | AccessType::TemporaryFree | AccessType::Paid
)
}
pub fn is_present_at(&self, path: &Path) -> bool {
let filepath = [path, &self.filename()].iter().collect::<PathBuf>();
filepath.is_file()
}
pub fn filename(&self) -> PathBuf {
let mut filename = fs::sanitize_name(self.title());
filename.set_extension("cbz");
filename
}
pub async fn fetch_pages(
&self,
client: Client,
) -> Result<impl Stream<Item = Result<DynamicImage>>> {
let html = client
.get_html(self.viewer_url())
.await
.context("get viewer page")?;
let payload = html
.select(&NEXT_DATA_SELECTOR)
.next()
.ok_or_else(|| eyre!("look for PageIteratorepisode __NEXT_DATA__"))?
.text()
.collect::<String>();
let data = serde_json::from_str::<models::viewer::NextData>(&payload)
.context("parse episode __NEXT_DATA__")?
.props
.page_props
.initial_state
.viewer
.p_data;
ensure!(
data.img.len() == usize::from(self.page_count),
"expected {} page, got {}",
self.page_count,
data.img.len(),
);
let pages = data
.img
.into_iter()
.map(|img| img.path.try_into())
.collect::<Result<Vec<_>, _>>()
.context("invalid page URL")?;
Ok(new_page_stream(client, pages, data.is_scrambled))
}
fn viewer_url(&self) -> Url {
Url::parse(&format!(
"https://piccoma.com/fr/viewer/{}/{}",
self.serie_id, self.id,
))
.expect("valid media URL")
}
}
impl TryFrom<models::serie::Media> for Media {
type Error = eyre::Report;
fn try_from(value: models::serie::Media) -> Result<Self, Self::Error> {
let number = match value.media_type {
MediaType::Episode => value.order_value,
MediaType::Volume => value.volume,
};
let title = match value.media_type {
MediaType::Episode => {
if value.title.is_empty() {
format!("Episode {:03}", number)
} else {
format!(
"{:03} - {}",
number,
EPISODE_TITLE_PREFIX.replace(&value.title, "")
)
}
},
MediaType::Volume => format!("Tome {:02}", number),
};
Ok(Self {
title,
id: value.id.into(),
serie_id: value.product_id.into(),
access: value.use_type.parse().context("parse access type")?,
number,
page_count: value.page_count,
})
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct MediaID(u32);
impl fmt::Display for MediaID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<u32> for MediaID {
fn from(value: u32) -> Self {
Self(value)
}
}
impl FromStr for MediaID {
type Err = eyre::Report;
fn from_str(value: &str) -> Result<Self, Self::Err> {
value.parse::<u32>().context("invalid media ID").map(Self)
}
}