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::{EpisodeScan, MediaRef, SeasonScan, episode};
17
18/// A season item
19pub type Item = crate::Item<Scanner>;
20
21/// The scanner for seasons
22pub enum Scanner {
23	/// A scanned season
24	Season(SeasonScan),
25	/// A scanned episode
26	Episode(EpisodeScan),
27}
28
29impl From<episode::Scanner> for Scanner {
30	fn from(value: episode::Scanner) -> Self {
31		match value {
32			episode::Scanner::Episode(e) => Self::Episode(e),
33		}
34	}
35}
36
37impl Scanner {
38	/// Scan a folder for a season and its episodes
39	pub fn scan_season(
40		path: &Path,
41		show_ref: MediaRef<ShowId>,
42		season: SeasonNumber,
43	) -> impl Stream<Item = Item> {
44		stream!({
45			let dirs = match fs::read_dir(path).await {
46				Ok(dirs) => dirs,
47				Err(err) => {
48					yield Item {
49						path: path.to_owned(),
50						event: Err(Error::ReadDir(err)),
51					};
52					return;
53				}
54			};
55
56			let mut poster_file_name = None;
57			let mut episode_dirs_to_scan = Vec::new();
58
59			for await dir in ReadDirStream::new(dirs) {
60				match dir {
61					Ok(dir) => {
62						let path = dir.path();
63
64						let filetype = match dir.file_type().await {
65							Ok(filetype) => filetype,
66							Err(err) => {
67								yield Item {
68									path,
69									event: Err(Error::FileType(err)),
70								};
71								continue;
72							}
73						};
74
75						if filetype.is_dir() {
76							episode_dirs_to_scan.push(path);
77							continue;
78						}
79
80						match path.extension().and_then(OsStr::to_str) {
81							is_image_extension!() => {
82								if poster_file_name.is_some() {
83									yield Item {
84										path,
85										event: Err(Error::DuplicatePosterFile),
86									};
87									continue;
88								}
89								poster_file_name = path
90									.file_name()
91									.and_then(|s| s.to_str())
92									.map(ToOwned::to_owned);
93							}
94							Some(_) | None => {
95								yield Item {
96									path,
97									event: Err(Error::UnexpectedFile),
98								};
99							}
100						}
101					}
102					Err(err) => {
103						yield Item {
104							path: path.to_owned(),
105							event: Err(Error::ReadDirEntry(err)),
106						}
107					}
108				}
109			}
110
111			yield Item {
112				path: path.to_owned(),
113				event: Ok(Self::Season(SeasonScan {
114					show_ref: show_ref.clone(),
115					season,
116					poster_file_name,
117				})),
118			};
119
120			for episode_dir in episode_dirs_to_scan {
121				let Some(episode_dir_name) = episode_dir.file_name().and_then(OsStr::to_str) else {
122					yield Item {
123						path: episode_dir,
124						event: Err(Error::UnexpectedFolder),
125					};
126					continue;
127				};
128
129				let Some((_, s_e_str)) = episode_dir_name.split_once('S') else {
130					yield Item {
131						path: episode_dir,
132						event: Err(Error::UnexpectedFolder),
133					};
134					continue;
135				};
136				let Some((s_str, e_str)) = s_e_str.split_once('E') else {
137					yield Item {
138						path: episode_dir,
139						event: Err(Error::UnexpectedFolder),
140					};
141					continue;
142				};
143
144				let Ok(season_number) = s_str.parse::<SeasonNumber>() else {
145					yield Item {
146						path: episode_dir,
147						event: Err(Error::UnexpectedFolder),
148					};
149					continue;
150				};
151				if season_number != season {
152					yield Item {
153						path: episode_dir,
154						event: Err(Error::Inconsistent),
155					};
156					continue;
157				}
158
159				let Ok(episode_numbers) = e_str
160					.split('E')
161					.map(|s| s.parse::<EpisodeNumber>())
162					.collect::<Result<Vec<_>, _>>()
163				else {
164					yield Item {
165						path: episode_dir,
166						event: Err(Error::UnexpectedFolder),
167					};
168					continue;
169				};
170				let Ok(episode_numbers) = EpisodeNumbers::try_from(episode_numbers.as_ref()) else {
171					yield Item {
172						path: episode_dir,
173						event: Err(Error::UnexpectedFolder),
174					};
175					continue;
176				};
177
178				for await event in episode::Scanner::scan_episode(
179					&episode_dir,
180					show_ref.clone(),
181					season_number,
182					episode_numbers,
183				) {
184					yield event.map(|e| e.into());
185				}
186			}
187		})
188	}
189}