use std::ffi::OsStr;
use std::path::Path;
use std::sync::OnceLock;
use flix_model::id::{CollectionId, MovieId, RawId, ShowId};
use flix_model::numbers::{EpisodeNumbers, SeasonNumber};
use async_stream::stream;
use regex::Regex;
use tokio::fs;
use tokio_stream::Stream;
use tokio_stream::wrappers::ReadDirStream;
use crate::Error;
use crate::scanner::{collection, movie, show};
static MEDIA_FOLDER_REGEX: OnceLock<Regex> = OnceLock::new();
static SEASON_FOLDER_REGEX: OnceLock<Regex> = OnceLock::new();
pub type Item = crate::Item<Scanner>;
pub enum Scanner {
Collection {
parent: Option<CollectionId>,
id: CollectionId,
poster_file_name: Option<String>,
},
Movie {
parent: Option<CollectionId>,
id: MovieId,
media_file_name: String,
poster_file_name: Option<String>,
},
Show {
parent: Option<CollectionId>,
id: ShowId,
poster_file_name: Option<String>,
},
Season {
show: ShowId,
season: SeasonNumber,
poster_file_name: Option<String>,
},
Episode {
show: ShowId,
season: SeasonNumber,
episode: EpisodeNumbers,
media_file_name: String,
poster_file_name: Option<String>,
},
}
impl From<collection::Scanner> for Scanner {
fn from(value: collection::Scanner) -> Self {
match value {
collection::Scanner::Collection {
parent,
id,
poster_file_name,
} => Self::Collection {
parent,
id,
poster_file_name,
},
collection::Scanner::Movie {
parent,
id,
media_file_name,
poster_file_name,
} => Self::Movie {
parent,
id,
media_file_name,
poster_file_name,
},
collection::Scanner::Show {
parent,
id,
poster_file_name,
} => Self::Show {
parent,
id,
poster_file_name,
},
collection::Scanner::Season {
show,
season,
poster_file_name,
} => Self::Season {
show,
season,
poster_file_name,
},
collection::Scanner::Episode {
show,
season,
episode,
media_file_name,
poster_file_name,
} => Self::Episode {
show,
season,
episode,
media_file_name,
poster_file_name,
},
}
}
}
impl From<movie::Scanner> for Scanner {
fn from(value: movie::Scanner) -> Self {
match value {
movie::Scanner::Movie {
parent,
id,
media_file_name,
poster_file_name,
} => Self::Movie {
parent,
id,
media_file_name,
poster_file_name,
},
}
}
}
impl From<show::Scanner> for Scanner {
fn from(value: show::Scanner) -> Self {
match value {
show::Scanner::Show {
parent,
id,
poster_file_name,
} => Self::Show {
parent,
id,
poster_file_name,
},
show::Scanner::Season {
show,
season,
poster_file_name,
} => Self::Season {
show,
season,
poster_file_name,
},
show::Scanner::Episode {
show,
season,
episode,
media_file_name,
poster_file_name,
} => Self::Episode {
show,
season,
episode,
media_file_name,
poster_file_name,
},
}
}
}
impl Scanner {
pub fn scan_detect_folder(
path: &Path,
parent: Option<CollectionId>,
) -> impl Stream<Item = Item> {
enum MediaType {
Collection,
Movie,
Show,
}
let media_folder_re = MEDIA_FOLDER_REGEX.get_or_init(|| {
Regex::new(r"^[[[:alnum:]] -]+ \([[:digit:]]+\) \[[[:digit:]]+\]$")
.unwrap_or_else(|err| panic!("regex is invalid: {err}"))
});
let season_folder_re = SEASON_FOLDER_REGEX.get_or_init(|| {
Regex::new(r"^S[[:digit:]]+$").unwrap_or_else(|err| panic!("regex is invalid: {err}"))
});
stream!({
let Some(dir_name) = path.file_name().and_then(OsStr::to_str) else {
yield Item {
path: path.to_owned(),
event: Err(Error::UnexpectedFolder),
};
return;
};
let Some(Ok(id)) = dir_name
.split_once('[')
.and_then(|(_, s)| s.split_once(']'))
.map(|(s, _)| s.parse::<RawId>())
else {
yield Item {
path: path.to_owned(),
event: Err(Error::UnexpectedFolder),
};
return;
};
let media_type: MediaType;
if media_folder_re.is_match(dir_name) {
let dirs = match fs::read_dir(path).await {
Ok(dirs) => dirs,
Err(err) => {
yield Item {
path: path.to_owned(),
event: Err(Error::ReadDir(err)),
};
return;
}
};
let mut is_show = false;
for await dir in ReadDirStream::new(dirs) {
match dir {
Ok(dir) => {
let filetype = match dir.file_type().await {
Ok(filetype) => filetype,
Err(err) => {
yield Item {
path: path.to_owned(),
event: Err(Error::FileType(err)),
};
continue;
}
};
if !filetype.is_dir() {
continue;
}
let dir_path = dir.path();
let Some(folder_name) = dir_path.file_name().and_then(OsStr::to_str)
else {
yield Item {
path: path.to_owned(),
event: Err(Error::UnexpectedFolder),
};
continue;
};
if season_folder_re.is_match(folder_name) {
is_show = true;
break;
}
}
Err(err) => {
yield Item {
path: path.to_owned(),
event: Err(Error::ReadDirEntry(err)),
};
}
}
}
if is_show {
media_type = MediaType::Show;
} else {
media_type = MediaType::Movie;
}
} else {
media_type = MediaType::Collection;
}
match media_type {
MediaType::Collection => {
for await event in collection::Scanner::scan_collection(
path,
parent,
CollectionId::from_raw(id),
) {
yield event.map(|e| e.into());
}
}
MediaType::Movie => {
for await event in
movie::Scanner::scan_movie(path, parent, MovieId::from_raw(id))
{
yield event.map(|e| e.into());
}
}
MediaType::Show => {
for await event in show::Scanner::scan_show(path, parent, ShowId::from_raw(id))
{
yield event.map(|e| e.into());
}
}
}
})
}
}