Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,33 @@ neolink ptz --config=config.toml CameraName zoom 2.5

With 1.0 being normal and 2.5 being 2.5x zoom

### Stored recording metadata

You can list recording metadata reported by a camera for one logical channel
without downloading footage:

```bash
# Privacy-conscious summary only (no filenames or recording paths)
neolink recordings --config=config.toml CameraName --date 2026-01-02

# Override the configured logical channel and request the main recording stream
neolink recordings --config=config.toml CameraName --date 2026-01-02 \
--channel 1 --stream main

# Machine-readable results, including camera-provided recording identifiers
neolink recordings --config=config.toml CameraName --date 2026-01-02 --json
```

Queries use the camera's local calendar date and are deliberately bounded. Use
`--max-pages` and `--max-entries` to lower the defaults; both also have hard
safety ceilings. The default text output reports only counts, pagination state,
and the earliest/latest timestamps. JSON never includes the camera UID,
credentials, or raw protocol XML. It includes the camera-provided identifiers
and any name/path fields already present in the response; the command does not
perform separate filename enrichment.

This command only lists metadata. It does not replay or download recordings.

### Services (camera ports)

You can inspect and change the camera's service ports (`baichuan`, `http`,
Expand Down
44 changes: 33 additions & 11 deletions crates/core/src/bc/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ fn bc_modern_msg<'a>(
// Now we'll take the buffer that Nom gave a ref to and parse it.
let extension = if ext_len > 0 {
if context.debug {
println!(
log::debug!(
"Extension Txt: {:?}",
String::from_utf8(processed_ext_buf.to_vec()).unwrap_or("Not Text".to_string())
);
Expand Down Expand Up @@ -217,19 +217,33 @@ fn bc_modern_msg<'a>(
};
} else {
if context.debug {
println!(
"Payload Txt: {:?}",
String::from_utf8(processed_payload_buf.to_vec())
.unwrap_or("Not Text".to_string())
);
if is_file_info_list_message(header.msg_id) {
log::debug!(
"Payload Txt: <FileInfoList redacted, {} bytes>",
processed_payload_buf.len()
);
} else {
log::debug!(
"Payload Txt: {:?}",
String::from_utf8(processed_payload_buf.to_vec())
.unwrap_or("Not Text".to_string())
);
}
}
let xml = BcXml::try_parse(processed_payload_buf.as_slice()).map_err(|e| {
error!("header.msg_id: {}", header.msg_id);
error!(
"processed_payload_buf: {:X?}::{:?}",
processed_payload_buf,
std::str::from_utf8(&processed_payload_buf)
);
if is_file_info_list_message(header.msg_id) {
error!(
"FileInfoList XML payload redacted ({} bytes)",
processed_payload_buf.len()
);
} else {
error!(
"processed_payload_buf: {:X?}::{:?}",
processed_payload_buf,
std::str::from_utf8(&processed_payload_buf)
);
}
log::error!("e: {:?}", e);
Err::Error(make_error(
buf,
Expand Down Expand Up @@ -301,6 +315,14 @@ mod tests {
.try_init();
}

#[test]
fn file_info_list_messages_are_classified_as_private() {
assert!(is_file_info_list_message(MSG_ID_FILE_INFO_LIST_OPEN));
assert!(is_file_info_list_message(MSG_ID_FILE_INFO_LIST_GET));
assert!(is_file_info_list_message(MSG_ID_FILE_INFO_LIST_CLOSE));
assert!(!is_file_info_list_message(MSG_ID_LOGIN));
}

#[test]
fn test_bc_modern_login() {
init();
Expand Down
15 changes: 15 additions & 0 deletions crates/core/src/bc/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ pub const MSG_ID_VIDEO_STOP: u32 = 4;
pub const MSG_ID_TALKABILITY: u32 = 10;
/// TalkReset messages have this ID
pub const MSG_ID_TALKRESET: u32 = 11;
/// Open a FileInfoList recording metadata search
pub const MSG_ID_FILE_INFO_LIST_OPEN: u32 = 14;
/// Read one page from a FileInfoList recording metadata search
pub const MSG_ID_FILE_INFO_LIST_GET: u32 = 15;
/// Close a FileInfoList recording metadata search
pub const MSG_ID_FILE_INFO_LIST_CLOSE: u32 = 16;

/// Whether a message ID belongs to the privacy-sensitive FileInfoList flow.
pub(crate) fn is_file_info_list_message(msg_id: u32) -> bool {
matches!(
msg_id,
MSG_ID_FILE_INFO_LIST_OPEN | MSG_ID_FILE_INFO_LIST_GET | MSG_ID_FILE_INFO_LIST_CLOSE
)
}

/// PtzControl messages have this ID
pub const MSG_ID_PTZ_CONTROL: u32 = 18;
/// PTZ goto preset position
Expand Down
15 changes: 15 additions & 0 deletions crates/core/src/bc/samples/file_info_list_alias_datetime.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body>
<FileInfoList version="1.1">
<FileInfo>
<ID>/fixture/channel0/uppercase-id.mp4</ID>
<startTime>2026-01-02 03:04:05</startTime>
<endTime>20260102030406</endTime>
</FileInfo>
<FileInfo>
<id>/fixture/channel0/lowercase-id.mp4</id>
<startTime>2026/01/02T04:05:06</startTime>
<endTime><year>2026</year><month>1</month><day>2</day><hour>4</hour><minute>5</minute><second>7</second></endTime>
</FileInfo>
</FileInfoList>
</body>
2 changes: 2 additions & 0 deletions crates/core/src/bc/samples/file_info_list_generic_empty.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body />
8 changes: 8 additions & 0 deletions crates/core/src/bc/samples/file_info_list_open.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body>
<FileInfoList version="1.1">
<FileInfo>
<handle>17</handle>
</FileInfo>
</FileInfoList>
</body>
6 changes: 6 additions & 0 deletions crates/core/src/bc/samples/file_info_list_open_top_level.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body>
<FileInfoList version="1.1">
<handle>23</handle>
</FileInfoList>
</body>
24 changes: 24 additions & 0 deletions crates/core/src/bc/samples/file_info_list_page_direct.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body>
<FileInfoList version="1.1">
<FileInfo>
<Id>/fixture/channel0/recording-a.mp4</Id>
<name>recording-a.mp4</name>
<type>sched</type>
<recordType>people</recordType>
<alarmType>vehicle</alarmType>
<size>1234</size>
<startTime><year>2026</year><month>1</month><day>2</day><hour>1</hour><minute>2</minute><second>3</second></startTime>
<endTime><year>2026</year><month>1</month><day>2</day><hour>1</hour><minute>3</minute><second>4</second></endTime>
<firmwareSpecificField>ignored</firmwareSpecificField>
</FileInfo>
<FileInfo>
<fileName>/fixture/channel0/recording-b.mp4</fileName>
<alarmType>vehicle</alarmType>
<fileSize>5678</fileSize>
<startTime><year>2026</year><month>1</month><day>2</day><hour>2</hour><minute>3</minute><second>4</second></startTime>
<endTime><year>2026</year><month>1</month><day>2</day><hour>2</hour><minute>4</minute><second>5</second></endTime>
</FileInfo>
<bFinished>1</bFinished>
</FileInfoList>
</body>
8 changes: 8 additions & 0 deletions crates/core/src/bc/samples/file_info_list_page_empty.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body>
<FileInfoList version="1.1">
<FileInfo>
<handle>17</handle>
</FileInfo>
</FileInfoList>
</body>
17 changes: 17 additions & 0 deletions crates/core/src/bc/samples/file_info_list_page_nested.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<body>
<FileInfoList version="1.1">
<FileInfo>
<fileList>
<File>
<Id>/fixture/channel1/recording-c.mp4</Id>
<name>recording-c.mp4</name>
<type>md</type>
<startTime><year>2026</year><month>1</month><day>2</day><hour>3</hour><minute>4</minute><second>5</second></startTime>
<endTime><year>2026</year><month>1</month><day>2</day><hour>3</hour><minute>5</minute><second>6</second></endTime>
</File>
<bFinished>1</bFinished>
</fileList>
</FileInfo>
</FileInfoList>
</body>
Loading