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_episodesasync 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 threadasync 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 prettypub 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<()> {// Createutils::create_directories()?;
fn main() -> Result<()> {smol::run(async {// Createutils::create_directories()?;
// Run CLI parser and get matcheslet mut app = parser::get_app(&VERSION);let matches = app.clone().get_matches();
// Run CLI parser and get matcheslet 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 filelet 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 filelet 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)}