1use 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
18pub type Item = crate::Item<Scanner>;
20
21pub enum Scanner {
23 Show(ShowScan),
25 Season(SeasonScan),
27 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 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}