use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard};
use unicode_segmentation::UnicodeSegmentation;
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use nohash_hasher::BuildNoHashHasher;
use regex::Regex;
use crate::downloads::DownloadMsg;
use crate::feeds::FeedMsg;
use crate::ui::UiMsg;
lazy_static! {
static ref RE_ARTICLES: Regex = Regex::new(r"^(a|an|the) ").expect("Regex error");
}
pub trait Menuable {
fn get_id(&self) -> i64;
fn get_title(&self, length: usize) -> String;
fn is_played(&self) -> bool;
}
#[derive(Debug, Clone)]
pub struct Podcast {
pub id: i64,
pub title: String,
pub sort_title: String,
pub url: String,
pub description: Option<String>,
pub author: Option<String>,
pub explicit: Option<bool>,
pub last_checked: DateTime<Utc>,
pub episodes: LockVec<Episode>,
}
impl Podcast {
fn num_unplayed(&self) -> usize {
return self
.episodes
.map(|ep| !ep.is_played() as usize, false)
.iter()
.sum();
}
}
impl Menuable for Podcast {
fn get_id(&self) -> i64 {
return self.id;
}
fn get_title(&self, length: usize) -> String {
let mut title_length = length;
if length > crate::config::PODCAST_UNPLAYED_TOTALS_LENGTH {
let meta_str = format!("({}/{})", self.num_unplayed(), self.episodes.len(false));
title_length = length - meta_str.chars().count() - 3;
let out = self.title.substr(0, title_length);
return format!(
" {out} {meta_str:>width$} ",
width = length - out.grapheme_len() - 3
); } else {
return format!(" {} ", self.title.substr(0, title_length - 2));
}
}
fn is_played(&self) -> bool {
return self.num_unplayed() == 0;
}
}
impl PartialEq for Podcast {
fn eq(&self, other: &Self) -> bool {
return self.sort_title == other.sort_title;
}
}
impl Eq for Podcast {}
impl PartialOrd for Podcast {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
return Some(self.cmp(other));
}
}
impl Ord for Podcast {
fn cmp(&self, other: &Self) -> Ordering {
return self.sort_title.cmp(&other.sort_title);
}
}
#[derive(Debug, Clone)]
pub struct Episode {
pub id: i64,
pub pod_id: i64,
pub title: String,
pub url: String,
pub guid: String,
pub description: String,
pub pubdate: Option<DateTime<Utc>>,
pub duration: Option<i64>,
pub path: Option<PathBuf>,
pub played: bool,
}
impl Episode {
pub fn format_duration(&self) -> String {
return match self.duration {
Some(dur) => {
let mut seconds = dur;
let hours = seconds / 3600;
seconds -= hours * 3600;
let minutes = seconds / 60;
seconds -= minutes * 60;
format!("{hours:02}:{minutes:02}:{seconds:02}")
}
None => "--:--:--".to_string(),
};
}
}
impl Menuable for Episode {
fn get_id(&self) -> i64 {
return self.id;
}
fn get_title(&self, length: usize) -> String {
let out = match self.path {
Some(_) => {
let title = self.title.substr(0, length - 4);
format!("[D] {title}")
}
None => self.title.substr(0, length),
};
if length > crate::config::EPISODE_PUBDATE_LENGTH {
let dur = self.format_duration();
let meta_dur = format!("[{dur}]");
if let Some(pubdate) = self.pubdate {
let pd = pubdate.format("%F");
let meta_str = format!("({pd}) {meta_dur}");
let added_len = meta_str.chars().count();
let out_added = out.substr(0, length - added_len - 3);
return format!(
" {out_added} {meta_str:>width$} ",
width = length - out_added.grapheme_len() - 3
);
} else {
let out_added = out.substr(0, length - meta_dur.chars().count() - 3);
return format!(
" {out_added} {meta_dur:>width$} ",
width = length - out_added.grapheme_len() - 3
);
}
} else if length > crate::config::EPISODE_DURATION_LENGTH {
let dur = self.format_duration();
let meta_dur = format!("[{dur}]");
let out_added = out.substr(0, length - meta_dur.chars().count() - 3);
return format!(
" {out_added} {meta_dur:>width$} ",
width = length - out_added.grapheme_len() - 3
);
} else {
return format!(" {} ", out.substr(0, length - 2));
}
}
fn is_played(&self) -> bool {
return self.played;
}
}
#[derive(Debug, Clone)]
pub struct PodcastNoId {
pub title: String,
pub url: String,
pub description: Option<String>,
pub author: Option<String>,
pub explicit: Option<bool>,
pub last_checked: DateTime<Utc>,
pub episodes: Vec<EpisodeNoId>,
}
#[derive(Debug, Clone)]
pub struct EpisodeNoId {
pub title: String,
pub url: String,
pub guid: String,
pub description: String,
pub pubdate: Option<DateTime<Utc>>,
pub duration: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct NewEpisode {
pub id: i64,
pub pod_id: i64,
pub title: String,
pub pod_title: String,
pub selected: bool,
}
impl Menuable for NewEpisode {
fn get_id(&self) -> i64 {
return self.id;
}
fn get_title(&self, length: usize) -> String {
let selected = if self.selected { "✓" } else { " " };
let title_len = self.title.grapheme_len();
let pod_title_len = self.pod_title.grapheme_len();
let empty_string = if length > title_len + pod_title_len + 9 {
let empty = vec![" "; length - title_len - pod_title_len - 9];
empty.join("")
} else {
"".to_string()
};
let full_string = format!(
" [{}] {} ({}){} ",
selected, self.title, self.pod_title, empty_string
);
return full_string.substr(0, length);
}
fn is_played(&self) -> bool {
return true;
}
}
#[derive(Debug)]
pub struct LockVec<T>
where T: Clone + Menuable
{
data: Arc<Mutex<HashMap<i64, T, BuildNoHashHasher<i64>>>>,
order: Arc<Mutex<Vec<i64>>>,
filtered_order: Arc<Mutex<Vec<i64>>>,
}
impl<T: Clone + Menuable> LockVec<T> {
pub fn new(data: Vec<T>) -> LockVec<T> {
let mut hm = HashMap::with_hasher(BuildNoHashHasher::default());
let mut order = Vec::new();
for i in data.into_iter() {
let id = i.get_id();
hm.insert(i.get_id(), i);
order.push(id);
}
return LockVec {
data: Arc::new(Mutex::new(hm)),
order: Arc::new(Mutex::new(order.clone())),
filtered_order: Arc::new(Mutex::new(order)),
};
}
pub fn borrow_map(&self) -> MutexGuard<HashMap<i64, T, BuildNoHashHasher<i64>>> {
return self.data.lock().expect("Mutex error");
}
pub fn borrow_order(&self) -> MutexGuard<Vec<i64>> {
return self.order.lock().expect("Mutex error");
}
pub fn borrow_filtered_order(&self) -> MutexGuard<Vec<i64>> {
return self.filtered_order.lock().expect("Mutex error");
}
#[allow(clippy::type_complexity)]
pub fn borrow(
&self,
) -> (
MutexGuard<HashMap<i64, T, BuildNoHashHasher<i64>>>,
MutexGuard<Vec<i64>>,
MutexGuard<Vec<i64>>,
) {
return (
self.data.lock().expect("Mutex error"),
self.order.lock().expect("Mutex error"),
self.filtered_order.lock().expect("Mutex error"),
);
}
pub fn replace(&self, id: i64, t: T) {
let mut borrowed = self.borrow_map();
borrowed.insert(id, t);
}
pub fn replace_all(&self, data: Vec<T>) {
let (mut map, mut order, mut filtered_order) = self.borrow();
map.clear();
order.clear();
filtered_order.clear();
for i in data.into_iter() {
let id = i.get_id();
map.insert(i.get_id(), i);
order.push(id);
filtered_order.push(id);
}
}
pub fn map<B, F>(&self, mut f: F, filtered: bool) -> Vec<B>
where F: FnMut(&T) -> B {
let (map, order, filtered_order) = self.borrow();
if filtered {
return filtered_order
.iter()
.map(|id| f(map.get(id).expect("Index error in LockVec")))
.collect();
} else {
return order
.iter()
.map(|id| f(map.get(id).expect("Index error in LockVec")))
.collect();
}
}
pub fn map_single<B, F>(&self, id: i64, f: F) -> Option<B>
where F: FnOnce(&T) -> B {
let borrowed = self.borrow_map();
return match borrowed.get(&id) {
Some(item) => Some(f(item)),
None => return None,
};
}
pub fn map_single_by_index<B, F>(&self, index: usize, f: F) -> Option<B>
where F: FnOnce(&T) -> B {
let order = self.borrow_filtered_order();
return match order.get(index) {
Some(id) => self.map_single(*id, f),
None => None,
};
}
pub fn filter_map<B, F>(&self, mut f: F) -> Vec<B>
where F: FnMut(&T) -> Option<B> {
let (map, order, _) = self.borrow();
return order
.iter()
.filter_map(|id| f(map.get(id).expect("Index error in LockVec")))
.collect();
}
pub fn len(&self, filtered: bool) -> usize {
if filtered {
return self.borrow_filtered_order().len();
} else {
return self.borrow_order().len();
}
}
pub fn is_empty(&self) -> bool {
return self.borrow_order().is_empty();
}
}
impl<T: Clone + Menuable> Clone for LockVec<T> {
fn clone(&self) -> Self {
return LockVec {
data: Arc::clone(&self.data),
order: Arc::clone(&self.order),
filtered_order: Arc::clone(&self.filtered_order),
};
}
}
impl LockVec<Podcast> {
pub fn clone_podcast(&self, id: i64) -> Option<Podcast> {
let pod_map = self.borrow_map();
return pod_map.get(&id).cloned();
}
pub fn clone_episode(&self, pod_id: i64, ep_id: i64) -> Option<Episode> {
let pod_map = self.borrow_map();
if let Some(pod) = pod_map.get(&pod_id) {
return pod.episodes.clone_episode(ep_id);
}
return None;
}
}
impl LockVec<Episode> {
pub fn clone_episode(&self, ep_id: i64) -> Option<Episode> {
let ep_map = self.borrow_map();
return ep_map.get(&ep_id).cloned();
}
}
#[derive(Debug)]
pub enum Message {
Ui(UiMsg),
Feed(FeedMsg),
Dl(DownloadMsg),
}
#[derive(Debug, Clone, Copy)]
pub enum FilterStatus {
PositiveCases,
NegativeCases,
All,
}
#[derive(Debug, Clone, Copy)]
pub enum FilterType {
Played,
Downloaded,
}
#[derive(Debug, Clone, Copy)]
pub struct Filters {
pub played: FilterStatus,
pub downloaded: FilterStatus,
}
impl Default for Filters {
fn default() -> Self {
return Self {
played: FilterStatus::All,
downloaded: FilterStatus::All,
};
}
}
pub trait StringUtils {
fn substr(&self, start: usize, length: usize) -> String;
fn grapheme_len(&self) -> usize;
}
impl StringUtils for String {
fn substr(&self, start: usize, length: usize) -> String {
return self
.graphemes(true)
.skip(start)
.take(length)
.collect::<String>();
}
fn grapheme_len(&self) -> usize {
return self.graphemes(true).count();
}
}