flix_fs/scanner/
show.rs

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