use base64::{engine::general_purpose, Engine as _};
use crossterm::{
cursor, execute,
terminal::{Clear, ClearType},
};
use image::DynamicImage;
use reqwest;
use scraper::{Html, Selector};
use std::error::Error;
use std::{
fmt,
io::{self},
};
use viuer::{print as print_image, Config};
pub const MIN_PAGE: i32 = 100;
pub const MAX_PAGE: i32 = 801;
pub enum PageDirection {
Next,
Prev,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Channel(i32);
impl Channel {
pub fn new(page: i32) -> Self {
Self(page.clamp(MIN_PAGE, MAX_PAGE))
}
pub fn number(self) -> i32 {
self.0
}
pub fn next_from_document(&self, document: &Html) -> Self {
self.navigate(document, PageDirection::Next)
}
pub fn prev_from_document(&self, document: &Html) -> Self {
self.navigate(document, PageDirection::Prev)
}
fn navigate(&self, document: &Html, direction: PageDirection) -> Self {
let fallback = match direction {
PageDirection::Next => self.0 + 1,
PageDirection::Prev => self.0 - 1,
};
let selector = match direction {
PageDirection::Next => "[title='Nästa sida']",
PageDirection::Prev => "[title='Förra sidan']",
};
let parsed = Selector::parse(selector)
.ok()
.and_then(|sel| document.select(&sel).next())
.and_then(|el| el.value().attr("href"))
.and_then(|href| href.split('/').last())
.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(fallback);
Channel::new(parsed)
}
}
impl fmt::Display for Channel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug)]
pub struct TextTvPage {
channel: Channel,
document: Html,
image: DynamicImage,
prev: Channel,
next: Channel,
}
impl TextTvPage {
pub fn fetch(channel: Channel) -> Result<Self, Box<dyn Error>> {
let url = format!("https://www.svt.se/text-tv/{}", channel);
let html = reqwest::blocking::get(&url)?.text()?;
let document = Html::parse_document(&html);
let selector = Selector::parse("img.Content_pageImage__bS0mg")?;
let element = document
.select(&selector)
.next()
.ok_or("Image element not found")?;
let data_url = element
.value()
.attr("src")
.ok_or("Missing src attribute on image")?;
let image_data = Self::decode_image_data(data_url)?;
let image = image::load_from_memory(&image_data)?;
let prev = channel.prev_from_document(&document);
let next = channel.next_from_document(&document);
Ok(Self {
channel,
document,
image,
prev,
next,
})
}
pub fn show(&self) -> Result<(), Box<dyn Error>> {
let config = Config {
width: Some(100),
..Default::default()
};
print_image(&self.image, &config)?;
Ok(())
}
pub fn page_number(&self, direction: PageDirection) -> Channel {
match direction {
PageDirection::Next => self.channel.next_from_document(&self.document),
PageDirection::Prev => self.channel.prev_from_document(&self.document),
}
}
pub fn next_page(&self) -> Result<Self, Box<dyn Error>> {
if self.next == self.channel {
return Err("Already at the last page".into());
}
self.clear_screen()?;
let page = Self::fetch(self.next)?;
page.show()?;
return Ok(page);
}
pub fn prev_page(&self) -> Result<Self, Box<dyn Error>> {
if self.prev == self.channel {
return Err("Already at the first page".into());
}
self.clear_screen()?;
let page = Self::fetch(self.prev)?;
page.show()?;
return Ok(page);
}
pub fn channel(&self) -> Channel {
self.channel
}
pub fn clear_screen(&self) -> Result<(), Box<dyn Error>> {
execute!(io::stdout(), Clear(ClearType::All), cursor::MoveTo(0, 0))?;
Ok(())
}
fn decode_image_data(data_url: &str) -> Result<Vec<u8>, Box<dyn Error>> {
let b64 = data_url
.splitn(2, ',')
.nth(1)
.ok_or("Invalid data URL format")?;
Ok(general_purpose::STANDARD.decode(b64)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
const PAGE_NUMBERS_HTML: &str = "
<a class=\"NavigationArrow_enabled__ueMbi NavigationArrow_navigationArrow__eaKzk\" title=\"Nästa sida\" href=\"/text-tv/103\"></a>
<a class=\"NavigationArrow_enabled__ueMbi NavigationArrow_navigationArrow__eaKzk\" title=\"Förra sidan\" href=\"/text-tv/101\"></a>
";
#[test]
fn next_from_document_returns_correct_page() {
let document = Html::parse_document(&PAGE_NUMBERS_HTML);
let current = Channel::new(200);
let next = current.next_from_document(&document);
assert_eq!(next, Channel::new(103));
}
#[test]
fn prev_from_document_returns_correct_page() {
let document = Html::parse_document(&PAGE_NUMBERS_HTML);
let current = Channel::new(200);
let prev = current.prev_from_document(&document);
assert_eq!(prev, Channel::new(101));
}
#[test]
fn next_from_document_falls_back_plus_one_if_missing() {
let document = Html::parse_document("");
let current = Channel::new(103);
let next = current.next_from_document(&document);
assert_eq!(next, Channel::new(104));
}
#[test]
fn prev_from_document_falls_back_minus_one_if_missing() {
let document = Html::parse_document("");
let current = Channel::new(103);
let prev = current.prev_from_document(&document);
assert_eq!(prev, Channel::new(102));
}
#[test]
fn next_does_not_exceed_max_page() {
let document = Html::parse_document("");
let current = Channel::new(MAX_PAGE);
let next = current.next_from_document(&document);
assert_eq!(next, Channel::new(MAX_PAGE));
}
#[test]
fn prev_does_not_go_below_min_page() {
let document = Html::parse_document("");
let current = Channel::new(MIN_PAGE);
let prev = current.prev_from_document(&document);
assert_eq!(prev, Channel::new(MIN_PAGE));
}
}