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