flix_fs/scanner/
collection.rs

1//! The collection scanner will scan a folder and its children
2
3use 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
19/// A collection item
20pub type Item = crate::Item<Scanner>;
21
22/// The scanner for collections
23pub enum Scanner {
24	/// A scanned collection
25	Collection {
26		/// The ID of the parent collection (if any)
27		parent: Option<CollectionId>,
28		/// The ID of the collection
29		id: CollectionId,
30		/// The file name of the poster file
31		poster_file_name: Option<String>,
32	},
33
34	/// A scanned movie
35	Movie {
36		/// The ID of the parent collection (if any)
37		parent: Option<CollectionId>,
38		/// The ID of the movie
39		id: MovieId,
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	/// A scanned show
47	Show {
48		/// The ID of the parent collection (if any)
49		parent: Option<CollectionId>,
50		/// The ID of the show this episode belongs to
51		id: ShowId,
52		/// The file name of the poster file
53		poster_file_name: Option<String>,
54	},
55	/// A scanned episode
56	Season {
57		/// The ID of the show this episode belongs to
58		show: ShowId,
59		/// The season this episode belongs to
60		number: SeasonNumber,
61		/// The file name of the poster file
62		poster_file_name: Option<String>,
63	},
64	/// A scanned episode
65	Episode {
66		/// The ID of the show this episode belongs to
67		show: ShowId,
68		/// The season this episode belongs to
69		season: SeasonNumber,
70		/// The number(s) of this episode
71		number: EpisodeNumbers,
72		/// The file name of the media file
73		media_file_name: String,
74		/// The file name of the poster file
75		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	/// Scan a folder for a collection
195	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}