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
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 season belongs to
58		show: ShowId,
59		/// The number of this season
60		season: 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		episode: 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				season,
112				poster_file_name,
113			} => Self::Season {
114				show,
115				season,
116				poster_file_name,
117			},
118			show::Scanner::Episode {
119				show,
120				season,
121				episode,
122				media_file_name,
123				poster_file_name,
124			} => Self::Episode {
125				show,
126				season,
127				episode,
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				season,
170				poster_file_name,
171			} => Self::Season {
172				show,
173				season,
174				poster_file_name,
175			},
176			generic::Scanner::Episode {
177				show,
178				season,
179				episode,
180				media_file_name,
181				poster_file_name,
182			} => Self::Episode {
183				show,
184				season,
185				episode,
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 path = dir.path();
219
220						let filetype = match dir.file_type().await {
221							Ok(filetype) => filetype,
222							Err(err) => {
223								yield Item {
224									path,
225									event: Err(Error::FileType(err)),
226								};
227								continue;
228							}
229						};
230
231						if filetype.is_dir() {
232							subdirs_to_scan.push(path);
233							continue;
234						}
235
236						match path.extension().and_then(OsStr::to_str) {
237							is_image_extension!() => {
238								if poster_file_name.is_some() {
239									yield Item {
240										path,
241										event: Err(Error::DuplicatePosterFile),
242									};
243									continue;
244								}
245								poster_file_name = path
246									.file_name()
247									.and_then(|s| s.to_str())
248									.map(ToOwned::to_owned);
249							}
250							Some(_) | None => {
251								yield Item {
252									path,
253									event: Err(Error::UnexpectedFile),
254								};
255							}
256						}
257					}
258					Err(err) => {
259						yield Item {
260							path: path.to_owned(),
261							event: Err(Error::ReadDirEntry(err)),
262						}
263					}
264				}
265			}
266
267			yield Item {
268				path: path.to_owned(),
269				event: Ok(Self::Collection {
270					parent,
271					id,
272					poster_file_name,
273				}),
274			};
275
276			for subdir in subdirs_to_scan {
277				for await event in generic::Scanner::scan_detect_folder(&subdir, Some(id)) {
278					yield event.map(|e| e.into());
279				}
280			}
281		}))
282	}
283}