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::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::{EpisodeScan, MediaRef, SeasonScan, ShowScan, 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(ShowScan),
25	/// A scanned season
26	Season(SeasonScan),
27	/// A scanned episode
28	Episode(EpisodeScan),
29}
30
31impl From<season::Scanner> for Scanner {
32	fn from(value: season::Scanner) -> Self {
33		match value {
34			season::Scanner::Season(s) => Self::Season(s),
35			season::Scanner::Episode(e) => Self::Episode(e),
36		}
37	}
38}
39
40impl Scanner {
41	/// Scan a folder for a show and its seasons/episodes
42	pub fn scan_show(
43		path: &Path,
44		parent_ref: Option<MediaRef<CollectionId>>,
45		id_ref: MediaRef<ShowId>,
46	) -> impl Stream<Item = Item> {
47		stream!({
48			let dirs = match fs::read_dir(path).await {
49				Ok(dirs) => dirs,
50				Err(err) => {
51					yield Item {
52						path: path.to_owned(),
53						event: Err(Error::ReadDir(err)),
54					};
55					return;
56				}
57			};
58
59			let mut poster_file_name = None;
60			let mut season_dirs_to_scan = Vec::new();
61
62			for await dir in ReadDirStream::new(dirs) {
63				match dir {
64					Ok(dir) => {
65						let path = dir.path();
66
67						let filetype = match dir.file_type().await {
68							Ok(filetype) => filetype,
69							Err(err) => {
70								yield Item {
71									path,
72									event: Err(Error::FileType(err)),
73								};
74								continue;
75							}
76						};
77
78						if filetype.is_dir() {
79							season_dirs_to_scan.push(path);
80							continue;
81						}
82
83						match path.extension().and_then(OsStr::to_str) {
84							is_image_extension!() => {
85								if poster_file_name.is_some() {
86									yield Item {
87										path,
88										event: Err(Error::DuplicatePosterFile),
89									};
90									continue;
91								}
92								poster_file_name = path
93									.file_name()
94									.and_then(|s| s.to_str())
95									.map(ToOwned::to_owned);
96							}
97							Some(_) | None => {
98								yield Item {
99									path,
100									event: Err(Error::UnexpectedFile),
101								};
102							}
103						}
104					}
105					Err(err) => {
106						yield Item {
107							path: path.to_owned(),
108							event: Err(Error::ReadDirEntry(err)),
109						}
110					}
111				}
112			}
113
114			yield Item {
115				path: path.to_owned(),
116				event: Ok(Self::Show(ShowScan {
117					parent_ref,
118					id_ref: id_ref.clone(),
119					poster_file_name,
120				})),
121			};
122
123			for season_dir in season_dirs_to_scan {
124				let Some(season_dir_name) = season_dir.file_name().and_then(OsStr::to_str) else {
125					yield Item {
126						path: season_dir,
127						event: Err(Error::UnexpectedFolder),
128					};
129					continue;
130				};
131
132				let Some(Ok(season_number)) = season_dir_name
133					.split_once('S')
134					.map(|(_, s)| s.parse::<SeasonNumber>())
135				else {
136					yield Item {
137						path: season_dir,
138						event: Err(Error::UnexpectedFolder),
139					};
140					continue;
141				};
142
143				for await event in
144					season::Scanner::scan_season(&season_dir, id_ref.clone(), season_number)
145				{
146					yield event.map(|e| e.into());
147				}
148			}
149		})
150	}
151}