flix_fs/scanner/
collection.rs1use core::pin::Pin;
4use std::ffi::OsStr;
5use std::path::Path;
6
7use flix_model::id::CollectionId;
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::{
17 CollectionScan, EpisodeScan, MediaRef, MovieScan, SeasonScan, ShowScan, generic, movie, show,
18};
19
20pub type Item = crate::Item<Scanner>;
22
23pub enum Scanner {
25 Collection(CollectionScan),
27 Movie(MovieScan),
29 Show(ShowScan),
31 Season(SeasonScan),
33 Episode(EpisodeScan),
35}
36
37impl From<movie::Scanner> for Scanner {
38 fn from(value: movie::Scanner) -> Self {
39 match value {
40 movie::Scanner::Movie(m) => Self::Movie(m),
41 }
42 }
43}
44
45impl From<show::Scanner> for Scanner {
46 fn from(value: show::Scanner) -> Self {
47 match value {
48 show::Scanner::Show(s) => Self::Show(s),
49 show::Scanner::Season(s) => Self::Season(s),
50 show::Scanner::Episode(e) => Self::Episode(e),
51 }
52 }
53}
54
55impl From<generic::Scanner> for Scanner {
56 fn from(value: generic::Scanner) -> Self {
57 match value {
58 generic::Scanner::Collection(c) => Self::Collection(c),
59 generic::Scanner::Movie(m) => Self::Movie(m),
60 generic::Scanner::Show(s) => Self::Show(s),
61 generic::Scanner::Season(s) => Self::Season(s),
62 generic::Scanner::Episode(e) => Self::Episode(e),
63 }
64 }
65}
66
67impl Scanner {
68 pub fn scan_collection(
70 path: &Path,
71 parent_ref: Option<MediaRef<CollectionId>>,
72 id_ref: MediaRef<CollectionId>,
73 ) -> Pin<Box<impl Stream<Item = Item>>> {
74 Box::pin(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 subdirs_to_scan = Vec::new();
88
89 for await dir in ReadDirStream::new(dirs) {
90 match dir {
91 Ok(dir) => {
92 let path = dir.path();
93
94 let filetype = match dir.file_type().await {
95 Ok(filetype) => filetype,
96 Err(err) => {
97 yield Item {
98 path,
99 event: Err(Error::FileType(err)),
100 };
101 continue;
102 }
103 };
104
105 if filetype.is_dir() {
106 subdirs_to_scan.push(path);
107 continue;
108 }
109
110 match path.extension().and_then(OsStr::to_str) {
111 is_image_extension!() => {
112 if poster_file_name.is_some() {
113 yield Item {
114 path,
115 event: Err(Error::DuplicatePosterFile),
116 };
117 continue;
118 }
119 poster_file_name = path
120 .file_name()
121 .and_then(|s| s.to_str())
122 .map(ToOwned::to_owned);
123 }
124 Some(_) | None => {
125 yield Item {
126 path,
127 event: Err(Error::UnexpectedFile),
128 };
129 }
130 }
131 }
132 Err(err) => {
133 yield Item {
134 path: path.to_owned(),
135 event: Err(Error::ReadDirEntry(err)),
136 }
137 }
138 }
139 }
140
141 yield Item {
142 path: path.to_owned(),
143 event: Ok(Self::Collection(CollectionScan {
144 parent_ref,
145 id_ref: id_ref.clone(),
146 poster_file_name,
147 })),
148 };
149
150 for subdir in subdirs_to_scan {
151 for await event in
152 generic::Scanner::scan_detect_folder(&subdir, Some(id_ref.clone()))
153 {
154 yield event.map(|e| e.into());
155 }
156 }
157 }))
158 }
159}