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
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 season belongs to
35		show: ShowId,
36		/// The season this episode belongs to
37		season: 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		episode: 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				season,
62				poster_file_name,
63			} => Self::Season {
64				show,
65				season,
66				poster_file_name,
67			},
68			season::Scanner::Episode {
69				show,
70				season,
71				episode,
72				media_file_name,
73				poster_file_name,
74			} => Self::Episode {
75				show,
76				season,
77				episode,
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 path = dir.path();
111
112						let filetype = match dir.file_type().await {
113							Ok(filetype) => filetype,
114							Err(err) => {
115								yield Item {
116									path,
117									event: Err(Error::FileType(err)),
118								};
119								continue;
120							}
121						};
122
123						if filetype.is_dir() {
124							season_dirs_to_scan.push(path);
125							continue;
126						}
127
128						match path.extension().and_then(OsStr::to_str) {
129							is_image_extension!() => {
130								if poster_file_name.is_some() {
131									yield Item {
132										path,
133										event: Err(Error::DuplicatePosterFile),
134									};
135									continue;
136								}
137								poster_file_name = path
138									.file_name()
139									.and_then(|s| s.to_str())
140									.map(ToOwned::to_owned);
141							}
142							Some(_) | None => {
143								yield Item {
144									path,
145									event: Err(Error::UnexpectedFile),
146								};
147							}
148						}
149					}
150					Err(err) => {
151						yield Item {
152							path: path.to_owned(),
153							event: Err(Error::ReadDirEntry(err)),
154						}
155					}
156				}
157			}
158
159			yield Item {
160				path: path.to_owned(),
161				event: Ok(Self::Show {
162					parent,
163					id,
164					poster_file_name,
165				}),
166			};
167
168			for season_dir in season_dirs_to_scan {
169				let Some(season_dir_name) = season_dir.file_name().and_then(OsStr::to_str) else {
170					yield Item {
171						path: season_dir,
172						event: Err(Error::UnexpectedFolder),
173					};
174					continue;
175				};
176
177				let Some(Ok(season_number)) = season_dir_name
178					.split_once('S')
179					.map(|(_, s)| s.parse::<SeasonNumber>())
180				else {
181					yield Item {
182						path: season_dir,
183						event: Err(Error::UnexpectedFolder),
184					};
185					continue;
186				};
187
188				for await event in season::Scanner::scan_season(&season_dir, id, season_number) {
189					yield event.map(|e| e.into());
190				}
191			}
192		})
193	}
194}