flix_fs/scanner/
episode.rs

1//! The episode scanner will scan a folder and exit
2
3use std::ffi::OsStr;
4use std::path::Path;
5
6use flix_model::id::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, is_media_extension};
16use crate::scanner::{EpisodeScan, MediaRef};
17
18/// An episode item
19pub type Item = crate::Item<Scanner>;
20
21/// The scanner for epispdes
22pub enum Scanner {
23	/// A scanned episode
24	Episode(EpisodeScan),
25}
26
27impl Scanner {
28	/// Scan a folder for an episode
29	pub fn scan_episode(
30		path: &Path,
31		show_ref: MediaRef<ShowId>,
32		season: SeasonNumber,
33		episode: EpisodeNumbers,
34	) -> impl Stream<Item = Item> {
35		stream!({
36			let dirs = match fs::read_dir(path).await {
37				Ok(dirs) => dirs,
38				Err(err) => {
39					yield Item {
40						path: path.to_owned(),
41						event: Err(Error::ReadDir(err)),
42					};
43					return;
44				}
45			};
46
47			let mut media_file_name = None;
48			let mut poster_file_name = None;
49
50			for await dir in ReadDirStream::new(dirs) {
51				match dir {
52					Ok(dir) => {
53						let path = dir.path();
54
55						let filetype = match dir.file_type().await {
56							Ok(filetype) => filetype,
57							Err(err) => {
58								yield Item {
59									path,
60									event: Err(Error::FileType(err)),
61								};
62								continue;
63							}
64						};
65						if !filetype.is_file() {
66							yield Item {
67								path,
68								event: Err(Error::UnexpectedNonFile),
69							};
70							continue;
71						}
72
73						match path.extension().and_then(OsStr::to_str) {
74							is_media_extension!() => {
75								if media_file_name.is_some() {
76									yield Item {
77										path,
78										event: Err(Error::DuplicateMediaFile),
79									};
80									continue;
81								}
82								media_file_name = path
83									.file_name()
84									.and_then(|s| s.to_str())
85									.map(ToOwned::to_owned);
86								continue;
87							}
88							is_image_extension!() => {
89								if poster_file_name.is_some() {
90									yield Item {
91										path,
92										event: Err(Error::DuplicatePosterFile),
93									};
94									continue;
95								}
96								poster_file_name = path
97									.file_name()
98									.and_then(|s| s.to_str())
99									.map(ToOwned::to_owned);
100							}
101							Some(_) | None => {
102								yield Item {
103									path,
104									event: Err(Error::UnexpectedFile),
105								};
106							}
107						}
108					}
109					Err(err) => {
110						yield Item {
111							path: path.to_owned(),
112							event: Err(Error::ReadDirEntry(err)),
113						}
114					}
115				}
116			}
117
118			let Some(media_file_name) = media_file_name else {
119				yield Item {
120					path: path.to_owned(),
121					event: Err(Error::Incomplete),
122				};
123				return;
124			};
125
126			yield Item {
127				path: path.to_owned(),
128				event: Ok(Self::Episode(EpisodeScan {
129					show_ref,
130					season,
131					episode,
132					media_file_name,
133					poster_file_name,
134				})),
135			};
136		})
137	}
138}