WWO4T3TSJAX5YJCVXLZEOQVI3KEBI6TVBRBSWVC44FGZCSYCVUQAC
57DU4YHARHICJPWTTHSBB5O7MAW7GJVJUMVEREGVWEMHCGI7JMLQC
XMB6SWDNXYSYHQAUJMAQANOBAOHA7DY7BOUR5VNSRHBHIBINER4QC
XIHPYOWDLQY2MVMVUQPH23O3TBALRG4G2CHSLWSCAYMY5NVJ32WQC
FBQOBNZ6JJQXSHYQK7MCFA4U7NBNB47FXED7Y7HPRTOQVXJFIAGAC
ELOFCNQ3LBRPKY33PMIHYSM32UIDSHJIM3W6ZO6TMRTO26NNIQHAC
IFBRAMVLQ4Z6BAEMWDIXD2V5HSZK4DHRWYZNB32IBY7ZRTNZJVCQC
AFGKYLKUV6QBTL7JYDNOUCO2BVUR6CUB5MAKA3V3C76XGCWJULQAC
HTHQWZRRS5N2J3VMS4VFT3MMOBA4MHVTO6REWCHU2RJIDSN3HTSQC
IMRHIIAUQDSKU4D6YONK67D6GH7ZLLMMB5S2HRZT76SYNJ7M2RSQC
ED7GAY6WJJ6Y3476KCKQULZ4XF73XBREX6FLX6URUAWCL5KDTBBQC
MQKD76RYJOC3SJ4EPKQRQFV7A2BRJAHAI4VMMLR4EKV4B3EV5YTQC
DP6ASS5FJGSZUG2B4SQOKZVHFAVZLEHFVEWBNFG3BHMC6FZAJNHQC
O3VZ5J3LIYD3KBZLZA6HOJI7MVOV5DEODDPCPOMIBTPCO3CZW4YQC
LG3NDQDEZ65QP564QKTTYC3XI4ZN3NVW4B5T36QJEBHMJAEH4VEAC
52CTWUQP2WCBMYNGOIOLOVUYWXF3YOJAPJY3WIHIFLIQD2WHIDFQC
HXMFYFMLDAJOOUUALCZRUJJFOIWUFK7CGO5MNSJVO2XMR63KOTAAC
BIZPSJDFFGGDYGVC5UJSJJ7YOBEPC27FPKMSHWJDI2DNMXHWRXSAC
UIMZBURR7KOWSREO4GDH5C2LZDUTEZBKQNYWBYSFGUTRYJ4GKSNQC
5GQNHICLSFAA7ZUFXUCNACCPAIIGK4DV2QPTONDNXLS4TJJTOFHAC
3TU6FAXGIE5K34LWVKCLI6DPH74VUV4BMEADXKA2PMSR6KMKDX6AC
3YR56Y65UIAL3J7PUXWVJMOOHYZYDIX4V54OT2TJPZ25WQ6MXHCQC
O53GR2OQHGRKAVJT2RVPRHYFB54W5LM4DQYT7EYVGKU7HDK5CJJQC
JJ4SMY257MAHSJSZH5PJZMLBH3GJX5VKH2ZZSBGWLL7FWP7OA7TQC
5JMYBRF3UYX4LFH7JK6S4BEDKRVKDFIL4YKTCWKMKP4TMNNGQFKQC
J64KBLKALQ3HQCY4HJU5H6WBXTATS7TKBYNNUUSNJE7JLWLYO66QC
SBPKWZNQF5BWAJ7SZHWVK5BG6DTVJNDYND6UG5PDZCWZ2W4W2HXQC
6FJACP6KUOZ4HWK4PSS5PFPGDYXZSCAWSKIARWBDGCZTPJWXA62AC
M4FCDZ745GHHL3OLH64EVYOEOEGGGVIBCVFGX5JUJDJRE5OLCXLQC
XCBCUDQRCNEEFIKGSHA52D53I5HGZSYB4W4JINRFKZKWHKEG3V6QC
EZMX4SYFEBYNJVQETRVAYONU5MIMQYTTSA5DRMTQET5B7CL6CI6AC
ZYS43ILR4OXI7S2AYNGYSTK3IU2UVELIWVCCWDS7RVZQDSNJMDHQC
XJXE6M6QRV4WRSY7D4T2QK35RUVD5YW3UP2RDUNFMXIGK2ZI6D2AC
64YYLD4JELQCTYFZRMLM32YQKZQXKO2CYYQH4BEFXD74MKF3HUXAC
JYSIHNS67XTGAR4HN7ZHWFMGGYSK5IY6J6EHO4YUZOR7UMMWAORQC
KHBYSOY7O3OGZLSIYZL6XRYHPNESD7WG2IAT7JT7HBTTCZVW4WJAC
GE7XXDPC73SUY6I6F3X6I4QFQO2BCPHD4MZALYOWG7H7SRE5XAFQC
76TBVFPIFU3LSMXY5NAHZBH6HRJLSLK43PGOPL6QQ2YYVBJ64QAQC
USEXBPODUNF4Y7KLGC7SFQZAARXFMTAHK4OKO6HR7G3UPZY4C42QC
BSPWOOHZMN3RAOHGJ2A3XKUOUCFFAOXS7YR67E3AARPPPIA5YPDAC
37X6ZKCCWP4OPXA7QMA2VG2IZHSXS6TQFTKMONU35NLMWKA47JUQC
MQKOX2CQ7AON24UJC7RORAC7Y2UROROVG7BBKLVWURPXKY75JV5AC
ADXMUSFXOKBPCHW4XS3T4KLWMPGQPYDMZW6YFFSHZPAQKEGIKCBQC
JPN37V6Q35ZAW7A2DTGX2WJ3IJ66BAGHXHWXOGHQRHGFAOETFQ7AC
*.mp3
use clap::{App, ArgMatches};
use std::env;
use std::path::Path;
use crate::actions::*;
use crate::download;
use crate::playback;
use crate::structs::*;
let download_matches = matches.subcommand_matches("download").unwrap();
let podcast = download_matches.value_of("PODCAST").unwrap();
match download_matches.value_of("EPISODE") {
Some(ep) => {
if String::from(ep).contains(|c| c == '-' || c == ',') {
} else if download_matches.occurrences_of("name") > 0 {
download::download_episode_by_name(
&state,
podcast,
ep,
download_matches.occurrences_of("all") > 0,
} else {
}
}
}
Ok(())
}
pub fn list(state: &mut State, matches: &ArgMatches) -> Result<()> {
let list_matches = matches
.subcommand_matches("ls")
.or_else(|| matches.subcommand_matches("list"))
.unwrap();
match list_matches.value_of("PODCAST") {
Some(regex) => list_episodes(regex)?,
None => list_subscriptions(&state)?,
}
Ok(())
}
pub fn play(state: &mut State, matches: &ArgMatches) -> Result<()> {
let play_matches = matches.subcommand_matches("play").unwrap();
let podcast = play_matches.value_of("PODCAST").unwrap();
match play_matches.value_of("EPISODE") {
Some(episode) => {
if play_matches.occurrences_of("name") > 0 {
playback::play_episode_by_name(&state, podcast, episode)?
} else {
playback::play_episode_by_num(&state, podcast, episode)?
}
}
None => playback::play_latest(&state, podcast)?,
}
Ok(())
}
let subscribe_matches = matches
.subcommand_matches("sub")
.or_else(|| matches.subcommand_matches("subscribe"))
.unwrap();
let url = subscribe_matches.value_of("URL").unwrap();
Ok(())
}
Ok(())
}
pub fn remove(state: &mut State, matches: &ArgMatches) -> Result<()> {
let rm_matches = matches.subcommand_matches("rm").unwrap();
let regex = rm_matches.value_of("PODCAST").unwrap();
remove_podcast(state, regex)?;
Ok(())
}
pub fn complete(app: &mut App, matches: &ArgMatches) -> Result<()> {
let matches = matches.subcommand_matches("completion").unwrap();
match matches.value_of("SHELL") {
Some(shell) => print_completion(app, shell),
None => {
let shell_path_env = env::var("SHELL");
if let Ok(p) = shell_path_env {
let shell_path = Path::new(&p);
if let Some(shell) = shell_path.file_name() {
print_completion(app, shell.to_str().unwrap())
}
}
}
}
None => eprintln!("Subscription failed. No url in API response."),
}
let rss_resp = &resp.results[n];
match &rss_resp.feed_url {
Some(r) => sub( state, config, &r).await?,
Ok(())
}
}
print!("Would you like to subscribe to any of these? (y/n): ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if input.to_lowercase().trim() != "y" {
return Ok(());
}
print!("Which one? (#): ");
io::stdout().flush().ok();
let mut num_input = String::new();
io::stdin().read_line(&mut num_input)?;
let n: usize = num_input.trim().parse()?;
eprintln!("Invalid!");
return Ok(());
if n > resp.results.len() {
}
}
Ok(())
}
let matches = matches.subcommand_matches("search").unwrap();
let podcast = matches.value_of("PODCAST").unwrap();
println!("No Results");
return Ok(());
}
{
let stdout = io::stdout();
let mut lock = stdout.lock();
for (i, r) in resp.results.iter().enumerate() {
writeln!(
&mut lock,
"({}) {} [{}]",
i,
r.collection_name.clone().unwrap_or_else(|| "".to_string()),
r.feed_url.clone().unwrap_or_else(|| "".to_string())
)?;
if resp.results.is_empty() {
let resp = podcast_search::search(podcast).await?;
pub async fn search(
state: &mut State,
config: Config,
matches: &ArgMatches<'_>,
) -> Result<()> {
state.subscribe(url).await?;
download::download_rss(state, config, url).await?;
async fn sub( state: &mut State, config: Config, url: &str) -> Result<()> {
sub( state, config, url).await?;
pub async fn subscribe(
state: &mut State,
config: Config,
matches: &ArgMatches<'_>,
) -> Result<()> {
None => match download_matches.value_of("latest") {
},
Some(num_of_latest) => {
}
None => download::download_all( &state, podcast).await?,
download::download_latest( &state, podcast, num_of_latest.parse()?).await?
download::download_episode_by_num( &state, podcast, ep).await?
)
.await?
download::download_range( &state, podcast, ep).await?
pub async fn download(
state: &mut State,
matches: &ArgMatches<'_>,
) -> Result<()> {
use anyhow::Result;
use std::io;
use std::io::Write;
use clap::{App, ArgMatches};
use crate::actions::*;
use crate::arg_parser;
use crate::commands;
use crate::structs::*;
pub fn parse_sub_command(matches: &ArgMatches) -> commands::Command {
match matches.subcommand_name() {
Some("download") => commands::Command::Download,
Some("ls") | Some("list") => commands::Command::List,
Some("play") => commands::Command::Play,
Some("sub") | Some("subscribe") => commands::Command::Subscribe,
Some("search") => commands::Command::Search,
Some("rm") => commands::Command::Remove,
Some("completion") => commands::Command::Complete,
Some("refresh") => commands::Command::Refresh,
Some("update") => commands::Command::Update,
_ => commands::Command::NoMatch,
}
}
version: &str,
state: &mut State,
config: Config,
) -> Result<()> {
let command = parse_sub_command(matches);
match command {
commands::Command::Download => {
}
commands::Command::List => {
arg_parser::list(state, matches)?;
}
commands::Command::Play => {
arg_parser::play(state, matches)?;
}
commands::Command::Subscribe => {
}
commands::Command::Search => {
}
commands::Command::Remove => {
arg_parser::remove(state, matches)?;
}
commands::Command::Complete => {
arg_parser::complete(app, matches)?;
}
commands::Command::Refresh => {
}
commands::Command::Update => {
}
_ => (),
};
Ok(())
}
check_for_update(version).await?;
update_rss( state, Some(config)).await?;
arg_parser::search( state, config, matches).await?;
arg_parser::subscribe( state, config, matches).await?;
arg_parser::download(state, matches).await?;
app: &mut App<'_, '_>,
matches: &ArgMatches<'_>,
pub async fn handle_matches(
use anyhow::Result;
let mut d_vec = vec![];
let subscription_limit = config
.unwrap_or_else(|| Config::default())
.download_subscription_limit
.unwrap_or(-1);
let mut episodes =
podcast.episodes()[..podcast.episodes().len() - sub.num_episodes].to_vec();
episodes.reverse();
let subscription_limit = config.download_subscription_limit.unwrap_or(-1);
let episodes = podcast.episodes()[..podcast.episodes().len() - sub.num_episodes].to_vec();
let mut to_download = vec![];
for ep in episodes.iter().take(subscription_limit as usize) {
d_vec.push(download::download(
client,
podcast.title().into(),
ep.clone(),
));
for ep in episodes.iter().rev().take(subscription_limit as usize) {
if let Some(episode) = Download::new(&state, &podcast, &ep).await? {
to_download.push(episode)
}
}
pub async fn update_rss(
state: &mut State,
config: Option<Config>,
) -> Result<()> {
println!("Checking for new episodes...");
let mut d_vec = vec![];
for (index, sub ) in state.subscriptions.iter().enumerate() {
d_vec.push(update_subscription(&state.client, index, sub, config));
}
let new_subscriptions = futures::future::join_all(d_vec).await;
for c in &new_subscriptions {
match c {
Ok([index, new_ep_count]) => {
state.subscriptions[*index].num_episodes = *new_ep_count;
},
Err(err) => {
println!("Error: {}", err);
}
}
}
println!("Done.");
Ok(())
pub async fn check_for_update(version: &str) -> Result<()> {
println!("Checking for updates...");
let resp: String =
reqwest::get("https://raw.githubusercontent.com/njaremko/podcast/master/Cargo.toml")
.await?
.text()
.await?;
let config = resp.parse::<toml::Value>()?;
let latest = config["package"]["version"]
.as_str()
.unwrap_or_else(|| panic!("Cargo.toml didn't have a version {:?}", config));
let local_version = match version::parse(&version) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse version {}: {}", &version, e);
return Ok(());
}
};
let remote_version = match version::parse(&latest) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse version {}: {}", &version, e);
return Ok(());
}
};
if local_version < remote_version {
println!("New version available: {} -> {}", version, latest);
}
Ok(())
}
pub fn remove_podcast(state: &mut State, p_search: &str) -> Result<()> {
if p_search == "*" {
state.subscriptions = vec![];
return utils::delete_all();
}
let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
for subscription in 0..state.subscriptions.len() {
let title = state.subscriptions[subscription].title.clone();
if re_pod.is_match(&title) {
state.subscriptions.remove(subscription);
utils::delete(&title)?;
}
}
Ok(())
}
use crate::{executor, structs::State};
use anyhow::Result;
use clap::{App, ArgMatches};
pub enum Command<'a> {
Download(State, ArgMatches<'a>),
List(State, ArgMatches<'a>),
Play(State, ArgMatches<'a>),
Subscribe(State, ArgMatches<'a>),
Search(State, ArgMatches<'a>),
Remove(State, ArgMatches<'a>),
Complete(State, App<'a, 'a>, ArgMatches<'a>),
Refresh(State),
Update(State),
NoMatch,
}
pub fn parse_command<'a>(state: State, app: App<'a, 'a>, matches: ArgMatches<'a>) -> Command<'a> {
matches
.subcommand_name()
.map(|command| match command {
"download" => Command::Download(
state,
matches.subcommand_matches("download").unwrap().clone(),
),
"ls" | "list" => Command::List(
state,
matches
.subcommand_matches("ls")
.or_else(|| matches.subcommand_matches("list"))
.unwrap()
.clone(),
),
"play" => Command::Play(state, matches.subcommand_matches("play").unwrap().clone()),
"sub" | "subscribe" => Command::Subscribe(
state,
matches
.subcommand_matches("sub")
.or_else(|| matches.subcommand_matches("subscribe"))
.unwrap()
.clone(),
),
"search" => {
Command::Search(state, matches.subcommand_matches("search").unwrap().clone())
}
"rm" => Command::Remove(state, matches.subcommand_matches("rm").unwrap().clone()),
"completion" => Command::Complete(
state,
app,
matches.subcommand_matches("completion").unwrap().clone(),
),
"refresh" => Command::Refresh(state),
"update" => Command::Update(state),
_ => Command::NoMatch,
})
.unwrap_or(Command::NoMatch)
}
pub async fn run_command<'a>(command: Command<'a>) -> Result<State> {
match command {
Command::Download(state, matches) => executor::download(state, &matches).await,
Command::List(state, matches) => executor::list(state, &matches),
Command::Play(state, matches) => executor::play(state, &matches),
Command::Subscribe(state, matches) => executor::subscribe(state, &matches).await,
Command::Search(state, matches) => executor::search(state, &matches).await,
Command::Remove(state, matches) => executor::remove(state, &matches),
Command::Complete(state, mut app, matches) => {
executor::complete(&mut app, &matches)?;
Ok(state)
}
Command::Refresh(mut state) => {
state.update_rss().await?;
Ok(state)
}
Command::Update(state) => {
state.check_for_update().await?;
Ok(state)
}
_ => panic!(),
}
}
use reqwest;
use thiserror::Error;
use reqwest::{self, header};
/// This handles downloading a single episode
///
/// Not to be used in conjunction with download_multiple_episodes
async fn download_episode(pb: ProgressBar, episode: Download) -> Result<()> {
pb.set_message(&format!("Downloading {}", &episode.title));
pb.set_style(
ProgressStyle::default_bar()
.template("[{eta_precise}] {msg} [{bytes_per_sec}] [{bytes}/{total_bytes}]"),
);
let client = reqwest::Client::new();
let mut request = client.get(&episode.url);
if episode.path.exists() {
let size = episode.path.metadata()?.len() - 1;
request = request.header(header::RANGE, format!("bytes={}-", size));
pb.inc(size);
}
let mut dest = smol::writer(BufWriter::new(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&episode.path)?,
));
let mut download = request.send().await?;
while let Some(chunk) = download.chunk().await? {
let written = dest.write(&chunk).await?;
pb.inc(written as u64);
}
dest.flush().await?;
pb.finish_with_message("Done");
Ok(())
}
/// Handles downloading a list of episodes on a single thread
async fn download_multiple_episodes(pb: ProgressBar, episodes: Vec<Download>) -> Result<()> {
let client = reqwest::Client::new();
for (index, episode) in episodes.iter().enumerate() {
pb.set_position(0);
pb.set_length(episode.size);
pb.set_message(&format!("Downloading {}", &episode.title));
pb.set_style(ProgressStyle::default_bar().template(
&(format!("[{}/{}]", index, episodes.len())
+ " [{eta_precise}] {msg} [{bytes_per_sec}] [{bytes}/{total_bytes}]"),
));
let mut request = client.get(&episode.url);
if episode.path.exists() {
let size = episode.path.metadata()?.len() - 1;
request = request.header(header::RANGE, format!("bytes={}-", size));
pb.inc(size);
}
let mut dest = smol::writer(BufWriter::new(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&episode.path)?,
));
let mut download = request.send().await?;
while let Some(chunk) = download.chunk().await? {
let written = dest.write(&chunk).await?;
pb.inc(written as u64);
}
dest.flush().await?;
}
pb.finish_with_message("Done");
Ok(())
}
/// Splits the given list optimally across available threads and downloads them pretty
pub async fn download_episodes(episodes: Vec<Download>) -> Result<()> {
let mp = MultiProgress::new();
let num_cpus = num_cpus::get();
if episodes.len() < num_cpus {
println!("Starting {} download threads...", episodes.len());
for episode in episodes.to_owned() {
let pb = mp.add(ProgressBar::new(episode.size));
std::thread::spawn(move || smol::block_on(download_episode(pb, episode)));
}
mp.join_and_clear()?;
return Ok(());
}
println!("Starting {} download threads...", &num_cpus);
let chunk_size = episodes.len() / num_cpus;
for chunk in episodes.chunks(chunk_size) {
let pb = mp.add(ProgressBar::new(0));
let cp = chunk.to_vec();
std::thread::spawn(move || smol::block_on(download_multiple_episodes(pb, cp)));
}
mp.join_and_clear()?;
Ok(())
}
let d = download(
&state.client,
podcast.title().into(),
episodes[episodes.len() - ep_num].clone(),
);
d_vec.push(d);
let mut path = utils::get_podcast_dir()?;
path.push(podcast.title());
utils::create_dir_if_not_exist(&path)?;
let episode = &episodes[episodes.len() - ep_num];
if let Some(ep) = Download::new(&state, &podcast, &episode).await? {
downloads.push(ep);
}
d_vec.push(download(
&state.client,
podcast.title().into(),
episodes[episodes.len() - ep_num].clone(),
));
if let Some(ep) =
Download::new(&state, &podcast, &episodes[episodes.len() - ep_num]).await?
{
downloads.push(ep);
}
Ok(())
}
#[derive(Debug, Error)]
enum DownloadError {
#[error("File already exists: {path:?}")]
AlreadyExists { path: String },
}
pub async fn download(
client: &reqwest::Client,
podcast_name: String,
episode: Episode,
) -> Result<()> {
let mut path = utils::get_podcast_dir()?;
path.push(podcast_name);
utils::create_dir_if_not_exist(&path)?;
if let (Some(mut title), Some(url)) = (episode.title(), episode.url()) {
if let Some(ext) = episode.extension() {
title = utils::append_extension(&title, &ext);
}
path.push(title);
if !path.exists() {
println!("Downloading: {:?}", &path);
let resp = client.get(url).send().await?.bytes().await?;
let file = File::create(&path)?;
let mut reader = BufReader::new(&resp[..]);
let mut writer = BufWriter::new(file);
io::copy(&mut reader, &mut writer)?;
} else {
return Err(DownloadError::AlreadyExists {
path: path.to_str().unwrap().to_string(),
}
.into());
}
}
Ok(())
Ok(downloads)
for ep in filtered_episodes {
let d = download(&state.client, podcast.title().into(), ep.clone());
d_vec.push(d);
for episode in filtered_episodes {
if let Some(ep) = Download::new(&state, &podcast, &episode).await? {
downloads.push(ep);
}
for ep in filtered_episodes.take(1) {
let d = download(&state.client, podcast.title().into(), ep.clone());
d_vec.push(d);
for episode in filtered_episodes.take(1) {
if let Some(ep) = Download::new(&state, &podcast, &episode).await? {
downloads.push(ep);
}
let mut d_vec = vec![];
for ep in &episodes[..latest] {
d_vec.push(download(&state.client, podcast.title().into(), ep.clone()));
}
for c in futures::future::join_all(d_vec).await.iter() {
if let Err(err) = c {
println!("Error: {}", err);
for episode in &episodes[..latest] {
if let Some(ep) = Download::new(&state, &podcast, &episode).await? {
downloads.push(ep);
let mut d_vec = vec![];
for ep in episodes[..download_limit].iter() {
d_vec.push(download(&state.client, podcast.title().into(), ep.clone()));
}
for c in futures::future::join_all(d_vec).await.iter() {
if let Err(err) = c {
eprintln!("Error downloading {}: {}", podcast.title(), err)
for episode in episodes[..download_limit].iter() {
if let Some(ep) = Download::new(&state, &podcast, &episode).await? {
downloads.push(ep);
use clap::{App, ArgMatches};
use std::env;
use crate::actions::*;
use crate::download;
use crate::playback;
use crate::{structs::*, utils};
use anyhow::Result;
use indicatif::ProgressBar;
use regex::Regex;
use download::download_episodes;
use std::{
io::{self, Read, Write},
path::Path,
};
struct DownloadProgress<R> {
inner: R,
progress_bar: ProgressBar,
}
impl<R: Read> Read for DownloadProgress<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf).map(|n| {
self.progress_bar.inc(n as u64);
n
})
}
}
pub async fn download(state: State, matches: &ArgMatches<'_>) -> Result<State> {
let podcast = matches.value_of("PODCAST").unwrap();
let mut to_download = vec![];
match matches.value_of("EPISODE") {
Some(ep) => {
if String::from(ep).contains(|c| c == '-' || c == ',') {
to_download.append(&mut download::download_range(&state, podcast, ep).await?);
} else if matches.occurrences_of("name") > 0 {
to_download.append(
&mut download::download_episode_by_name(
&state,
podcast,
ep,
0 < matches.occurrences_of("all"),
)
.await?,
);
} else {
to_download
.append(&mut download::download_episode_by_num(&state, podcast, ep).await?);
}
}
None => match matches.value_of("latest") {
Some(num_of_latest) => {
to_download.append(
&mut download::download_latest(&state, podcast, num_of_latest.parse()?).await?,
);
}
None => {
to_download.append(&mut download::download_all(&state, podcast).await?);
}
},
}
download_episodes(to_download).await?;
Ok(state)
}
pub fn list(state: State, matches: &ArgMatches) -> Result<State> {
match matches.value_of("PODCAST") {
Some(regex) => list_episodes(regex)?,
None => list_subscriptions(&state)?,
}
Ok(state)
}
pub fn play(state: State, matches: &ArgMatches) -> Result<State> {
let podcast = matches.value_of("PODCAST").unwrap();
match matches.value_of("EPISODE") {
Some(episode) => {
if matches.occurrences_of("name") > 0 {
playback::play_episode_by_name(&state, podcast, episode)?
} else {
playback::play_episode_by_num(&state, podcast, episode)?
}
}
None => playback::play_latest(&state, podcast)?,
}
Ok(state)
}
pub async fn subscribe(state: State, matches: &ArgMatches<'_>) -> Result<State> {
let url = matches.value_of("URL").unwrap();
sub(state, url).await
}
async fn sub(mut state: State, url: &str) -> Result<State> {
state.subscribe(url).await?;
Ok(state)
}
pub fn remove(mut state: State, matches: &ArgMatches) -> Result<State> {
let p_search = matches.value_of("PODCAST").unwrap();
if p_search == "*" {
state.subscriptions = vec![];
utils::delete_all()?;
return Ok(state);
}
let re_pod = Regex::new(&format!("(?i){}", &p_search))?;
for subscription in 0..state.subscriptions.len() {
let title = state.subscriptions[subscription].title.clone();
if re_pod.is_match(&title) {
state.subscriptions.remove(subscription);
utils::delete(&title)?;
}
}
Ok(state)
}
pub fn complete(app: &mut App, matches: &ArgMatches) -> Result<()> {
match matches.value_of("SHELL") {
Some(shell) => print_completion(app, shell),
None => {
let shell_path_env = env::var("SHELL");
if let Ok(p) = shell_path_env {
let shell_path = Path::new(&p);
if let Some(shell) = shell_path.file_name() {
print_completion(app, shell.to_str().unwrap())
}
}
}
}
Ok(())
}
pub async fn search(state: State, matches: &ArgMatches<'_>) -> Result<State> {
let podcast = matches
.values_of("PODCAST")
.unwrap()
.fold("".to_string(), |acc, x| {
if acc.is_empty() {
return acc + x;
}
acc + " " + x
});
let resp = podcast_search::search(&podcast).await?;
if resp.results.is_empty() {
println!("No Results");
return Ok(state);
}
{
let stdout = io::stdout();
let mut lock = stdout.lock();
for (i, r) in resp.results.iter().enumerate() {
writeln!(
&mut lock,
"({}) {} [{}]",
i,
r.collection_name.clone().unwrap_or_else(|| "".to_string()),
r.feed_url.clone().unwrap_or_else(|| "".to_string())
)?;
}
}
print!("Would you like to subscribe to any of these? (y/n): ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if input.to_lowercase().trim() != "y" {
return Ok(state);
}
print!("Which one? (#): ");
io::stdout().flush().ok();
let mut num_input = String::new();
io::stdin().read_line(&mut num_input)?;
let n: usize = num_input.trim().parse()?;
if n > resp.results.len() {
eprintln!("Invalid!");
return Ok(state);
}
let rss_resp = &resp.results[n];
match &rss_resp.feed_url {
Some(r) => sub(state, &r).await,
None => {
eprintln!("Subscription failed. No url in API response.");
return Ok(state);
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Create
utils::create_directories()?;
fn main() -> Result<()> {
smol::run(async {
// Create
utils::create_directories()?;
// Run CLI parser and get matches
let mut app = parser::get_app(&VERSION);
let matches = app.clone().get_matches();
// Run CLI parser and get matches
let app = parser::get_app(&VERSION);
let matches = app.clone().get_matches();
// Has the user specified that they want the CLI to do minimal output?
let is_quiet = matches.occurrences_of("quiet") != 0;
// Has the user specified that they want the CLI to do minimal output?
let is_quiet = matches.occurrences_of("quiet") != 0;
// Load config file
let config = Config::new()?;
if !config.quiet.unwrap_or(false) && !is_quiet {
let path = utils::get_podcast_dir()?;
writeln!(std::io::stdout().lock(), "Using PODCAST dir: {:?}", &path).ok();
}
// Load config file
let config = Config::load()?.unwrap_or_default();
if !config.quiet.unwrap_or(false) && !is_quiet {
let path = utils::get_podcast_dir()?;
writeln!(std::io::stdout().lock(), "Using PODCAST dir: {:?}", &path).ok();
}
fn create_new_config_file(path: &PathBuf) -> Result<Config> {
writeln!(
io::stdout().lock(),
"Creating new config file at {:?}",
&path
)
.ok();
let file = File::create(&path)?;
let config = Config {
auto_download_limit: Some(1),
download_subscription_limit: Some(1),
quiet: Some(false),
};
serde_yaml::to_writer(file, &config)?;
Ok(config)
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
/// This information is persisted to disk as part of PublicState
/// and allows for configuration of the CLI
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
match serde_yaml::from_reader(file) {
Ok(config) => config,
Err(err) => {
let mut new_path = path.clone();
new_path.set_extension("yaml.bk");
let stderr = io::stderr();
let mut handle = stderr.lock();
writeln!(
&mut handle,
"{}\nFailed to open config file, moving to {:?}",
err, &new_path
)
.ok();
fs::rename(&path, new_path)?;
create_new_config_file(&path)?
}
}
} else {
create_new_config_file(&path)?
};
Ok(config)
return Ok(Some(serde_yaml::from_reader(file)?));
}
Ok(None)
pub async fn update_rss(&mut self) -> Result<()> {
println!("Checking for new episodes...");
let mut d_vec = vec![];
for (index, sub) in self.subscriptions.iter().enumerate() {
d_vec.push(update_subscription(&self, index, sub, &self.config));
}
let new_subscriptions = futures::future::join_all(d_vec).await;
for c in &new_subscriptions {
match c {
Ok([index, new_ep_count]) => {
self.subscriptions[*index].num_episodes = *new_ep_count;
}
Err(err) => {
println!("Error: {}", err);
}
}
}
println!("Done.");
Ok(())
}
pub async fn check_for_update(&self) -> Result<()> {
println!("Checking for updates...");
let resp: String =
reqwest::get("https://raw.githubusercontent.com/njaremko/podcast/master/Cargo.toml")
.await?
.text()
.await?;
let config = resp.parse::<toml::Value>()?;
let latest = config["package"]["version"]
.as_str()
.unwrap_or_else(|| panic!("Cargo.toml didn't have a version {:?}", config));
let local_version = match version::parse(&self.version) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse version {}: {}", &self.version, e);
return Ok(());
}
};
let remote_version = match version::parse(&latest) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse version {}: {}", &self.version, e);
return Ok(());
}
};
if local_version < remote_version {
println!("New version available: {} -> {}", &self.version, latest);
}
Ok(())
}
}
/// Represent an intention to download a file
#[derive(Clone, Debug, PartialEq)]
pub struct Download {
pub title: String,
pub path: PathBuf,
pub url: String,
pub size: u64,
}
impl Download {
pub async fn new(
state: &State,
podcast: &Podcast,
episode: &Episode,
) -> Result<Option<Download>> {
let mut path = utils::get_podcast_dir()?;
path.push(podcast.title());
utils::create_dir_if_not_exist(&path)?;
if let (Some(mut title), Some(url)) = (episode.title(), episode.url()) {
if let Some(ext) = episode.extension() {
title = utils::append_extension(&title, &ext);
}
path.push(&title);
let head_resp = state.client.head(url).send().await?;
let total_size = head_resp
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|ct_len| ct_len.to_str().ok())
.and_then(|ct_len| ct_len.parse().ok())
.unwrap_or(0);
if !path.exists() {
return Ok(Some(Download {
title,
path,
url: url.into(),
size: total_size,
}));
}
}
Ok(None)
}