156 lines
5.1 KiB
Rust
156 lines
5.1 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use anyhow::{anyhow, Context};
|
|
use log::{debug, error};
|
|
use regex::Regex;
|
|
use serde::Deserialize;
|
|
use serde_with::{serde_as, DisplayFromStr, KeyValueMap};
|
|
|
|
mod enumeration;
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct Show {
|
|
#[serde(rename = "$key$")]
|
|
pub title: String,
|
|
pub path: PathBuf,
|
|
#[serde(default)]
|
|
pub custom_episodes: Option<CustomEpisodes>,
|
|
#[serde(default)]
|
|
pub tags: Vec<String>,
|
|
#[serde(default)]
|
|
pub parts: HashMap<u32, String>,
|
|
#[serde(default = "default_weight")]
|
|
pub weight: f32,
|
|
#[serde(default, rename = "18+")]
|
|
pub eighteen_plus: Option<bool>,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Deserialize)]
|
|
pub struct CustomEpisodes {
|
|
#[serde(default)]
|
|
pub prefix: String,
|
|
#[serde_as(as = "DisplayFromStr")]
|
|
pub regex: Regex,
|
|
}
|
|
|
|
fn default_weight() -> f32 {
|
|
1.0
|
|
}
|
|
|
|
pub type Shows = Vec<Show>;
|
|
|
|
#[serde_as]
|
|
#[derive(Deserialize)]
|
|
struct ShowsWrapper(#[serde_as(as = "KeyValueMap<_>")] Shows);
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub enum EpisodeNumber {
|
|
Standalone,
|
|
SingleSeason(u32),
|
|
MultiSeason(u32, u32),
|
|
Custom(String),
|
|
}
|
|
|
|
type Episodes = HashMap<EpisodeNumber, PathBuf>;
|
|
|
|
pub fn load<P: AsRef<Path>>(shows_file: P) -> anyhow::Result<Shows> {
|
|
Ok(serde_yaml::from_reader::<_, ShowsWrapper>(
|
|
fs::File::open(shows_file).context("Failed to open shows file")?,
|
|
)
|
|
.context("Failed to parse YAML from shows file")?
|
|
.0)
|
|
}
|
|
|
|
pub fn display_show_episode(show: &Show, episode: &EpisodeNumber) -> String {
|
|
match episode {
|
|
EpisodeNumber::Standalone => show.title.to_string(),
|
|
EpisodeNumber::SingleSeason(n) => format!("{} episode {}", show.title, n),
|
|
EpisodeNumber::MultiSeason(season, ep) => format!(
|
|
"{} {} episode {}",
|
|
show.title,
|
|
show.parts
|
|
.get(season)
|
|
.unwrap_or(&format!("season {}", season)),
|
|
ep
|
|
),
|
|
EpisodeNumber::Custom(s) => {
|
|
show.custom_episodes
|
|
.as_ref()
|
|
.map(|c| c.prefix.clone())
|
|
.unwrap_or_default()
|
|
+ s
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Show {
|
|
pub fn episodes(&self) -> anyhow::Result<Episodes> {
|
|
let path = &self.path;
|
|
let metadata =
|
|
fs::metadata(path).context("Failed to stat the show's path to determine file type")?;
|
|
if metadata.is_file() {
|
|
debug!("{} is a file, standalone", path.display());
|
|
Ok(HashMap::from([(
|
|
EpisodeNumber::Standalone,
|
|
path.to_path_buf(),
|
|
)]))
|
|
} else if metadata.is_dir() {
|
|
debug!("{} is a directory, enumerating episodes", path.display());
|
|
let files: Vec<PathBuf> = fs::read_dir(path)
|
|
.context("Failed to get a directory listing for the show's path")?
|
|
.map(|entry| {
|
|
let entry =
|
|
entry.context("Failed to read a directory entry under the show's path")?;
|
|
if !entry
|
|
.file_type()
|
|
.with_context(|| {
|
|
format!(
|
|
"Failed to get file type for directory entry {}",
|
|
entry.path().display()
|
|
)
|
|
})?
|
|
.is_file()
|
|
{
|
|
debug!("Skipping {}, not a file", entry.path().display());
|
|
return Ok(None);
|
|
}
|
|
if entry.file_name().into_string().is_err() {
|
|
error!(
|
|
"Path {} contains invalid unicode, skipping",
|
|
entry.path().display()
|
|
);
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(entry.path()))
|
|
})
|
|
.filter_map(|r| r.transpose())
|
|
.collect::<anyhow::Result<Vec<PathBuf>>>()?;
|
|
if let Some(CustomEpisodes { ref regex, .. }) = self.custom_episodes {
|
|
files
|
|
.into_iter()
|
|
.filter_map(|f| -> Option<anyhow::Result<(EpisodeNumber, PathBuf)>> {
|
|
let episode_name = regex
|
|
.captures(f.file_name().unwrap().to_str().unwrap())?
|
|
.name("episode")
|
|
.map(|m| m.as_str().to_string())
|
|
.ok_or(anyhow!(
|
|
"Failed to find capture group `episode` in episode regex"
|
|
));
|
|
Some(episode_name.map(|n| (EpisodeNumber::Custom(n), f)))
|
|
})
|
|
.collect::<anyhow::Result<Episodes>>()
|
|
} else {
|
|
enumeration::enumerate_episodes(files)
|
|
.ok_or(anyhow!("Could not detect any episode numbering scheme"))
|
|
}
|
|
} else {
|
|
Err(anyhow!("The show's path is not a file or a directory"))
|
|
}
|
|
}
|
|
}
|