1use std::{
2 cmp::Ordering,
3 collections::HashMap,
4 fmt,
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8
9use chrono::{DateTime, FixedOffset};
10use serde::{Deserialize, Serialize};
11
12use anyhow::Result;
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct PullEntryFilter {
16 pub profile: Option<String>,
17 pub title_contains: Option<String>,
18 pub title_is: Option<String>,
19 pub season_is: Option<u32>,
20 pub episode_is: Option<Episode>,
21 pub state: Option<PullState>,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct SearchResult {
26 pub title: String,
27 pub torrent_link: String,
28 pub view_link: String,
29 pub date: DateTime<FixedOffset>,
30 pub seeders: u64,
31 pub leechers: u64,
32 pub downloads: u64,
33 pub size: u64,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
37#[serde(untagged)]
38pub enum Episode {
39 Standard(u32),
40 Special(String),
41}
42
43impl PartialOrd for Episode {
44 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45 match (self, other) {
46 (Episode::Standard(e1), Episode::Standard(e2)) => e1.partial_cmp(e2),
47 (Episode::Standard(_), Episode::Special(_)) => Some(Ordering::Less),
48 (Episode::Special(_), Episode::Standard(_)) => Some(Ordering::Greater),
49 (Episode::Special(e1), Episode::Special(e2)) => e1.partial_cmp(e2),
50 }
51 }
52}
53
54impl Ord for Episode {
55 fn cmp(&self, other: &Self) -> Ordering {
56 self.partial_cmp(other).unwrap()
57 }
58}
59
60impl FromStr for Episode {
61 type Err = ();
62
63 fn from_str(s: &str) -> Result<Self, Self::Err> {
64 if let Some(s) = s.parse().ok() {
65 Ok(Episode::Standard(s))
66 } else {
67 Ok(Episode::Special(s.to_string()))
68 }
69 }
70}
71
72impl Default for Episode {
73 fn default() -> Self {
74 Episode::Standard(0)
75 }
76}
77
78impl fmt::Display for Episode {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Episode::Standard(x) => write!(f, "{}", x),
82 Episode::Special(x) => write!(f, "{}", x),
83 }
84 }
85}
86
87#[derive(Default, Debug, Clone, Serialize, Deserialize)]
88pub struct StandardEpisode {
89 pub title: String,
90 pub season: u32,
91 pub episode: Episode,
92 pub checksum: u32,
93 pub ext: HashMap<String, String>,
94}
95
96#[derive(Serialize, Deserialize)]
97pub struct PullEntryNamed {
98 pub id: String,
99 #[serde(flatten)]
100 pub pull_entry: PullEntry,
101}
102
103#[derive(Debug, Serialize, Deserialize)]
104pub struct PullEntry {
105 pub result: ParsedSearchResult,
106 pub torrent_id: Option<i64>,
107 pub torrent_hash: String,
108 pub state: PullState,
109 #[serde(default)]
110 pub files: Vec<String>,
111}
112
113impl PullEntry {
114 pub fn key(&self) -> String {
115 self.result.key()
116 }
117}
118
119#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
120pub enum PullState {
121 Downloading,
122 Finished,
123}
124
125impl fmt::Display for PullState {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 match self {
128 PullState::Downloading => write!(f, "downloading"),
129 PullState::Finished => write!(f, "finished"),
130 }
131 }
132}
133
134impl FromStr for PullState {
135 type Err = ();
136
137 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
138 match s {
139 "downloading" => Ok(PullState::Downloading),
140 "finished" => Ok(PullState::Finished),
141 _ => Err(()),
142 }
143 }
144}
145
146#[derive(Clone, Debug, Serialize, Deserialize)]
147pub struct ParsedSearchResult {
148 pub result: SearchResult,
149 pub parsed: StandardEpisode,
150 pub profile: String,
151 pub relocate: Option<String>,
152 pub relocate_season: bool,
153}
154
155impl ParsedSearchResult {
156 pub fn key(&self) -> String {
157 format!(
158 "{}_S{:02}E{:02}",
159 self.parsed.title, self.parsed.season, self.parsed.episode
160 )
161 }
162
163 pub fn relocate_dir(&self) -> Option<PathBuf> {
164 let mut relocate = Path::new(self.relocate.as_ref()?).to_owned();
165 if self.relocate_season {
166 relocate = relocate.join(format!("Season {}", self.parsed.season));
167 }
168 Some(relocate)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_episode_order() {
178 assert!(Episode::Standard(5) < Episode::Standard(10));
179 assert!(Episode::Standard(15) < Episode::Special("test".to_string()));
180 assert!(Episode::Special("2".to_string()) < Episode::Special("20".to_string()));
181 }
182}