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 path = dir.path();
93
94						let filetype = match dir.file_type().await {
95							Ok(filetype) => filetype,
96							Err(err) => {
97								yield Item {
98									path,
99									event: Err(Error::FileType(err)),
100								};
101								continue;
102							}
103						};
104
105						if filetype.is_dir() {
106							episode_dirs_to_scan.push(path);
107							continue;
108						}
109
110						match path.extension().and_then(OsStr::to_str) {
111							is_image_extension!() => {
112								if poster_file_name.is_some() {
113									yield Item {
114										path,
115										event: Err(Error::DuplicatePosterFile),
116									};
117									continue;
118								}
119								poster_file_name = path
120									.file_name()
121									.and_then(|s| s.to_str())
122									.map(ToOwned::to_owned);
123							}
124							Some(_) | None => {
125								yield Item {
126									path,
127									event: Err(Error::UnexpectedFile),
128								};
129							}
130						}
131					}
132					Err(err) => {
133						yield Item {
134							path: path.to_owned(),
135							event: Err(Error::ReadDirEntry(err)),
136						}
137					}
138				}
139			}
140
141			yield Item {
142				path: path.to_owned(),
143				event: Ok(Self::Season {
144					show,
145					season,
146					poster_file_name,
147				}),
148			};
149
150			for episode_dir in episode_dirs_to_scan {
151				let Some(episode_dir_name) = episode_dir.file_name().and_then(OsStr::to_str) else {
152					yield Item {
153						path: episode_dir,
154						event: Err(Error::UnexpectedFolder),
155					};
156					continue;
157				};
158
159				let Some((_, s_e_str)) = episode_dir_name.split_once('S') else {
160					yield Item {
161						path: episode_dir,
162						event: Err(Error::UnexpectedFolder),
163					};
164					continue;
165				};
166				let Some((s_str, e_str)) = s_e_str.split_once('E') else {
167					yield Item {
168						path: episode_dir,
169						event: Err(Error::UnexpectedFolder),
170					};
171					continue;
172				};
173
174				let Ok(season_number) = s_str.parse::<SeasonNumber>() else {
175					yield Item {
176						path: episode_dir,
177						event: Err(Error::UnexpectedFolder),
178					};
179					continue;
180				};
181				if season_number != season {
182					yield Item {
183						path: episode_dir,
184						event: Err(Error::Inconsistent),
185					};
186					continue;
187				}
188
189				let Ok(episode_numbers) = e_str
190					.split('E')
191					.map(|s| s.parse::<EpisodeNumber>())
192					.collect::<Result<Vec<_>, _>>()
193				else {
194					yield Item {
195						path: episode_dir,
196						event: Err(Error::UnexpectedFolder),
197					};
198					continue;
199				};
200				let Ok(episode_numbers) = EpisodeNumbers::try_from(episode_numbers.as_ref()) else {
201					yield Item {
202						path: episode_dir,
203						event: Err(Error::UnexpectedFolder),
204					};
205					continue;
206				};
207
208				for await event in episode::Scanner::scan_episode(
209					&episode_dir,
210					show,
211					season_number,
212					episode_numbers,
213				) {
214					yield event.map(|e| e.into());
215				}
216			}
217		})
218	}
219}