use crate::config::{Keys, Settings};
use crate::podcast::{download_list, EpData, PodcastFeed, PodcastNoId};
use crate::track::MediaType;
use crate::ui::{Id, Model, Msg, PCMsg};
use anyhow::{anyhow, bail, Result};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use sanitize_filename::{sanitize_with_options, Options};
use serde_json::Value;
use std::time::Duration;
use tui_realm_stdlib::List;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{Alignment, BorderType, TableBuilder, TextSpan};
use tuirealm::props::{Borders, Color};
use tuirealm::{
event::{Key, KeyEvent, KeyModifiers, NoUserEvent},
AttrValue, Attribute, Component, Event, MockComponent, State, StateValue,
};
#[derive(MockComponent)]
pub struct FeedsList {
component: List,
on_key_tab: Msg,
on_key_backtab: Msg,
keys: Keys,
}
impl FeedsList {
pub fn new(config: &Settings, on_key_tab: Msg, on_key_backtab: Msg) -> Self {
Self {
component: List::default()
.borders(
Borders::default().modifiers(BorderType::Rounded).color(
config
.style_color_symbol
.library_border()
.unwrap_or(Color::Blue),
),
)
.background(
config
.style_color_symbol
.library_background()
.unwrap_or(Color::Reset),
)
.foreground(
config
.style_color_symbol
.library_foreground()
.unwrap_or(Color::Yellow),
)
.title(" Podcast Feeds: ", Alignment::Left)
.scroll(true)
.highlighted_color(
config
.style_color_symbol
.library_highlight()
.unwrap_or(Color::LightBlue),
)
.highlighted_str(&config.style_color_symbol.library_highlight_symbol)
.rewind(false)
.step(4)
.scroll(true)
.rows(
TableBuilder::default()
.add_col(TextSpan::from("Empty"))
.build(),
),
on_key_tab,
on_key_backtab,
keys: config.keys.clone(),
}
}
}
impl Component<Msg, NoUserEvent> for FeedsList {
#[allow(clippy::too_many_lines)]
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let _cmd_result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Down,
modifiers: KeyModifiers::NONE,
}) => {
if let Some(AttrValue::Table(t)) = self.query(Attribute::Content) {
if let State::One(StateValue::Usize(index)) = self.state() {
if index >= t.len() - 1 {
return Some(self.on_key_tab.clone());
}
}
}
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(KeyEvent {
code: Key::Up,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Move(Direction::Up)),
Event::Keyboard(key) if key == self.keys.global_down.key_event() => {
if let Some(AttrValue::Table(t)) = self.query(Attribute::Content) {
if let State::One(StateValue::Usize(index)) = self.state() {
if index >= t.len() - 1 {
return Some(self.on_key_tab.clone());
}
}
}
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(key) if key == self.keys.global_up.key_event() => {
self.perform(Cmd::Move(Direction::Up))
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Scroll(Direction::Down)),
Event::Keyboard(KeyEvent {
code: Key::PageUp,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Scroll(Direction::Up)),
Event::Keyboard(key) if key == self.keys.global_goto_top.key_event() => {
self.perform(Cmd::GoTo(Position::Begin))
}
Event::Keyboard(key) if key == self.keys.global_goto_bottom.key_event() => {
self.perform(Cmd::GoTo(Position::End))
}
Event::Keyboard(KeyEvent {
code: Key::Home,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::Begin)),
Event::Keyboard(KeyEvent {
code: Key::Enter | Key::Right,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::PodcastSelected(index)));
}
CmdResult::None
}
Event::Keyboard(key) if key == self.keys.global_right.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::PodcastSelected(index)));
}
CmdResult::None
}
Event::Keyboard(KeyEvent {
code: Key::End,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::End)),
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => {
return Some(self.on_key_tab.clone());
}
Event::Keyboard(KeyEvent {
code: Key::BackTab,
modifiers: KeyModifiers::SHIFT,
}) => return Some(self.on_key_backtab.clone()),
Event::Keyboard(keyevent)
if keyevent == self.keys.podcast_search_add_feed.key_event() =>
{
return Some(Msg::Podcast(PCMsg::PodcastAddPopupShow));
}
Event::Keyboard(keyevent) if keyevent == self.keys.podcast_refresh_feed.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::PodcastRefreshOne(index)));
}
CmdResult::None
}
Event::Keyboard(keyevent)
if keyevent == self.keys.podcast_refresh_all_feeds.key_event() =>
{
return Some(Msg::Podcast(PCMsg::PodcastRefreshAll));
}
Event::Keyboard(keyevent) if keyevent == self.keys.podcast_delete_feed.key_event() => {
return Some(Msg::Podcast(PCMsg::FeedDeleteShow));
}
Event::Keyboard(keyevent)
if keyevent == self.keys.podcast_delete_all_feeds.key_event() =>
{
return Some(Msg::Podcast(PCMsg::FeedsDeleteShow));
}
_ => CmdResult::None,
};
Some(Msg::None)
}
}
#[derive(MockComponent)]
pub struct EpisodeList {
component: List,
on_key_tab: Msg,
on_key_backtab: Msg,
keys: Keys,
}
impl EpisodeList {
pub fn new(config: &Settings, on_key_tab: Msg, on_key_backtab: Msg) -> Self {
Self {
component: List::default()
.borders(
Borders::default().modifiers(BorderType::Rounded).color(
config
.style_color_symbol
.library_border()
.unwrap_or(Color::Blue),
),
)
.background(
config
.style_color_symbol
.library_background()
.unwrap_or(Color::Reset),
)
.foreground(
config
.style_color_symbol
.library_foreground()
.unwrap_or(Color::Yellow),
)
.title(" Episodes: ", Alignment::Left)
.scroll(true)
.highlighted_color(
config
.style_color_symbol
.library_highlight()
.unwrap_or(Color::LightBlue),
)
.highlighted_str(&config.style_color_symbol.library_highlight_symbol)
.rewind(false)
.step(4)
.scroll(true)
.rows(
TableBuilder::default()
.add_col(TextSpan::from("Empty"))
.build(),
),
on_key_tab,
on_key_backtab,
keys: config.keys.clone(),
}
}
}
impl Component<Msg, NoUserEvent> for EpisodeList {
#[allow(clippy::too_many_lines)]
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let _cmd_result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Down,
modifiers: KeyModifiers::NONE,
}) => {
self.perform(Cmd::Move(Direction::Down));
return Some(Msg::Podcast(PCMsg::DescriptionUpdate));
}
Event::Keyboard(KeyEvent {
code: Key::Up,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
if index == 0 {
return Some(self.on_key_backtab.clone());
}
}
self.perform(Cmd::Move(Direction::Up));
return Some(Msg::Podcast(PCMsg::DescriptionUpdate));
}
Event::Keyboard(key) if key == self.keys.global_down.key_event() => {
self.perform(Cmd::Move(Direction::Down));
return Some(Msg::Podcast(PCMsg::DescriptionUpdate));
}
Event::Keyboard(key) if key == self.keys.global_up.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
if index == 0 {
return Some(self.on_key_backtab.clone());
}
}
self.perform(Cmd::Move(Direction::Up));
return Some(Msg::Podcast(PCMsg::DescriptionUpdate));
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Scroll(Direction::Down)),
Event::Keyboard(KeyEvent {
code: Key::PageUp,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Scroll(Direction::Up)),
Event::Keyboard(key) if key == self.keys.global_goto_top.key_event() => {
self.perform(Cmd::GoTo(Position::Begin))
}
Event::Keyboard(key) if key == self.keys.global_goto_bottom.key_event() => {
self.perform(Cmd::GoTo(Position::End))
}
Event::Keyboard(KeyEvent {
code: Key::Home,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::Begin)),
Event::Keyboard(KeyEvent {
code: Key::End,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::End)),
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => return Some(self.on_key_tab.clone()),
Event::Keyboard(KeyEvent {
code: Key::BackTab,
modifiers: KeyModifiers::SHIFT,
}) => return Some(self.on_key_backtab.clone()),
Event::Keyboard(KeyEvent {
code: Key::Enter | Key::Right,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::EpisodeAdd(index)));
}
CmdResult::None
}
Event::Keyboard(keyevent) if keyevent == self.keys.global_right.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::EpisodeAdd(index)));
}
CmdResult::None
}
Event::Keyboard(keyevent) if keyevent == self.keys.podcast_mark_played.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::EpisodeMarkPlayed(index)));
}
CmdResult::None
}
Event::Keyboard(keyevent)
if keyevent == self.keys.podcast_mark_all_played.key_event() =>
{
return Some(Msg::Podcast(PCMsg::EpisodeMarkAllPlayed));
}
Event::Keyboard(keyevent)
if keyevent == self.keys.podcast_episode_download.key_event() =>
{
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::EpisodeDownload(index)));
}
CmdResult::None
}
Event::Keyboard(keyevent)
if keyevent == self.keys.podcast_episode_delete_file.key_event() =>
{
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::Podcast(PCMsg::EpisodeDeleteFile(index)));
}
CmdResult::None
}
_ => CmdResult::None,
};
Some(Msg::None)
}
}
impl Model {
pub fn podcast_search_itunes(&self, search_str: &str) {
let encoded: String = utf8_percent_encode(search_str, NON_ALPHANUMERIC).to_string();
let url = format!(
"https://itunes.apple.com/search?media=podcast&entity=podcast&term={}",
encoded
);
let agent = ureq::builder()
.timeout_connect(Duration::from_secs(5))
.timeout_read(Duration::from_secs(20))
.build();
let mut max_retries = self.config.podcast_max_retries;
let tx = self.tx_to_main.clone();
std::thread::spawn(move || {
let request: Result<ureq::Response> = loop {
let response = agent.get(&url).call();
if let Ok(resp) = response {
break Ok(resp);
}
max_retries -= 1;
if max_retries == 0 {
break Err(anyhow!("No response from feed"));
}
};
match request {
Ok(result) => match result.status() {
200 => match result.into_string() {
Ok(text) => {
if let Some(vec) = parse_itunes_results(&text) {
tx.send(Msg::Podcast(PCMsg::SearchSuccess(vec))).ok();
} else {
tx.send(Msg::Podcast(PCMsg::SearchError(
"Error parsing result".to_string(),
)))
.ok();
}
}
Err(_) => {
tx.send(Msg::Podcast(PCMsg::SearchError(
"Error in into_string".to_string(),
)))
.ok();
}
},
_ => {
tx.send(Msg::Podcast(PCMsg::SearchError(
"Error result status code".to_string(),
)))
.ok();
}
},
Err(e) => {
tx.send(Msg::Podcast(PCMsg::SearchError(e.to_string())))
.ok();
}
}
});
}
pub fn podcast_add(&mut self, url: &str) {
let feed = PodcastFeed::new(None, url, None);
crate::podcast::check_feed(
feed,
self.config.podcast_max_retries,
&self.threadpool,
self.tx_to_main.clone(),
);
}
pub fn podcast_sync_feeds_and_episodes(&mut self) {
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in self.podcasts.iter().enumerate() {
if idx > 0 {
table.add_row();
}
let new = record.num_unplayed();
let total = record.episodes.len();
if new > 0 {
table.add_col(TextSpan::new(format!("{} ({new}/{total})", record.title)).bold());
continue;
}
table.add_col(TextSpan::new(format!("{} ({new}/{total})", record.title)));
}
if self.podcasts.is_empty() {
table.add_col(TextSpan::from("empty feeds list"));
}
let table = table.build();
self.app
.attr(
&Id::Podcast,
tuirealm::Attribute::Content,
tuirealm::AttrValue::Table(table),
)
.ok();
if let Err(e) = self.podcast_sync_episodes() {
self.mount_error_popup(format!("Error sync episodes: {e}"));
}
}
pub fn podcast_sync_episodes(&mut self) -> Result<()> {
if self.podcasts.is_empty() {
let mut table: TableBuilder = TableBuilder::default();
table.add_col(TextSpan::from("empty episodes list"));
let table = table.build();
self.app
.attr(
&Id::Episode,
tuirealm::Attribute::Content,
tuirealm::AttrValue::Table(table),
)
.ok();
self.lyric_update();
return Ok(());
}
let podcast_selected = self
.podcasts
.get(self.podcasts_index)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in podcast_selected.episodes.iter().enumerate() {
if idx > 0 {
table.add_row();
}
let mut title = record.title.clone();
if record.path.is_some() {
title = format!("[D] {title}");
}
if record.played {
table.add_col(TextSpan::new(title).strikethrough());
continue;
}
table.add_col(TextSpan::new(title).bold());
}
if podcast_selected.episodes.is_empty() {
table.add_col(TextSpan::from("empty episodes list"));
}
let table = table.build();
self.app
.attr(
&Id::Episode,
tuirealm::Attribute::Content,
tuirealm::AttrValue::Table(table),
)
.ok();
Ok(())
}
pub fn episode_mark_played(&mut self, index: usize) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
let podcast_selected = self
.podcasts
.get_mut(self.podcasts_index)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
let ep = podcast_selected
.episodes
.get_mut(index)
.ok_or_else(|| anyhow!("get episode selected failed"))?;
ep.played = !ep.played;
self.db_podcast.set_played_status(ep.id, ep.played)?;
self.podcast_sync_feeds_and_episodes();
Ok(())
}
pub fn episode_mark_all_played(&mut self) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
let mut ep_index = 0;
if let Ok(idx) = self.podcast_get_episode_index() {
ep_index = idx;
}
let podcast_selected = self
.podcasts
.get_mut(self.podcasts_index)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
let played = podcast_selected
.episodes
.get(ep_index)
.ok_or_else(|| anyhow!("get first episode failed."))?
.played;
let mut epid_vec = Vec::new();
for ep in &mut podcast_selected.episodes {
epid_vec.push(ep.id);
ep.played = !played;
}
self.db_podcast.set_all_played_status(&epid_vec, !played)?;
self.podcast_sync_feeds_and_episodes();
Ok(())
}
pub fn add_or_sync_data(&mut self, pod: &PodcastNoId, pod_id: Option<i64>) -> Result<()> {
let db_result;
if let Some(id) = pod_id {
db_result = self.db_podcast.update_podcast(id, pod);
} else {
db_result = self.db_podcast.insert_podcast(pod);
}
match db_result {
Ok(_result) => {
{
self.podcasts = self.db_podcast.get_podcasts()?;
self.podcast_sync_feeds_and_episodes();
Ok(())
}
}
Err(e) => Err(e),
}
}
pub fn podcast_refresh_feeds(&mut self, index: Option<usize>) -> Result<()> {
let mut pod_data = Vec::new();
match index {
Some(i) => {
if self.podcasts.is_empty() {
return Ok(());
}
let pod_selected = self
.podcasts
.get(i)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
let pcf = PodcastFeed::new(
Some(pod_selected.id),
&pod_selected.url.clone(),
Some(pod_selected.title.clone()),
);
pod_data.push(pcf);
}
None => {
pod_data = self
.podcasts
.iter()
.map(|pod| {
PodcastFeed::new(Some(pod.id), &pod.url.clone(), Some(pod.title.clone()))
})
.collect();
}
}
for feed in pod_data {
crate::podcast::check_feed(
feed,
self.config.podcast_max_retries,
&self.threadpool,
self.tx_to_main.clone(),
);
}
self.podcast_sync_feeds_and_episodes();
Ok(())
}
pub fn episode_download(&mut self, index: Option<usize>) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
let podcast_selected = self
.podcasts
.get_mut(self.podcasts_index)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
let pod_title;
let mut ep_data = Vec::new();
{
pod_title = podcast_selected.title.clone();
match index {
Some(idx) => {
let ep = podcast_selected
.episodes
.get_mut(idx)
.ok_or_else(|| anyhow!("get episode selected failed"))?;
let data = EpData {
id: ep.id,
pod_id: ep.pod_id,
title: ep.title.clone(),
url: ep.url.clone(),
pubdate: ep.pubdate,
file_path: None,
};
if ep.path.is_none() && !self.download_tracker.contains(&ep.url) {
ep_data.push(data);
}
}
None => {
ep_data = podcast_selected
.episodes
.iter()
.filter_map(|ep| {
if ep.path.is_none() && !self.download_tracker.contains(&ep.url) {
Some(EpData {
id: ep.id,
pod_id: ep.pod_id,
title: ep.title.clone(),
url: ep.url.clone(),
pubdate: ep.pubdate,
file_path: None,
})
} else {
None
}
})
.collect();
}
}
}
if !ep_data.is_empty() {
let dir_name = sanitize_with_options(
&pod_title,
Options {
truncate: true,
windows: true, replacement: "",
},
);
match crate::utils::create_podcast_dir(&self.config, dir_name) {
Ok(path) => {
download_list(
ep_data,
&path,
self.config.podcast_max_retries,
&self.threadpool,
&self.tx_to_main,
);
}
Err(_) => bail!("Could not create dir: {pod_title}"),
}
}
Ok(())
}
pub fn episode_download_complete(&mut self, ep_data: EpData) -> Result<()> {
let file_path = ep_data.file_path.unwrap();
let res = self.db_podcast.insert_file(ep_data.id, &file_path);
if res.is_err() {
bail!(
"Could not add episode file to database: {}",
file_path.to_string_lossy()
);
}
let podcasts = self.db_podcast.get_podcasts()?;
self.podcasts = podcasts;
self.podcast_sync_feeds_and_episodes();
self.episode_update_playlist();
Ok(())
}
pub fn episode_delete_file(&mut self, ep_index: usize) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
let podcast_selected = self
.podcasts
.get_mut(self.podcasts_index)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
let ep = podcast_selected
.episodes
.get_mut(ep_index)
.ok_or_else(|| anyhow!("get episode selected failed"))?;
if ep.path.is_some() {
let title = &ep.title;
let path = ep.path.clone().unwrap();
match std::fs::remove_file(path) {
Ok(_) => {
self.db_podcast.remove_file(ep.id).map_err(|e| {
anyhow!(format!("Could not remove file from db: {title} {e}"))
})?;
ep.path = None;
}
Err(e) => bail!(format!("Error deleting \"{title}\": {e}")),
}
}
self.podcast_sync_feeds_and_episodes();
self.episode_update_playlist();
Ok(())
}
fn episode_update_playlist(&mut self) {
self.player.playlist.reload().ok();
self.playlist_sync();
}
pub fn podcast_delete_files(&mut self, pod_index: usize) -> Result<()> {
let mut eps_to_remove = Vec::new();
let mut success = true;
{
let podcast_selected = self
.podcasts
.get_mut(pod_index)
.ok_or_else(|| anyhow!("get podcast selected failed."))?;
for ep in &mut podcast_selected.episodes {
if ep.path.is_some() {
match std::fs::remove_file(ep.path.clone().unwrap()) {
Ok(()) => {
eps_to_remove.push(ep.id);
ep.path = None;
}
Err(_) => success = false,
}
}
}
}
self.db_podcast.remove_files(&eps_to_remove)?;
if !success {
bail!("Error happend when removing local file. Please check.");
}
Ok(())
}
pub fn podcast_remove_all_feeds(&mut self) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
let len = self.podcasts.len();
for index in 0..len {
self.podcast_delete_files(index).ok();
}
self.db_podcast.clear_db()?;
self.podcasts = Vec::new();
self.podcasts_index = 0;
self.podcast_sync_feeds_and_episodes();
self.episode_update_playlist();
Ok(())
}
pub fn podcast_remove_feed(&mut self) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
if let Ok(feed_index) = self.podcast_get_feed_index() {
self.podcast_delete_files(feed_index)?;
let podcast_selected = self.podcasts.remove(feed_index);
self.db_podcast.remove_podcast(podcast_selected.id)?;
}
self.podcasts_index = self.podcasts_index.saturating_sub(1);
self.podcast_sync_feeds_and_episodes();
self.episode_update_playlist();
Ok(())
}
fn podcast_get_feed_index(&self) -> Result<usize> {
if let Ok(State::One(StateValue::Usize(feed_index))) = self.app.state(&Id::Podcast) {
return Ok(feed_index);
}
Err(anyhow!("cannot get feed index"))
}
fn podcast_get_episode_index(&self) -> Result<usize> {
if let Ok(State::One(StateValue::Usize(episode_index))) = self.app.state(&Id::Episode) {
return Ok(episode_index);
}
Err(anyhow!("cannot get feed index"))
}
pub fn podcast_mark_current_track_played(&mut self) -> Result<()> {
if self.podcasts.is_empty() {
return Ok(());
}
if let Some(track) = self.player.playlist.current_track() {
if let Some(MediaType::Podcast) = track.media_type {
if let Some(url) = track.file() {
'outer: for pod in &mut self.podcasts {
for ep in &mut pod.episodes {
if ep.url == url {
if !ep.played {
ep.played = true;
self.db_podcast.set_played_status(ep.id, ep.played)?;
}
break 'outer;
}
}
}
}
}
}
self.podcast_sync_feeds_and_episodes();
Ok(())
}
pub fn podcast_get_album_photo_by_url(&self, url: &str) -> Option<String> {
if self.podcasts.is_empty() {
return None;
}
for pod in &self.podcasts {
for ep in &pod.episodes {
if ep.url == url {
return pod.image_url.clone();
}
}
}
None
}
#[cfg(not(any(feature = "mpv", feature = "gst")))]
pub fn podcast_get_episode_index_by_url(&mut self, url: &str) -> Option<usize> {
if self.podcasts.is_empty() {
return None;
}
for (idx_pod, pod) in self.podcasts.iter().enumerate() {
for (idx_ep, ep) in pod.episodes.iter().enumerate() {
if ep.url == url {
self.podcasts_index = idx_pod;
return Some(idx_ep);
}
}
}
None
}
}
fn parse_itunes_results(data: &str) -> Option<Vec<PodcastFeed>> {
if let Ok(value) = serde_json::from_str::<Value>(data) {
let mut vec: Vec<PodcastFeed> = Vec::new();
let array = value.get("results")?.as_array()?;
for v in array.iter() {
if let Some((title, url)) = parse_itunes_item(v) {
vec.push(PodcastFeed {
id: None,
url,
title: Some(title),
});
}
}
return Some(vec);
}
None
}
fn parse_itunes_item(v: &Value) -> Option<(String, String)> {
let title = v.get("collectionName")?.as_str()?.to_owned();
let url = v.get("feedUrl")?.as_str()?.to_owned();
Some((title, url))
}