flix_fs/scanner/
season.rs1use 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
18pub type Item = crate::Item<Scanner>;
20
21pub enum Scanner {
23 Season {
25 show: ShowId,
27 season: SeasonNumber,
29 poster_file_name: Option<String>,
31 },
32 Episode {
34 show: ShowId,
36 season: SeasonNumber,
38 episode: EpisodeNumbers,
40 media_file_name: String,
42 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 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}