1use 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
18pub type Item = crate::Item<Scanner>;
20
21pub enum Scanner {
23 Show {
25 parent: Option<CollectionId>,
27 id: ShowId,
29 poster_file_name: Option<String>,
31 },
32 Season {
34 show: ShowId,
36 season: SeasonNumber,
38 poster_file_name: Option<String>,
40 },
41 Episode {
43 show: ShowId,
45 season: SeasonNumber,
47 episode: EpisodeNumbers,
49 media_file_name: String,
51 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 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}