mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-24 19:46:17 -05:00
add matching time parsing and formatting routines
* add a --ts subcommand to convert between numeric and human-readable representations. This is handy when directly inspecting the SQLite database or API output. * also take the human-readable form in the web interface's camera view. * to reduce confusion, when using trim=true on the web interface's camera view, trim the displayed starting and ending times as well as the actual .mp4 file links.
This commit is contained in:
13
src/main.rs
13
src/main.rs
@@ -93,6 +93,7 @@ const USAGE: &'static str = "
|
||||
Usage: moonfire-nvr [options]
|
||||
moonfire-nvr --upgrade [options]
|
||||
moonfire-nvr --check [options]
|
||||
moonfire-nvr --ts <ts>...
|
||||
moonfire-nvr (--help | --version)
|
||||
|
||||
Options:
|
||||
@@ -127,8 +128,10 @@ struct Args {
|
||||
flag_read_only: bool,
|
||||
flag_check: bool,
|
||||
flag_upgrade: bool,
|
||||
flag_ts: bool,
|
||||
flag_no_vacuum: bool,
|
||||
flag_preset_journal: String,
|
||||
arg_ts: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -171,11 +174,21 @@ fn main() {
|
||||
upgrade::run(conn, &args.flag_preset_journal, args.flag_no_vacuum).unwrap();
|
||||
} else if args.flag_check {
|
||||
check::run(conn, &args.flag_sample_file_dir).unwrap();
|
||||
} else if args.flag_ts {
|
||||
run_ts(args.arg_ts).unwrap();
|
||||
} else {
|
||||
run(args, conn, &signal);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_ts(timestamps: Vec<String>) -> Result<(), error::Error> {
|
||||
for timestamp in ×tamps {
|
||||
let t = recording::Time::parse(timestamp)?;
|
||||
println!("{} == {}", t, t.0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(args: Args, conn: rusqlite::Connection, signal: &chan::Receiver<chan_signal::Signal>) {
|
||||
let db = Arc::new(db::Database::new(conn).unwrap());
|
||||
let dir = dir::SampleFileDir::new(&args.flag_sample_file_dir, db.clone()).unwrap();
|
||||
|
||||
100
src/recording.rs
100
src/recording.rs
@@ -31,9 +31,11 @@
|
||||
extern crate uuid;
|
||||
|
||||
use coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
|
||||
use core::str::FromStr;
|
||||
use db;
|
||||
use std::ops;
|
||||
use error::Error;
|
||||
use regex::Regex;
|
||||
use std::ops;
|
||||
use std::fmt;
|
||||
use std::ops::Range;
|
||||
use std::string::String;
|
||||
@@ -53,6 +55,74 @@ impl Time {
|
||||
Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000)
|
||||
}
|
||||
|
||||
/// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
|
||||
///
|
||||
/// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds.
|
||||
///
|
||||
/// The latter is a string such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of
|
||||
/// a second such as `:00001`, followed by an optional time zone offset such as `Z` or
|
||||
/// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the
|
||||
/// local time zone.
|
||||
pub fn parse(s: &str) -> Result<Self, Error> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"(?x)
|
||||
^
|
||||
([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})
|
||||
(?::([0-9]{5}))?
|
||||
(Z|[+-]([0-9]{2}):([0-9]{2}))?
|
||||
$"#).unwrap();
|
||||
}
|
||||
|
||||
// First try parsing as 90,000ths of a second since epoch.
|
||||
match i64::from_str(s) {
|
||||
Ok(i) => return Ok(Time(i)),
|
||||
Err(_) => {},
|
||||
}
|
||||
|
||||
// If that failed, parse as a time string or bust.
|
||||
let c = RE.captures(s).ok_or_else(|| Error::new(format!("unparseable time {:?}", s)))?;
|
||||
let mut tm = time::Tm{
|
||||
tm_sec: i32::from_str(c.get(6).unwrap().as_str()).unwrap(),
|
||||
tm_min: i32::from_str(c.get(5).unwrap().as_str()).unwrap(),
|
||||
tm_hour: i32::from_str(c.get(4).unwrap().as_str()).unwrap(),
|
||||
tm_mday: i32::from_str(c.get(3).unwrap().as_str()).unwrap(),
|
||||
tm_mon: i32::from_str(c.get(2).unwrap().as_str()).unwrap(),
|
||||
tm_year: i32::from_str(c.get(1).unwrap().as_str()).unwrap(),
|
||||
tm_wday: 0,
|
||||
tm_yday: 0,
|
||||
tm_isdst: -1,
|
||||
tm_utcoff: 0,
|
||||
tm_nsec: 0,
|
||||
};
|
||||
if tm.tm_mon == 0 {
|
||||
return Err(Error::new(format!("time {:?} has month 0", s)));
|
||||
}
|
||||
tm.tm_mon -= 1;
|
||||
if tm.tm_year < 1900 {
|
||||
return Err(Error::new(format!("time {:?} has year before 1900", s)));
|
||||
}
|
||||
tm.tm_year -= 1900;
|
||||
|
||||
// The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0,
|
||||
// mktime() otherwise. If a zone is specified, use the timegm path and a manual offset.
|
||||
// If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the
|
||||
// chrono crate's lead and just use 0 or 1 to choose between these functions.
|
||||
let sec = if let Some(zone) = c.get(8) {
|
||||
tm.to_timespec().sec + if zone.as_str() == "Z" {
|
||||
0
|
||||
} else {
|
||||
let off = i64::from_str(c.get(9).unwrap().as_str()).unwrap() * 3600 +
|
||||
i64::from_str(c.get(10).unwrap().as_str()).unwrap() * 60;
|
||||
if zone.as_str().as_bytes()[0] == b'-' { off } else { -off }
|
||||
}
|
||||
} else {
|
||||
tm.tm_utcoff = 1;
|
||||
tm.to_timespec().sec
|
||||
};
|
||||
let fraction = if let Some(f) = c.get(7) { i64::from_str(f.as_str()).unwrap() } else { 0 };
|
||||
Ok(Time(sec * TIME_UNITS_PER_SEC + fraction))
|
||||
}
|
||||
|
||||
pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
|
||||
}
|
||||
|
||||
@@ -78,8 +148,10 @@ impl ops::Sub<Duration> for Time {
|
||||
impl fmt::Display for Time {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0});
|
||||
write!(f, "{}:{:05}", tm.strftime("%FT%T%Z").or_else(|_| Err(fmt::Error))?,
|
||||
self.0 % TIME_UNITS_PER_SEC)
|
||||
let zone_minutes = tm.tm_utcoff.abs() / 60;
|
||||
write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?,
|
||||
self.0 % TIME_UNITS_PER_SEC,
|
||||
if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +494,28 @@ mod tests {
|
||||
use super::*;
|
||||
use testutil::TestDb;
|
||||
|
||||
#[test]
|
||||
fn test_parse_time() {
|
||||
let tests = &[
|
||||
("2006-01-02T15:04:05-07:00", 102261550050000),
|
||||
("2006-01-02T15:04:05:00001-07:00", 102261550050001),
|
||||
("2006-01-02T15:04:05-08:00", 102261874050000),
|
||||
("2006-01-02T15:04:05", 102261874050000), // implied -08:00
|
||||
("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00
|
||||
("2006-01-02T15:04:05-00:00", 102259282050000),
|
||||
("2006-01-02T15:04:05Z", 102259282050000),
|
||||
("102261550050000", 102261550050000),
|
||||
];
|
||||
for test in tests {
|
||||
assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time() {
|
||||
assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_duration() {
|
||||
let tests = &[
|
||||
|
||||
29
src/web.rs
29
src/web.rs
@@ -198,21 +198,21 @@ struct Segments {
|
||||
impl Segments {
|
||||
pub fn parse(input: &str) -> Result<Segments, ()> {
|
||||
let caps = SEGMENTS_RE.captures(input).ok_or(())?;
|
||||
let ids_start = i32::from_str(caps.at(1).unwrap()).map_err(|_| ())?;
|
||||
let ids_end = match caps.at(2) {
|
||||
Some(e) => i32::from_str(&e[1..]).map_err(|_| ())?,
|
||||
let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?;
|
||||
let ids_end = match caps.get(2) {
|
||||
Some(e) => i32::from_str(&e.as_str()[1..]).map_err(|_| ())?,
|
||||
None => ids_start,
|
||||
} + 1;
|
||||
if ids_start < 0 || ids_end <= ids_start {
|
||||
return Err(());
|
||||
}
|
||||
let start_time = caps.at(3).map_or(Ok(0), i64::from_str).map_err(|_| ())?;
|
||||
let start_time = caps.get(3).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?;
|
||||
if start_time < 0 {
|
||||
return Err(());
|
||||
}
|
||||
let end_time = match caps.at(4) {
|
||||
let end_time = match caps.get(4) {
|
||||
Some(v) => {
|
||||
let e = i64::from_str(v).map_err(|_| ())?;
|
||||
let e = i64::from_str(v.as_str()).map_err(|_| ())?;
|
||||
if e <= start_time {
|
||||
return Err(());
|
||||
}
|
||||
@@ -309,19 +309,18 @@ impl Handler {
|
||||
fn camera_html(&self, db: MutexGuard<db::LockedDatabase>, query: &str,
|
||||
uuid: Uuid) -> Result<Vec<u8>, Error> {
|
||||
let (r, trim) = {
|
||||
let mut start = i64::min_value();
|
||||
let mut end = i64::max_value();
|
||||
let mut time = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
|
||||
let mut trim = false;
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
let (key, value) = (key.borrow(), value.borrow());
|
||||
match key {
|
||||
"start_time_90k" => start = i64::from_str(value)?,
|
||||
"end_time_90k" => end = i64::from_str(value)?,
|
||||
"start_time" => time.start = recording::Time::parse(value)?,
|
||||
"end_time" => time.end = recording::Time::parse(value)?,
|
||||
"trim" if value == "true" => trim = true,
|
||||
_ => {},
|
||||
}
|
||||
};
|
||||
(recording::Time(start) .. recording::Time(end), trim)
|
||||
(time, trim)
|
||||
};
|
||||
let camera = db.get_camera(uuid)
|
||||
.ok_or_else(|| Error::new("no such camera".to_owned()))?;
|
||||
@@ -383,12 +382,13 @@ impl Handler {
|
||||
}
|
||||
url
|
||||
};
|
||||
let start = if trim && row.time.start < r.start { r.start } else { row.time.start };
|
||||
let end = if trim && row.time.end > r.end { r.end } else { row.time.end };
|
||||
write!(&mut buf, "\
|
||||
<tr><td><a href=\"{}\">{}</a></td>\
|
||||
<td>{}</td><td>{}x{}</td><td>{:.0}</td><td>{:b}B</td><td>{}bps</td></tr>\n",
|
||||
url, HumanizedTimestamp(Some(row.time.start)),
|
||||
HumanizedTimestamp(Some(row.time.end)), row.video_sample_entry.width,
|
||||
row.video_sample_entry.height,
|
||||
url, HumanizedTimestamp(Some(start)), HumanizedTimestamp(Some(end)),
|
||||
row.video_sample_entry.width, row.video_sample_entry.height,
|
||||
if seconds == 0 { 0. } else { row.video_samples as f32 / seconds as f32 },
|
||||
Humanized(row.sample_file_bytes),
|
||||
Humanized(if seconds == 0 { 0 } else { row.sample_file_bytes * 8 / seconds }))?;
|
||||
@@ -540,6 +540,7 @@ impl Handler {
|
||||
impl server::Handler for Handler {
|
||||
fn handle(&self, req: server::Request, res: server::Response) {
|
||||
let (path, query) = get_path_and_query(&req.uri);
|
||||
error!("path={:?}, query={:?}", path, query);
|
||||
let res = match decode_path(path) {
|
||||
Path::CamerasList => self.list_cameras(&req, res),
|
||||
Path::Camera(uuid) => self.camera(uuid, query, &req, res),
|
||||
|
||||
Reference in New Issue
Block a user