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 filetype = match dir.file_type().await {
64							Ok(filetype) => filetype,
65							Err(err) => {
66								yield Item {
67									path: path.to_owned(),
68									event: Err(Error::FileType(err)),
69								};
70								continue;
71							}
72						};
73						if !filetype.is_file() {
74							yield Item {
75								path: path.to_owned(),
76								event: Err(Error::UnexpectedNonFile),
77							};
78							continue;
79						}
80
81						let path = dir.path();
82						match path.extension().and_then(OsStr::to_str) {
83							is_media_extension!() => {
84								if media_file_name.is_some() {
85									yield Item {
86										path: path.to_owned(),
87										event: Err(Error::DuplicateMediaFile),
88									};
89									continue;
90								}
91								media_file_name = path
92									.file_name()
93									.and_then(|s| s.to_str())
94									.map(ToOwned::to_owned);
95								continue;
96							}
97							is_image_extension!() => {
98								if poster_file_name.is_some() {
99									yield Item {
100										path: path.to_owned(),
101										event: Err(Error::DuplicatePosterFile),
102									};
103									continue;
104								}
105								poster_file_name = path
106									.file_name()
107									.and_then(|s| s.to_str())
108									.map(ToOwned::to_owned);
109							}
110							Some(_) | None => {
111								yield Item {
112									path: path.to_owned(),
113									event: Err(Error::UnexpectedFile),
114								};
115							}
116						}
117					}
118					Err(err) => {
119						yield Item {
120							path: path.to_owned(),
121							event: Err(Error::ReadDirEntry(err)),
122						}
123					}
124				}
125			}
126
127			let Some(media_file_name) = media_file_name else {
128				yield Item {
129					path: path.to_owned(),
130					event: Err(Error::Incomplete),
131				};
132				return;
133			};
134
135			yield Item {
136				path: path.to_owned(),
137				event: Ok(Self::Episode {
138					show,
139					season,
140					episode,
141					media_file_name,
142					poster_file_name,
143				}),
144			};
145		})
146	}
147}