flix_fs/scanner/
season.rs1use std::ffi::OsStr;
4use std::path::Path;
5
6use flix_model::id::ShowId;
7use flix_model::numbers::{EpisodeNumber, 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::episode;
17
18pub type Item = crate::Item<Scanner>;
20
21pub enum Scanner {
23 Season {
25 show: ShowId,
27 season: SeasonNumber,
29 poster_file_name: Option<String>,
31 },
32 Episode {
34 show: ShowId,
36 season: SeasonNumber,
38 episode: EpisodeNumbers,
40 media_file_name: String,
42 poster_file_name: Option<String>,
44 },
45}
46
47impl From<episode::Scanner> for Scanner {
48 fn from(value: episode::Scanner) -> Self {
49 match value {
50 episode::Scanner::Episode {
51 show,
52 season,
53 episode,
54 media_file_name,
55 poster_file_name,
56 } => Self::Episode {
57 show,
58 season,
59 episode,
60 media_file_name,
61 poster_file_name,
62 },
63 }
64 }
65}
66
67impl Scanner {
68 pub fn scan_season(
70 path: &Path,
71 show: ShowId,
72 season: SeasonNumber,
73 ) -> impl Stream<Item = Item> {
74 stream!({
75 let dirs = match fs::read_dir(path).await {
76 Ok(dirs) => dirs,
77 Err(err) => {
78 yield Item {
79 path: path.to_owned(),
80 event: Err(Error::ReadDir(err)),
81 };
82 return;
83 }
84 };
85
86 let mut poster_file_name = None;
87 let mut episode_dirs_to_scan = Vec::new();
88
89 for await dir in ReadDirStream::new(dirs) {
90 match dir {
91 Ok(dir) => {
92 let filetype = match dir.file_type().await {
93 Ok(filetype) => filetype,
94 Err(err) => {
95 yield Item {
96 path: path.to_owned(),
97 event: Err(Error::FileType(err)),
98 };
99 continue;
100 }
101 };
102
103 let path = dir.path();
104 if filetype.is_dir() {
105 episode_dirs_to_scan.push(path);
106 continue;
107 }
108
109 match path.extension().and_then(OsStr::to_str) {
110 is_image_extension!() => {
111 if poster_file_name.is_some() {
112 yield Item {
113 path: path.to_owned(),
114 event: Err(Error::DuplicatePosterFile),
115 };
116 continue;
117 }
118 poster_file_name = path
119 .file_name()
120 .and_then(|s| s.to_str())
121 .map(ToOwned::to_owned);
122 }
123 Some(_) | None => {
124 yield Item {
125 path: path.to_owned(),
126 event: Err(Error::UnexpectedFile),
127 };
128 }
129 }
130 }
131 Err(err) => {
132 yield Item {
133 path: path.to_owned(),
134 event: Err(Error::ReadDirEntry(err)),
135 }
136 }
137 }
138 }
139
140 yield Item {
141 path: path.to_owned(),
142 event: Ok(Self::Season {
143 show,
144 season,
145 poster_file_name,
146 }),
147 };
148
149 for episode_dir in episode_dirs_to_scan {
150 let Some(episode_dir_name) = episode_dir.file_name().and_then(OsStr::to_str) else {
151 yield Item {
152 path: path.to_owned(),
153 event: Err(Error::UnexpectedFolder),
154 };
155 continue;
156 };
157
158 let Some((_, s_e_str)) = episode_dir_name.split_once('S') else {
159 yield Item {
160 path: path.to_owned(),
161 event: Err(Error::UnexpectedFolder),
162 };
163 continue;
164 };
165 let Some((s_str, e_str)) = s_e_str.split_once('E') else {
166 yield Item {
167 path: path.to_owned(),
168 event: Err(Error::UnexpectedFolder),
169 };
170 continue;
171 };
172
173 let Ok(season_number) = s_str.parse::<SeasonNumber>() else {
174 yield Item {
175 path: path.to_owned(),
176 event: Err(Error::UnexpectedFolder),
177 };
178 continue;
179 };
180 if season_number != season {
181 yield Item {
182 path: path.to_owned(),
183 event: Err(Error::Inconsistent),
184 };
185 continue;
186 }
187
188 let Ok(episode_numbers) = e_str
189 .split('E')
190 .map(|s| s.parse::<EpisodeNumber>())
191 .collect::<Result<Vec<_>, _>>()
192 else {
193 yield Item {
194 path: path.to_owned(),
195 event: Err(Error::UnexpectedFolder),
196 };
197 continue;
198 };
199 let Ok(episode_numbers) = EpisodeNumbers::try_from(episode_numbers.as_ref()) else {
200 yield Item {
201 path: path.to_owned(),
202 event: Err(Error::UnexpectedFolder),
203 };
204 continue;
205 };
206
207 for await event in episode::Scanner::scan_episode(
208 &episode_dir,
209 show,
210 season_number,
211 episode_numbers,
212 ) {
213 yield event.map(|e| e.into());
214 }
215 }
216 })
217 }
218}