Compare commits

..

No commits in common. "9c2b726faa936ad7402958f5fcf243515572d39e" and "3113a501699e6e193a5a677af42f0d16c9449218" have entirely different histories.

6 changed files with 102 additions and 93 deletions

8
Cargo.lock generated
View file

@ -814,9 +814,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.55" version = "0.10.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@ -846,9 +846,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.90" version = "0.9.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",

View file

@ -31,13 +31,6 @@
packages.x86_64-linux.default = packages.x86_64-linux.${pname}; packages.x86_64-linux.default = packages.x86_64-linux.${pname};
devShells.x86_64-linux.default = pkgs.mkShell { devShells.x86_64-linux.default = pkgs.mkShell buildDeps;
inherit (buildDeps) buildInputs LIBCLANG_PATH;
nativeBuildInputs = buildDeps.nativeBuildInputs ++ (with pkgs; [
cargo-audit
clippy
rustfmt
]);
};
}; };
} }

View file

@ -15,7 +15,7 @@ pub struct Config {
pub cohost_page: String, pub cohost_page: String,
} }
const VAR_PREFIX: &str = "SCREENCAP_BOT_"; const VAR_PREFIX: &'static str = "SCREENCAP_BOT_";
fn get_var(name: &str) -> Result<String, VarError> { fn get_var(name: &str) -> Result<String, VarError> {
env::var(VAR_PREFIX.to_string() + name) env::var(VAR_PREFIX.to_string() + name)
@ -24,19 +24,19 @@ fn get_var(name: &str) -> Result<String, VarError> {
fn parse_var<E: Debug, T: FromStr<Err = E>>(name: &str) -> Result<T, VarError> { fn parse_var<E: Debug, T: FromStr<Err = E>>(name: &str) -> Result<T, VarError> {
get_var(name).map(|s| { get_var(name).map(|s| {
s.parse() s.parse()
.unwrap_or_else(|e| panic!("Failed to parse {}{}: {:?}", VAR_PREFIX, name, e)) .expect(&format!("Failed to parse {}{}", VAR_PREFIX, name))
}) })
} }
fn expect_var(name: &str) -> String { fn expect_var(name: &str) -> String {
get_var(name).unwrap_or_else(|_| panic!("{}{} must be set", VAR_PREFIX, name)) get_var(name).expect(&format!("{}{} must be set", VAR_PREFIX, name))
} }
pub fn load() -> Config { pub fn load() -> Config {
Config { Config {
shows_file: parse_var("SHOWS_FILE").unwrap_or(PathBuf::from("./shows.yaml")), shows_file: parse_var("SHOWS_FILE").unwrap_or(PathBuf::from("./shows.yaml")),
global_tags: get_var("GLOBAL_TAGS") global_tags: get_var("GLOBAL_TAGS")
.map(|s| s.split(',').map(String::from).collect()) .map(|s| s.split(",").map(String::from).collect())
.unwrap_or_default(), .unwrap_or_default(),
post_interval: Duration::from_secs(parse_var("POST_INTERVAL").unwrap_or(6 * 3600)), post_interval: Duration::from_secs(parse_var("POST_INTERVAL").unwrap_or(6 * 3600)),
cohost_email: expect_var("COHOST_EMAIL"), cohost_email: expect_var("COHOST_EMAIL"),

View file

@ -27,14 +27,7 @@ async fn main() {
loop { 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 episodes = match show.episodes() { let (num, file) = show.episodes.iter().choose(&mut rng).unwrap();
Ok(eps) => eps,
Err(e) => {
error!("Failed to get episodes for {}: {}", title, e);
continue;
}
};
let (num, file) = episodes.iter().choose(&mut rng).unwrap();
let descriptor = format!( let descriptor = format!(
"{}{}", "{}{}",

View file

@ -6,7 +6,7 @@ use serde::Deserialize;
mod enumeration; mod enumeration;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Show { pub struct ShowSpec {
pub path: PathBuf, pub path: PathBuf,
pub tags: Vec<String>, pub tags: Vec<String>,
} }
@ -20,50 +20,71 @@ pub enum EpisodeNumber {
type Episodes = HashMap<EpisodeNumber, PathBuf>; type Episodes = HashMap<EpisodeNumber, PathBuf>;
pub fn load(shows_file: PathBuf) -> HashMap<String, Show> { pub struct Show {
serde_yaml::from_reader(fs::File::open(shows_file).expect("Failed to open shows file")) pub episodes: Episodes,
.expect("Failed to parse YAML from shows file") pub tags: Vec<String>,
} }
impl Show { pub fn load(shows_file: PathBuf) -> HashMap<String, Show> {
pub fn episodes(&self) -> std::io::Result<Episodes> { let show_specs: HashMap<String, ShowSpec> =
let path = &self.path; serde_yaml::from_reader(fs::File::open(shows_file).expect("Failed to open shows file"))
let metadata = fs::metadata(path)?; .expect("Failed to parse YAML from shows file");
if metadata.is_file() {
debug!("{} is a file, standalone", path.display()); show_specs
Ok(HashMap::from([( .into_iter()
EpisodeNumber::Standalone, .filter_map(|(name, show)| {
path.to_path_buf(), debug!("Enumerating show {}: {}", name, show.path.display());
)])) load_path(show.path)
} else if metadata.is_dir() { .map_err(|e| {
debug!("{} is a directory, enumerating episodes", path.display()); error!("Error processing {}: {}", name, e);
let files: Vec<PathBuf> = 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() {
error!(
"Path {} contains invalid unicode, skipping",
entry.path().display()
);
return Ok(None);
}
Ok(Some(entry.path()))
}) })
.filter_map(|r| r.transpose()) .ok()
.collect::<std::io::Result<Vec<PathBuf>>>()?; .map(|eps| {
enumeration::enumerate_episodes(files).ok_or(std::io::Error::new( (
ErrorKind::InvalidData, name,
"No valid prefixes found", Show {
)) episodes: eps,
} else { tags: show.tags,
Err(std::io::Error::new( },
ErrorKind::InvalidInput, )
format!("Invalid file type for {}", path.display()), })
)) })
} .collect()
}
fn load_path(path: PathBuf) -> std::io::Result<Episodes> {
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<PathBuf> = 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::<std::io::Result<Vec<PathBuf>>>()?;
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()),
))
} }
} }

View file

@ -27,31 +27,33 @@ pub fn get_video_info<P: AsRef<Path>>(
let duration_secs = ctx.duration() as f64 / f64::from(ffmpeg_next::ffi::AV_TIME_BASE); let duration_secs = ctx.duration() as f64 / f64::from(ffmpeg_next::ffi::AV_TIME_BASE);
let subtitle_stream_index = subtitle_lang.and_then(|lang| { let subtitle_stream_index = subtitle_lang
ctx.streams() .map(|lang| {
.filter(|stream| { ctx.streams()
ffmpeg_next::codec::context::Context::from_parameters(stream.parameters()) .filter(|stream| {
.map(|c| c.medium()) ffmpeg_next::codec::context::Context::from_parameters(stream.parameters())
== Ok(Type::Subtitle) .map(|c| c.medium())
}) == Ok(Type::Subtitle)
.enumerate() })
.filter(|(_, stream)| { .enumerate()
let metadata = stream.metadata(); .filter(|(_, stream)| {
if metadata.get("language") != Some(lang) { let metadata = stream.metadata();
return false; if metadata.get("language") != Some(lang) {
} return false;
if metadata }
.get("title") if metadata
.map(|t| SUBTITLE_FORBID_REGEX.is_match(t)) .get("title")
== Some(true) .map(|t| SUBTITLE_FORBID_REGEX.is_match(t))
{ == Some(true)
return false; {
} return false;
true }
}) true
.min_by_key(|(_, stream)| stream.disposition().contains(Disposition::FORCED)) })
.map(|(idx, _)| idx) .min_by_key(|(_, stream)| stream.disposition().contains(Disposition::FORCED))
}); .map(|(idx, _)| idx)
})
.flatten();
Ok(VideoInfo { Ok(VideoInfo {
duration_secs, duration_secs,
@ -64,7 +66,7 @@ pub async fn take_screencap<P: AsRef<Path>>(
timestamp_secs: f64, timestamp_secs: f64,
subtitle_stream_index: Option<usize>, subtitle_stream_index: Option<usize>,
) -> std::io::Result<Vec<u8>> { ) -> std::io::Result<Vec<u8>> {
let ext = source.as_ref().extension().and_then(|s| s.to_str()); let ext = source.as_ref().extension().map(|s| s.to_str()).flatten();
if ext != Some("mkv") && ext != Some("mp4") { if ext != Some("mkv") && ext != Some("mp4") {
return Err(std::io::Error::new( return Err(std::io::Error::new(
ErrorKind::Other, ErrorKind::Other,