i15-anyhow-context (#17)
Closes kemitix/podal#15 Reviewed-on: #17 Co-authored-by: Paul Campbell <pcampbell@kemitix.net> Co-committed-by: Paul Campbell <pcampbell@kemitix.net>
This commit is contained in:
parent
d305a2bb1b
commit
4eaf158483
12 changed files with 71 additions and 146 deletions
|
@ -1,79 +0,0 @@
|
|||
use std::{str::Utf8Error, string::FromUtf8Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub details: String,
|
||||
pub source: String,
|
||||
}
|
||||
impl Error {
|
||||
pub fn message(details: &str) -> Self {
|
||||
Self {
|
||||
details: details.to_string(),
|
||||
source: "(not provided)".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: value.source().unwrap().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Utf8Error> for Error {
|
||||
fn from(value: Utf8Error) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: "Utf8Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(value: FromUtf8Error) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: "FromUtf8Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<String> for Error {
|
||||
fn from(details: String) -> Self {
|
||||
Self {
|
||||
details,
|
||||
source: "String".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: "std::io::Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<std::sync::mpsc::SendError<String>> for Error {
|
||||
fn from(value: std::sync::mpsc::SendError<String>) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: "std::sync::mpsc::SendError".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<atom_syndication::Error> for Error {
|
||||
fn from(value: atom_syndication::Error) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: "atom_syndication::Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
source: "reqwest::Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,26 @@
|
|||
use crate::network::NetworkEnv;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::network::NetworkEnv;
|
||||
|
||||
pub fn find(site: &str, channel_name: &str, e: &NetworkEnv) -> Result<String> {
|
||||
if let Some(channel_prefix) = channel_name.chars().next() {
|
||||
if channel_prefix != '@' {
|
||||
return Err(format!("Channel Name must begin with an '@': {}", channel_name).into());
|
||||
return Err(anyhow!(
|
||||
"Channel Name must begin with an '@': {}",
|
||||
channel_name
|
||||
));
|
||||
}
|
||||
}
|
||||
let channel_url = format!("{}{}", site, channel_name);
|
||||
let response = (e.fetch_as_text)(&channel_url)?;
|
||||
let response = (e.fetch_as_text)(&channel_url)
|
||||
.context(format!("Fetching channel to find RSS: {}", channel_url))?;
|
||||
let rss_url = scraper::Html::parse_document(&response)
|
||||
.select(&scraper::Selector::parse("link[title='RSS']").unwrap())
|
||||
.next()
|
||||
.unwrap()
|
||||
.context("No RSS link found")?
|
||||
.value()
|
||||
.attr("href")
|
||||
.unwrap()
|
||||
.context("No href attribute found in RSS link")?
|
||||
.to_string();
|
||||
Ok(rss_url)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ mod find;
|
|||
pub use find::find;
|
||||
|
||||
pub fn get(url: &str, e: &NetworkEnv) -> Result<Feed> {
|
||||
let content = (e.fetch_as_bytes)(url)?;
|
||||
let channel = Feed::read_from(&content[..])?;
|
||||
let content = (e.fetch_as_bytes)(url).context(format!("Fetching feed: {}", url))?;
|
||||
let channel = Feed::read_from(&content[..]).context("Could not parse RSS feed")?;
|
||||
Ok(channel)
|
||||
}
|
||||
|
|
|
@ -21,9 +21,7 @@ impl FileEnv {
|
|||
Self {
|
||||
open: Box::new(move |file_name| {
|
||||
let path = format!("{}/{}", &open_dir, file_name);
|
||||
let file = File::open(&path).with_context(|| {
|
||||
format!("FileEnv::open: file_name={file_name}, path={path}")
|
||||
})?;
|
||||
let file = File::open(&path).context(format!("Opening file: {path}"))?;
|
||||
Ok(file)
|
||||
}),
|
||||
append_line: Box::new(move |file_name, line| {
|
||||
|
@ -32,8 +30,8 @@ impl FileEnv {
|
|||
.write(true)
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
.open(&path)
|
||||
.context(format!("Appending to file: {path}"))?;
|
||||
writeln!(file, "{}", line)?;
|
||||
Ok(())
|
||||
}),
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::file::FileEnv;
|
|||
use std::io::{BufRead, BufReader};
|
||||
|
||||
pub fn lines_from(file_name: &str, e: &FileEnv) -> Result<Vec<String>> {
|
||||
let file = (e.open)(file_name)?;
|
||||
let file = (e.open)(file_name).context(format!("Opening file: {file_name}"))?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = vec![];
|
||||
for line in reader.lines().flatten() {
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::prelude::*;
|
|||
use super::Link;
|
||||
|
||||
pub fn add(link: &Link, file_name: &str, e: &FileEnv) -> Result<()> {
|
||||
(e.append_line)(file_name, &link.href)?;
|
||||
(e.append_line)(file_name, &link.href).context(format!("Appending to file: {}", file_name))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ use std::io::{BufRead, BufReader};
|
|||
use super::Link;
|
||||
|
||||
pub fn find(link: &Link, file_name: &str, e: &FileEnv) -> Result<bool> {
|
||||
let file = (e.open)(file_name)?;
|
||||
let file = (e.open)(file_name).context(format!("Opening file: {file_name}"))?;
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let line = line.context(format!("Reading line in file: {file_name}"))?;
|
||||
if line == link.href {
|
||||
return Ok(true); // is already downloaded
|
||||
}
|
||||
|
|
18
src/lib.rs
18
src/lib.rs
|
@ -1,7 +1,6 @@
|
|||
use params::Args;
|
||||
use prelude::*;
|
||||
|
||||
mod errors;
|
||||
pub mod feed;
|
||||
pub mod file;
|
||||
pub mod history;
|
||||
|
@ -21,15 +20,20 @@ pub struct Env {
|
|||
}
|
||||
|
||||
pub fn run(site: &str, a: &Args, e: Env) -> Result<()> {
|
||||
for channel_name in file::read::lines_from(&a.subscriptions, &e.file)? {
|
||||
for channel_name in
|
||||
file::read::lines_from(&a.subscriptions, &e.file).context("Reading subscriptions")?
|
||||
{
|
||||
println!("Channel: {}", channel_name);
|
||||
let feed_url = feed::find(site, &channel_name, &e.network)?;
|
||||
for entry in feed::get(&feed_url, &e.network)?.entries() {
|
||||
let feed_url = feed::find(site, &channel_name, &e.network).context("Finding channel")?;
|
||||
for entry in feed::get(&feed_url, &e.network)
|
||||
.context("Fetching channel")?
|
||||
.entries()
|
||||
{
|
||||
if let Some(link) = entry.links().get(0).cloned() {
|
||||
if !history::find(&link, &a.history, &e.file)? {
|
||||
if !history::find(&link, &a.history, &e.file).context("Finding history")? {
|
||||
println!("Downloading {}: {}", &channel_name, entry.title().as_str());
|
||||
(e.network.download_as_mp3)(&link.href)?;
|
||||
history::add(&link, &a.history, &e.file)?;
|
||||
(e.network.download_as_mp3)(&link.href).context("Downloading as MP3")?;
|
||||
history::add(&link, &a.history, &e.file).context("Adding to history")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ fn main() -> Result<()> {
|
|||
network: NetworkEnv::default(),
|
||||
file: FileEnv::create(&args),
|
||||
},
|
||||
)?;
|
||||
)
|
||||
.context("Running")?;
|
||||
|
||||
println!("Done");
|
||||
Ok(())
|
||||
|
|
|
@ -16,20 +16,41 @@ pub struct NetworkEnv {
|
|||
impl Default for NetworkEnv {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fetch_as_text: Box::new(|url| Ok(reqwest::blocking::get(url)?.text()?)),
|
||||
fetch_as_bytes: Box::new(|url| Ok(reqwest::blocking::get(url)?.bytes()?)),
|
||||
fetch_as_text: Box::new(|url| {
|
||||
reqwest::blocking::get(url)
|
||||
.context(format!("Fetching {}", url))?
|
||||
.text()
|
||||
.context(format!("Parsing text from body of response for {}", url))
|
||||
}),
|
||||
fetch_as_bytes: Box::new(|url| {
|
||||
reqwest::blocking::get(url)
|
||||
.context(format!("Fetching {}", url))?
|
||||
.bytes()
|
||||
.context(format!("Parsing bytes from body of response for {}", url))
|
||||
}),
|
||||
download_as_mp3: Box::new(|url| {
|
||||
let cmd = "yt-dlp";
|
||||
// println!("{} --extract-audio --audio-format mp3 {}", cmd, &link.href);
|
||||
let output = Command::new(cmd)
|
||||
.arg("--extract-audio")
|
||||
.arg("--audio-format")
|
||||
.arg("mp3")
|
||||
.arg(url)
|
||||
.output()?;
|
||||
.output()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Running: {} --extract-audio --audio-format mp3 {}",
|
||||
cmd, url
|
||||
)
|
||||
})?;
|
||||
if !output.stderr.is_empty() {
|
||||
eprintln!("Error: {}", String::from_utf8(output.stderr)?);
|
||||
println!("{}", String::from_utf8(output.stdout)?);
|
||||
eprintln!(
|
||||
"Error: {}",
|
||||
String::from_utf8(output.stderr).context("Parsing stderr")?
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
String::from_utf8(output.stdout).context("Parsing stdout")?
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
use crate::errors::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub use anyhow::{anyhow, Context, Error, Result};
|
||||
|
|
|
@ -11,7 +11,6 @@ use anyhow::Context;
|
|||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use crate::{
|
||||
errors::Error,
|
||||
file::{FileAppendLineFn, FileOpenFn},
|
||||
network::{NetworkDownloadAsMp3Fn, NetworkFetchAsBytesFn, NetworkFetchAsTextFn},
|
||||
prelude::*,
|
||||
|
@ -45,9 +44,7 @@ pub fn mock_fetch_as_text_with_rss_url(
|
|||
"#,
|
||||
url
|
||||
)),
|
||||
None => Err(Error::message(
|
||||
format!("Unexpected request for {}", url).as_str(),
|
||||
)),
|
||||
None => Err(anyhow!("Unexpected request for {}", url)),
|
||||
})
|
||||
}
|
||||
pub fn mock_network_fetch_as_bytes_with_rss_entries(
|
||||
|
@ -57,19 +54,21 @@ pub fn mock_network_fetch_as_bytes_with_rss_entries(
|
|||
if let Some(feed) = feeds.get(url).cloned() {
|
||||
Ok(bytes::Bytes::from(feed))
|
||||
} else {
|
||||
Err(Error::message(format!("No mock feed: {}", url).as_str()))
|
||||
Err(anyhow!("No mock feed: {}", url))
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn mock_file_open(real_paths: HashMap<String, String>) -> FileOpenFn {
|
||||
Box::new(move |path: &str| {
|
||||
if let Some(real_path) = real_paths.get(&path.to_string()) {
|
||||
Ok(File::open(real_path).with_context(|| {
|
||||
format!("test_utils/mock_file_open: path={path}, real_path={real_path}, path_map=[{:?}]", real_paths)
|
||||
})?)
|
||||
Ok(File::open(real_path)
|
||||
.context(format!(
|
||||
"test_utils/mock_file_open: path={path}, real_path={real_path}, path_map=[{:?}]",
|
||||
real_paths))?)
|
||||
} else {
|
||||
Err(Error::message(
|
||||
format!("Not implemented: test_utils/mock_file_open: {}", path).as_str(),
|
||||
Err(anyhow!(
|
||||
"Not implemented: test_utils/mock_file_open: {}",
|
||||
path
|
||||
))
|
||||
}
|
||||
})
|
||||
|
@ -87,30 +86,8 @@ pub fn mock_file_append_line() -> FileAppendLineFn {
|
|||
}
|
||||
|
||||
pub fn stub_network_fetch_as_bytes() -> NetworkFetchAsBytesFn {
|
||||
Box::new(|url: &str| {
|
||||
Err(Error::message(
|
||||
format!("Not implemented: network_fetch_as_bytes: {}", url).as_str(),
|
||||
))
|
||||
})
|
||||
Box::new(|url: &str| Err(anyhow!("Not implemented: network_fetch_as_bytes: {}", url)))
|
||||
}
|
||||
pub fn stub_network_download_as_mp3() -> NetworkDownloadAsMp3Fn {
|
||||
Box::new(|url: &str| {
|
||||
Err(Error::message(
|
||||
format!("Not implemented: network_download_as_mp3: {}", url).as_str(),
|
||||
))
|
||||
})
|
||||
Box::new(|url: &str| Err(anyhow!("Not implemented: network_download_as_mp3: {}", url)))
|
||||
}
|
||||
// pub fn stub_file_open() -> FileOpenFn {
|
||||
// Box::new(|path: &str| {
|
||||
// Err(Error::message(
|
||||
// format!("Not implemented: file_open: {}", path).as_str(),
|
||||
// ))
|
||||
// })
|
||||
// }
|
||||
// pub fn stub_file_append_line() -> FileAppendLineFn {
|
||||
// Box::new(|path: &str, line: &str| {
|
||||
// Err(Error::message(
|
||||
// format!("Not implemented: file_append_line: {} to {}", line, path).as_str(),
|
||||
// ))
|
||||
// })
|
||||
// }
|
||||
|
|
Loading…
Add table
Reference in a new issue