flix_fs/scanner/
season.rs

1//! The episode scanner will scan a folder and its children
2
3use std::ffi::OsStr;
4use std::path::Path;
5
6use flix_model::id::ShowId;
7use flix_model::numbers::{EpisodeNumber, EpisodeNumbers, SeasonNumber};
8
9use async_stream::stream;
10use tokio::fs;
11use tokio_stream::Stream;
12use tokio_stream::wrappers::ReadDirStream;
13
14use crate::Error;
15use crate::macros::is_image_extension;
16use crate::scanner::episode;
17
18/// A season item
19pub type Item = crate::Item<Scanner>;
20
21/// The scanner for seasons
22pub enum Scanner {
23	/// A scanned episode
24	Season {
25		/// The ID of the show this season belongs to
26		show: ShowId,
27		/// The season this episode belongs to
28		season: SeasonNumber,
29		/// The file name of the poster file
30		poster_file_name: Option<String>,
31	},
32	/// A scanned episode
33	Episode {
34		/// The ID of the show this episode belongs to
35		show: ShowId,
36		/// The season this episode belongs to
37		season: SeasonNumber,
38		/// The number(s) of this episode
39		episode: EpisodeNumbers,
40		/// The file name of the media file
41		media_file_name: String,
42		/// The file name of the poster file
43		poster_file_name: Option<String>,
44	},
45}
46
47impl From<episode::Scanner> for Scanner {
48	fn from(value: episode::Scanner) -> Self {
49		match value {
50			episode::Scanner::Episode {
51				show,
52				season,
53				episode,
54				media_file_name,
55				poster_file_name,
56			} => Self::Episode {
57				show,
58				season,
59				episode,
60				media_file_name,
61				poster_file_name,
62			},
63		}
64	}
65}
66
67impl Scanner {
68	/// Scan a folder for a season and its episodes
69	pub fn scan_season(
70		path: &Path,
71		show: ShowId,
72		season: SeasonNumber,
73	) -> impl Stream<Item = Item> {
74		stream!({
75			let dirs = match fs::read_dir(path).await {
76				Ok(dirs) => dirs,
77				Err(err) => {
78					yield Item {
79						path: path.to_owned(),
80						event: Err(Error::ReadDir(err)),
81					};
82					return;
83				}
84			};
85
86			let mut poster_file_name = None;
87			let mut episode_dirs_to_scan = Vec::new();
88
89			for await dir in ReadDirStream::new(dirs) {
90				match dir {
91					Ok(dir) => {
92						let filetype = match dir.file_type().await {
93							Ok(filetype) => filetype,
94							Err(err) => {
95								yield Item {
96									path: path.to_owned(),
97									event: Err(Error::FileType(err)),
98								};
99								continue;
100							}
101						};
102
103						let path = dir.path();
104						if filetype.is_dir() {
105							episode_dirs_to_scan.push(path);
106							continue;
107						}
108
109						match path.extension().and_then(OsStr::to_str) {
110							is_image_extension!() => {
111								if poster_file_name.is_some() {
112									yield Item {
113										path: path.to_owned(),
114										event: Err(Error::DuplicatePosterFile),
115									};
116									continue;
117								}
118								poster_file_name = path
119									.file_name()
120									.and_then(|s| s.to_str())
121									.map(ToOwned::to_owned);
122							}
123							Some(_) | None => {
124								yield Item {
125									path: path.to_owned(),
126									event: Err(Error::UnexpectedFile),
127								};
128							}
129						}
130					}
131					Err(err) => {
132						yield Item {
133							path: path.to_owned(),
134							event: Err(Error::ReadDirEntry(err)),
135						}
136					}
137				}
138			}
139
140			yield Item {
141				path: path.to_owned(),
142				event: Ok(Self::Season {
143					show,
144					season,
145					poster_file_name,
146				}),
147			};
148
149			for episode_dir in episode_dirs_to_scan {
150				let Some(episode_dir_name) = episode_dir.file_name().and_then(OsStr::to_str) else {
151					yield Item {
152						path: path.to_owned(),
153						event: Err(Error::UnexpectedFolder),
154					};
155					continue;
156				};
157
158				let Some((_, s_e_str)) = episode_dir_name.split_once('S') else {
159					yield Item {
160						path: path.to_owned(),
161						event: Err(Error::UnexpectedFolder),
162					};
163					continue;
164				};
165				let Some((s_str, e_str)) = s_e_str.split_once('E') else {
166					yield Item {
167						path: path.to_owned(),
168						event: Err(Error::UnexpectedFolder),
169					};
170					continue;
171				};
172
173				let Ok(season_number) = s_str.parse::<SeasonNumber>() else {
174					yield Item {
175						path: path.to_owned(),
176						event: Err(Error::UnexpectedFolder),
177					};
178					continue;
179				};
180				if season_number != season {
181					yield Item {
182						path: path.to_owned(),
183						event: Err(Error::Inconsistent),
184					};
185					continue;
186				}
187
188				let Ok(episode_numbers) = e_str
189					.split('E')
190					.map(|s| s.parse::<EpisodeNumber>())
191					.collect::<Result<Vec<_>, _>>()
192				else {
193					yield Item {
194						path: path.to_owned(),
195						event: Err(Error::UnexpectedFolder),
196					};
197					continue;
198				};
199				let Ok(episode_numbers) = EpisodeNumbers::try_from(episode_numbers.as_ref()) else {
200					yield Item {
201						path: path.to_owned(),
202						event: Err(Error::UnexpectedFolder),
203					};
204					continue;
205				};
206
207				for await event in episode::Scanner::scan_episode(
208					&episode_dir,
209					show,
210					season_number,
211					episode_numbers,
212				) {
213					yield event.map(|e| e.into());
214				}
215			}
216		})
217	}
218}