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