flix_fs/scanner/
collection.rs

1//! The collection scanner will scan a folder and its children
2
3use 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
20/// A collection item
21pub type Item = crate::Item<Scanner>;
22
23/// The scanner for collections
24pub enum Scanner {
25	/// A scanned collection
26	Collection(CollectionScan),
27	/// A scanned movie
28	Movie(MovieScan),
29	/// A scanned show
30	Show(ShowScan),
31	/// A scanned episode
32	Season(SeasonScan),
33	/// A scanned episode
34	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	/// Scan a folder for a collection
69	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}