1use core::pin::Pin;
4use std::ffi::OsStr;
5use std::path::Path;
6
7use flix_model::id::{CollectionId, MovieId, ShowId};
8use flix_model::numbers::{EpisodeNumbers, SeasonNumber};
9
10use async_stream::stream;
11use tokio::fs;
12use tokio_stream::Stream;
13use tokio_stream::wrappers::ReadDirStream;
14
15use crate::Error;
16use crate::macros::is_image_extension;
17use crate::scanner::{generic, movie, show};
18
19pub type Item = crate::Item<Scanner>;
21
22pub enum Scanner {
24 Collection {
26 parent: Option<CollectionId>,
28 id: CollectionId,
30 poster_file_name: Option<String>,
32 },
33
34 Movie {
36 parent: Option<CollectionId>,
38 id: MovieId,
40 media_file_name: String,
42 poster_file_name: Option<String>,
44 },
45
46 Show {
48 parent: Option<CollectionId>,
50 id: ShowId,
52 poster_file_name: Option<String>,
54 },
55 Season {
57 show: ShowId,
59 number: SeasonNumber,
61 poster_file_name: Option<String>,
63 },
64 Episode {
66 show: ShowId,
68 season: SeasonNumber,
70 number: EpisodeNumbers,
72 media_file_name: String,
74 poster_file_name: Option<String>,
76 },
77}
78
79impl From<movie::Scanner> for Scanner {
80 fn from(value: movie::Scanner) -> Self {
81 match value {
82 movie::Scanner::Movie {
83 parent,
84 id,
85 media_file_name,
86 poster_file_name,
87 } => Self::Movie {
88 parent,
89 id,
90 media_file_name,
91 poster_file_name,
92 },
93 }
94 }
95}
96
97impl From<show::Scanner> for Scanner {
98 fn from(value: show::Scanner) -> Self {
99 match value {
100 show::Scanner::Show {
101 parent,
102 id,
103 poster_file_name,
104 } => Self::Show {
105 parent,
106 id,
107 poster_file_name,
108 },
109 show::Scanner::Season {
110 show,
111 number,
112 poster_file_name,
113 } => Self::Season {
114 show,
115 number,
116 poster_file_name,
117 },
118 show::Scanner::Episode {
119 show,
120 season,
121 number,
122 media_file_name,
123 poster_file_name,
124 } => Self::Episode {
125 show,
126 season,
127 number,
128 media_file_name,
129 poster_file_name,
130 },
131 }
132 }
133}
134
135impl From<generic::Scanner> for Scanner {
136 fn from(value: generic::Scanner) -> Self {
137 match value {
138 generic::Scanner::Collection {
139 parent,
140 id,
141 poster_file_name,
142 } => Self::Collection {
143 parent,
144 id,
145 poster_file_name,
146 },
147 generic::Scanner::Movie {
148 parent,
149 id,
150 media_file_name,
151 poster_file_name,
152 } => Self::Movie {
153 parent,
154 id,
155 media_file_name,
156 poster_file_name,
157 },
158 generic::Scanner::Show {
159 parent,
160 id,
161 poster_file_name,
162 } => Self::Show {
163 parent,
164 id,
165 poster_file_name,
166 },
167 generic::Scanner::Season {
168 show,
169 number,
170 poster_file_name,
171 } => Self::Season {
172 show,
173 number,
174 poster_file_name,
175 },
176 generic::Scanner::Episode {
177 show,
178 season,
179 number,
180 media_file_name,
181 poster_file_name,
182 } => Self::Episode {
183 show,
184 season,
185 number,
186 media_file_name,
187 poster_file_name,
188 },
189 }
190 }
191}
192
193impl Scanner {
194 pub fn scan_collection(
196 path: &Path,
197 parent: Option<CollectionId>,
198 id: CollectionId,
199 ) -> Pin<Box<impl Stream<Item = Item>>> {
200 Box::pin(stream!({
201 let dirs = match fs::read_dir(path).await {
202 Ok(dirs) => dirs,
203 Err(err) => {
204 yield Item {
205 path: path.to_owned(),
206 event: Err(Error::ReadDir(err)),
207 };
208 return;
209 }
210 };
211
212 let mut poster_file_name = None;
213 let mut subdirs_to_scan = Vec::new();
214
215 for await dir in ReadDirStream::new(dirs) {
216 match dir {
217 Ok(dir) => {
218 let filetype = match dir.file_type().await {
219 Ok(filetype) => filetype,
220 Err(err) => {
221 yield Item {
222 path: path.to_owned(),
223 event: Err(Error::FileType(err)),
224 };
225 continue;
226 }
227 };
228
229 let path = dir.path();
230 if filetype.is_dir() {
231 subdirs_to_scan.push(path);
232 continue;
233 }
234
235 match path.extension().and_then(OsStr::to_str) {
236 is_image_extension!() => {
237 if poster_file_name.is_some() {
238 yield Item {
239 path: path.to_owned(),
240 event: Err(Error::DuplicatePosterFile),
241 };
242 continue;
243 }
244 poster_file_name = path
245 .file_name()
246 .and_then(|s| s.to_str())
247 .map(ToOwned::to_owned);
248 }
249 Some(_) | None => {
250 yield Item {
251 path: path.to_owned(),
252 event: Err(Error::UnexpectedFile),
253 };
254 }
255 }
256 }
257 Err(err) => {
258 yield Item {
259 path: path.to_owned(),
260 event: Err(Error::ReadDirEntry(err)),
261 }
262 }
263 }
264 }
265
266 yield Item {
267 path: path.to_owned(),
268 event: Ok(Self::Collection {
269 parent,
270 id,
271 poster_file_name,
272 }),
273 };
274
275 for subdir in subdirs_to_scan {
276 for await event in generic::Scanner::scan_detect_folder(&subdir, Some(id)) {
277 yield event.map(|e| e.into());
278 }
279 }
280 }))
281 }
282}