diff --git a/src/config.rs b/src/config.rs index 54ab8da..9f06828 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,4 @@ -use std::{ - env::{self, VarError}, - fmt::Debug, - path::PathBuf, - str::FromStr, - time::Duration, -}; +use std::{env::{self, VarError}, path::PathBuf, str::FromStr, fmt::Debug, time::Duration}; pub struct Config { pub shows_file: PathBuf, @@ -22,10 +16,7 @@ fn get_var(name: &str) -> Result { } fn parse_var>(name: &str) -> Result { - get_var(name).map(|s| { - s.parse() - .expect(&format!("Failed to parse {}{}", VAR_PREFIX, name)) - }) + get_var(name).map(|s| s.parse().expect(&format!("Failed to parse {}{}", VAR_PREFIX, name))) } fn expect_var(name: &str) -> String { @@ -35,10 +26,8 @@ fn expect_var(name: &str) -> String { pub fn load() -> Config { Config { shows_file: parse_var("SHOWS_FILE").unwrap_or(PathBuf::from("./shows.yaml")), - global_tags: get_var("GLOBAL_TAGS") - .map(|s| s.split(",").map(String::from).collect()) - .unwrap_or_default(), - post_interval: Duration::from_secs(parse_var("POST_INTERVAL").unwrap_or(6 * 3600)), + global_tags: get_var("GLOBAL_TAGS").map(|s| s.split(",").map(String::from).collect()).unwrap_or_default(), + post_interval: Duration::from_secs(parse_var("POST_INTERVAL").unwrap_or(6*3600)), cohost_email: expect_var("COHOST_EMAIL"), cohost_password: expect_var("COHOST_PASSWORD"), cohost_page: expect_var("COHOST_PAGE"), diff --git a/src/main.rs b/src/main.rs index 83c4ba8..1b794a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,6 @@ mod config; mod shows; mod video; -use crate::shows::EpisodeNumber; - #[tokio::main] async fn main() { dotenvy::dotenv().ok(); @@ -21,22 +19,22 @@ async fn main() { let shows = shows::load(conf.shows_file); info!("Logging into cohost as {}", conf.cohost_email); - let session = eggbug::Session::login(&conf.cohost_email, &conf.cohost_password) - .await - .expect("Failed to login to cohost"); + let session = eggbug::Session::login(&conf.cohost_email, &conf.cohost_password).await.expect("Failed to login to cohost"); loop { - let (title, show) = shows.iter().choose(&mut rng).expect("No shows found!"); + let (title, show) = shows + .iter() + .choose(&mut rng) + .expect("No shows found!"); let (num, file) = show.episodes.iter().choose(&mut rng).unwrap(); let descriptor = format!( "{}{}", title, - match num { - EpisodeNumber::Standalone => String::new(), - EpisodeNumber::SingleSeason(n) => format!(" episode {}", n), - EpisodeNumber::MultiSeason(season, ep) => - format!(" season {} episode {}", season, ep), + if let Some(n) = num { + format!(" episode {}", n) + } else { + String::new() } ); @@ -56,41 +54,36 @@ async fn main() { let formatted_timestamp = format_timestamp(timestamp, Some(video_info.duration_secs)); info!("Taking screencap at {}", formatted_timestamp); - match video::take_screencap(file, timestamp, video_info.subtitle_stream_index).await { - Err(e) => { - error!("Failed to take screencap: {}", e); - } + match video::take_screencap( + file, + timestamp, + video_info.subtitle_stream_index, + ).await { + Err(e) => { error!("Failed to take screencap: {}", e); } Ok(img_data) => { let attachment = eggbug::Attachment::new( img_data, format!("{} @{}.png", descriptor, formatted_timestamp), String::from("image/png"), - ) - .with_alt_text(format!( - "Screencap of {} at {}", - descriptor, formatted_timestamp - )); + ).with_alt_text(format!("Screencap of {} at {}", descriptor, formatted_timestamp)); let mut tags = show.tags.clone(); tags.extend_from_slice(&conf.global_tags); - match session - .create_post( - &conf.cohost_page, - &mut eggbug::Post { - content_warnings: vec![descriptor], - attachments: vec![attachment], - tags, - draft: false, - adult_content: false, - headline: String::new(), - markdown: String::new(), - }, - ) - .await - { + match session.create_post( + &conf.cohost_page, + &mut eggbug::Post { + content_warnings: vec![descriptor], + attachments: vec![attachment], + tags, + draft: false, + adult_content: false, + headline: String::new(), + markdown: String::new(), + } + ).await { Ok(id) => info!("Created post {}", id), - Err(e) => error!("Failed to create post: {}", e), + Err(e) => error!("Failed to create post: {}", e) } } } diff --git a/src/shows.rs b/src/shows.rs new file mode 100644 index 0000000..30210f0 --- /dev/null +++ b/src/shows.rs @@ -0,0 +1,134 @@ +use std::{ + collections::{BTreeSet, HashMap}, + fs, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use lazy_static::lazy_static; +use log::{debug, error, trace, warn}; +use regex::Regex; +use serde::Deserialize; + +lazy_static! { + static ref NUMBER_REGEX: Regex = Regex::new("[0-9]+").unwrap(); +} + +#[derive(Deserialize)] +pub struct ShowSpec { + pub path: PathBuf, + pub tags: Vec, +} + +/// [episode number, or None for a standalone movie] -> [video file path] +type Episodes = HashMap, PathBuf>; + +pub struct Show { + pub episodes: Episodes, + pub tags: Vec, +} + +pub fn load(shows_file: PathBuf) -> HashMap { + let show_specs: HashMap = serde_yaml::from_reader( + fs::File::open(shows_file).expect("Failed to open shows file"), + ) + .expect("Failed to parse YAML from shows file"); + + show_specs + .into_iter() + .filter_map(|(name, show)| { + debug!("Enumerating show {}: {}", name, show.path.display()); + enumerate_show(show.path) + .map_err(|e| { + error!("Error processing {}: {}", name, e); + }) + .ok() + .map(|eps| (name, Show { episodes: eps, tags: show.tags })) + }) + .collect() +} + +fn enumerate_show(path: PathBuf) -> std::io::Result { + let metadata = fs::metadata(&path)?; + if metadata.is_file() { + debug!("{} is a file, standalone", path.display()); + Ok(HashMap::from([(None, path)])) + } else if metadata.is_dir() { + debug!("{} is a directory, enumerating episodes", path.display()); + enumerate_dir(path) + } else { + Err(std::io::Error::new( + ErrorKind::InvalidInput, + format!("Invalid file type for {}", path.display()), + )) + } +} + +fn enumerate_dir(path: PathBuf) -> std::io::Result { + let files: Vec<(PathBuf, String)> = fs::read_dir(&path)? + .map(|entry| { + let entry = entry?; + if !entry.file_type()?.is_file() { + debug!("Skipping {}, not a file", entry.path().display()); + return Ok(None); + } + Ok(Some(( + entry.path(), + entry.file_name().into_string().map_err(|o| { + std::io::Error::new(ErrorKind::InvalidData, format!("Invalid filename {:?}", o)) + })?, + ))) + }) + .filter_map(|r| r.transpose()) + .collect::>>()?; + + // For each string prefix which is followed by a number in one or + // more filenames, record what the number is for each filename + // with that prefix, sorted by number. + // + // We use a BTreeSet of pairs rather than a BTreeMap so that + // duplicates are preserved. + let mut prefix_enumerations: HashMap<&str, BTreeSet<(u32, &Path)>> = HashMap::new(); + for (path, filename) in files.iter() { + for m in NUMBER_REGEX.find_iter(filename) { + let num = m.as_str().parse(); + if let Err(e) = num { + warn!("Failed to parse candidate number: {}", e); + continue; + } + let num = num.unwrap(); + let prefix = filename.get(..m.start()).unwrap(); + trace!("{}: candidate prefix {}, number {}", filename, prefix, num); + prefix_enumerations + .entry(prefix) + .or_default() + .insert((num, path)); + } + } + + // Use the prefix enumeration that produces the most consecutive numbers + let best_enumeration = prefix_enumerations + .into_values() + .max_by_key(|set| { + set.iter() + .zip(set.iter().skip(1)) + .filter(|((cur, _), (next, _))| next - cur == 1) + .count() + }) + .ok_or(std::io::Error::new( + ErrorKind::InvalidData, + "No valid prefixes found", + ))?; + + let mut result: Episodes = HashMap::new(); + for (num, path) in best_enumeration.into_iter() { + debug!("Episode {}: {}", num, path.display()); + if let Some(dup) = result.insert(Some(num), path.to_path_buf()) { + warn!( + "Duplicate episode number, discarding file {}", + dup.display() + ); + } + } + Ok(result) +} diff --git a/src/shows/enumeration.rs b/src/shows/enumeration.rs deleted file mode 100644 index 425a509..0000000 --- a/src/shows/enumeration.rs +++ /dev/null @@ -1,348 +0,0 @@ -use std::{ - collections::{BTreeSet, HashMap}, - path::PathBuf, -}; - -use lazy_static::lazy_static; -use log::{debug, trace, warn}; -use regex::Regex; - -use super::{EpisodeNumber, Episodes}; - -lazy_static! { - static ref NUMBER_REGEX: Regex = Regex::new("[0-9]+").unwrap(); -} - -// An association of numbers to items, sorted by number. -// -// We use a BTreeSet of pairs rather than a BTreeMap so that -// duplicates are preserved. -type Enumeration = BTreeSet<(u32, T)>; - -fn consecutiveness(enumeration: &Enumeration) -> usize { - enumeration - .iter() - .zip(enumeration.iter().skip(1)) - .filter(|((cur, _), (next, _))| next - cur == 1) - .count() -} - -fn fully_consecutive(enumeration: &Enumeration) -> bool { - consecutiveness(enumeration) == enumeration.len() - 1 -} - -// Take a series of items that can be represented as strings. For each -// string prefix which is followed by a number in one or more items' -// representations, record what the number is for each item that has -// that prefix. -fn prefix_enumerations<'a, T: Copy + Ord, I: Iterator, F: Fn(T) -> &'a str>( - items: I, - f: F, -) -> HashMap<&'a str, Enumeration> { - let mut enumerations: HashMap<&str, Enumeration> = HashMap::new(); - for item in items { - let name = f(item); - for m in NUMBER_REGEX.find_iter(name) { - let num = m.as_str().parse(); - if let Err(e) = num { - warn!("Failed to parse candidate number: {}", e); - continue; - } - let num = num.unwrap(); - let prefix = name.get(..m.start()).unwrap(); - trace!("{}: candidate prefix {}, number {}", name, prefix, num); - enumerations.entry(prefix).or_default().insert((num, item)); - } - } - enumerations -} - -pub fn enumerate_episodes(files: Vec) -> Option { - let mut episode_enumerations = prefix_enumerations(files.iter(), |path| { - path.file_name().unwrap().to_str().unwrap() - }); - - // Retain only prefix enumerations that produce consecutive numbers - episode_enumerations.retain(|_, en| consecutiveness(en) > 0); - - let mut result: Episodes = HashMap::new(); - - if episode_enumerations.len() > 1 { - // There are multiple viable episode enumerations, which may - // mean that this list of files includes multiple seasons of a - // series. Look for a fully consecutive prefix enumeration *of - // the candidate episode prefixes* to find season numbers. - let season_enumerations = prefix_enumerations(episode_enumerations.keys(), |s| s); - let best_season_enumeration = season_enumerations - .into_values() - .max_by_key(consecutiveness); - if let Some(season_enumeration) = best_season_enumeration { - if season_enumeration.len() > 1 && fully_consecutive(&season_enumeration) { - for (season_num, prefix) in season_enumeration.into_iter() { - for (episode_num, path) in episode_enumerations.get(prefix).unwrap().iter() { - debug!( - "Season {} episode {}: {}", - season_num, - episode_num, - path.display() - ); - if let Some(dup) = result.insert( - EpisodeNumber::MultiSeason(season_num, *episode_num), - path.to_path_buf(), - ) { - warn!( - "Duplicate episode number, discarding file {}", - dup.display() - ); - } - } - } - - return Some(result); - } - } - } - - // No evidence found for multiple seasons, so just take the best episode enumeration - let best_episode_enumeration = episode_enumerations - .into_values() - .max_by_key(consecutiveness)?; - - for (num, path) in best_episode_enumeration.into_iter() { - debug!("Episode {}: {}", num, path.display()); - if let Some(dup) = result.insert(EpisodeNumber::SingleSeason(num), path.to_path_buf()) { - warn!( - "Duplicate episode number, discarding file {}", - dup.display() - ); - } - } - Some(result) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_enumerate_single_season() { - let files = vec![ - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 07v2 (1080p) [BBF1FDA4].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 08 (1080p) [95D16D74].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 04v2 (1080p) [D291A3B0].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 11v2 (1080p) [B7B3ECB6].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 05 (1080p) [16CC6267].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 02v2 (1080p) [5E88C757].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 03v2 (1080p) [5F5AD4BD].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 01v2 (1080p) [8C038972].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 12 (1080p) [179132FA].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 06v2 (1080p) [21EA6641].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 09 (1080p) [5A9C6CEC].mkv", - ), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 10 (1080p) [97953FA7].mkv", - ), - ]; - - let expected = Some(HashMap::from([ - ( - EpisodeNumber::SingleSeason(1), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 01v2 (1080p) [8C038972].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(2), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 02v2 (1080p) [5E88C757].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(3), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 03v2 (1080p) [5F5AD4BD].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(4), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 04v2 (1080p) [D291A3B0].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(5), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 05 (1080p) [16CC6267].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(6), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 06v2 (1080p) [21EA6641].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(7), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 07v2 (1080p) [BBF1FDA4].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(8), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 08 (1080p) [95D16D74].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(9), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 09 (1080p) [5A9C6CEC].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(10), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 10 (1080p) [97953FA7].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(11), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 11v2 (1080p) [B7B3ECB6].mkv", - ), - ), - ( - EpisodeNumber::SingleSeason(12), - PathBuf::from( - "media/Chainsaw Man/[SubsPlease] Chainsaw Man - 12 (1080p) [179132FA].mkv", - ), - ), - ])); - - assert_eq!(enumerate_episodes(files), expected); - } - - #[test] - fn test_enumerate_two_seasons_with_number_in_title() { - let files = vec![ - PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E04 - A Reason to Fight (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E03 - Allelujah Rescue Operation (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E05 - Homeland Burning (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E02 - Gundam Meisters (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E02 - Twin Drive (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E01 - Celestial Being (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E06 - Scars (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E04 - International Negotiation (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E05 - Escape Limit Zone (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E03 - The Changing World (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E01 - The Angels' Second Advent (720p - DUAL Audio).mkv"), - PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E06 - Seven Swords (720p - DUAL Audio).mkv"), - ]; - let expected = Some(HashMap::from([ - (EpisodeNumber::MultiSeason(1, 1), PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E01 - Celestial Being (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(1, 2), PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E02 - Gundam Meisters (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(1, 3), PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E03 - The Changing World (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(1, 4), PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E04 - International Negotiation (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(1, 5), PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E05 - Escape Limit Zone (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(1, 6), PathBuf::from("media/MSG 00/MS Gundam 00 - S01 E06 - Seven Swords (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(2, 1), PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E01 - The Angels' Second Advent (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(2, 2), PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E02 - Twin Drive (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(2, 3), PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E03 - Allelujah Rescue Operation (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(2, 4), PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E04 - A Reason to Fight (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(2, 5), PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E05 - Homeland Burning (720p - DUAL Audio).mkv")), - (EpisodeNumber::MultiSeason(2, 6), PathBuf::from("media/MSG 00/MS Gundam 00 - S02 E06 - Scars (720p - DUAL Audio).mkv")), - ])); - - assert_eq!(enumerate_episodes(files), expected); - } - - #[test] - fn test_enumerate_non_ascii() { - let files = vec![ - PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 03 / 機動戦士ガンダム Twilight AXIS 第3話 [720p].mkv"), - PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 06 / 機動戦士ガンダム Twilight AXIS 第6話 [720p].mkv"), - PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 02 / 機動戦士ガンダム Twilight AXIS 第2話 [720p].mkv"), - PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 04 / 機動戦士ガンダム Twilight AXIS 第4話 [720p].mkv"), - PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 01 / 機動戦士ガンダム Twilight AXIS 第1話 [720p].mkv"), - PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 05 / 機動戦士ガンダム Twilight AXIS 第5話 [720p].mkv"), - ]; - let expected = Some(HashMap::from([ - (EpisodeNumber::SingleSeason(1), PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 01 / 機動戦士ガンダム Twilight AXIS 第1話 [720p].mkv")), - (EpisodeNumber::SingleSeason(2), PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 02 / 機動戦士ガンダム Twilight AXIS 第2話 [720p].mkv")), - (EpisodeNumber::SingleSeason(3), PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 03 / 機動戦士ガンダム Twilight AXIS 第3話 [720p].mkv")), - (EpisodeNumber::SingleSeason(4), PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 04 / 機動戦士ガンダム Twilight AXIS 第4話 [720p].mkv")), - (EpisodeNumber::SingleSeason(5), PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 05 / 機動戦士ガンダム Twilight AXIS 第5話 [720p].mkv")), - (EpisodeNumber::SingleSeason(6), PathBuf::from("media/Twilight AXIS/[(́◉◞౪◟◉‵)] Mobile Suit Gundam Twilight AXIS - 06 / 機動戦士ガンダム Twilight AXIS 第6話 [720p].mkv")), - ])); - - assert_eq!(enumerate_episodes(files), expected); - } - - #[test] - fn test_enumerate_extraneous_numbered_items() { - let files = vec![ - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_03_Resub(720p)[0A99BA5D].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_OP1_Resub(720p)[A5ADDABF].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]Gundam_Frag_(1080p)[546747A1].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_06_Resub(720p)[CAADADF2].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_ED2_Resub(720p)[21717EAF].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_02_Resub(720p)[115ABF72].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_05_Resub(720p)[A0FD098A].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_01_Resub(720p)[7CDE63CD].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_ED1_Resub(720p)[CA9B2E3F].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]Gundam_Frag_II(1080p)[49465CA8].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_04_Resub(720p)[8D219C3C].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_OP2_Resub(720p)[F691D6D5].mkv"), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_OP3_Resub(720p)[BD7DCCE6].mkv"), - ]; - let expected = Some(HashMap::from([ - ( - EpisodeNumber::SingleSeason(1), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_01_Resub(720p)[7CDE63CD].mkv"), - ), - ( - EpisodeNumber::SingleSeason(2), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_02_Resub(720p)[115ABF72].mkv"), - ), - ( - EpisodeNumber::SingleSeason(3), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_03_Resub(720p)[0A99BA5D].mkv"), - ), - ( - EpisodeNumber::SingleSeason(4), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_04_Resub(720p)[8D219C3C].mkv"), - ), - ( - EpisodeNumber::SingleSeason(5), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_05_Resub(720p)[A0FD098A].mkv"), - ), - ( - EpisodeNumber::SingleSeason(6), - PathBuf::from("media/ZZ Gundam/[EG]ZZ_Gundam_BD_06_Resub(720p)[CAADADF2].mkv"), - ), - ])); - - assert_eq!(enumerate_episodes(files), expected); - } -} diff --git a/src/shows/mod.rs b/src/shows/mod.rs deleted file mode 100644 index 1dd894b..0000000 --- a/src/shows/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::{collections::HashMap, fs, io::ErrorKind, path::PathBuf}; - -use log::{debug, error}; -use serde::Deserialize; - -mod enumeration; - -#[derive(Deserialize)] -pub struct ShowSpec { - pub path: PathBuf, - pub tags: Vec, -} - -#[derive(Debug, Eq, Hash, PartialEq)] -pub enum EpisodeNumber { - Standalone, - SingleSeason(u32), - MultiSeason(u32, u32), -} - -type Episodes = HashMap; - -pub struct Show { - pub episodes: Episodes, - pub tags: Vec, -} - -pub fn load(shows_file: PathBuf) -> HashMap { - let show_specs: HashMap = - serde_yaml::from_reader(fs::File::open(shows_file).expect("Failed to open shows file")) - .expect("Failed to parse YAML from shows file"); - - show_specs - .into_iter() - .filter_map(|(name, show)| { - debug!("Enumerating show {}: {}", name, show.path.display()); - load_path(show.path) - .map_err(|e| { - error!("Error processing {}: {}", name, e); - }) - .ok() - .map(|eps| { - ( - name, - Show { - episodes: eps, - tags: show.tags, - }, - ) - }) - }) - .collect() -} - -fn load_path(path: PathBuf) -> std::io::Result { - let metadata = fs::metadata(&path)?; - if metadata.is_file() { - debug!("{} is a file, standalone", path.display()); - Ok(HashMap::from([(EpisodeNumber::Standalone, path)])) - } else if metadata.is_dir() { - debug!("{} is a directory, enumerating episodes", path.display()); - let files: Vec = fs::read_dir(&path)? - .map(|entry| { - let entry = entry?; - if !entry.file_type()?.is_file() { - debug!("Skipping {}, not a file", entry.path().display()); - return Ok(None); - } - if entry.file_name().into_string().is_err() { - debug!( - "Skipping {}, contains invalid unicode", - entry.path().display() - ); - return Ok(None); - } - Ok(Some(entry.path())) - }) - .filter_map(|r| r.transpose()) - .collect::>>()?; - enumeration::enumerate_episodes(files).ok_or(std::io::Error::new( - ErrorKind::InvalidData, - "No valid prefixes found", - )) - } else { - Err(std::io::Error::new( - ErrorKind::InvalidInput, - format!("Invalid file type for {}", path.display()), - )) - } -} diff --git a/src/video.rs b/src/video.rs index 5b320dd..1330334 100644 --- a/src/video.rs +++ b/src/video.rs @@ -106,6 +106,6 @@ pub async fn take_screencap>( "ffmpeg command failed", )); } - + fs::read(&dest_path).await }