From 58cd830a6c10be870a247a5c51cc32052e784f39 Mon Sep 17 00:00:00 2001 From: vklimk <111297622+vklimk@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:30:20 +0300 Subject: [PATCH] Change dvrip.py: - add get_channel_titles() - add get_channel_statuses() - change send_custom() and get_file(): get the first chunk size from netip header instead of hardcoded - change list_local_files(): allow list files for any channel, review error processing logic Add NVR class Add NVRVideoDownloader application which allows downloading video-files from NVR --- NVR.py | 94 +++++++++++++++++++++++++++++++++++++++++ NVRVideoDownloader.json | 11 +++++ NVRVideoDownloader.py | 89 ++++++++++++++++++++++++++++++++++++++ dvrip.py | 44 +++++++++---------- 4 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 NVR.py create mode 100644 NVRVideoDownloader.json create mode 100644 NVRVideoDownloader.py diff --git a/NVR.py b/NVR.py new file mode 100644 index 0000000..2f18ebd --- /dev/null +++ b/NVR.py @@ -0,0 +1,94 @@ +from time import sleep, monotonic +from dvrip import DVRIPCam, SomethingIsWrongWithCamera +from pathlib import Path +import logging + + +class NVR: + nvr = None + logger = None + + def __init__(self, host_ip, user, password, logger): + self.logger = logger + self.nvr = DVRIPCam( + host_ip, + user=user, + password=password, + ) + if logger.level <= logging.DEBUG: + self.nvr.debug() + + def login(self): + try: + self.logger.info(f"Connecting to NVR...") + self.nvr.login() + self.logger.info("Successfuly connected to NVR.") + return + except SomethingIsWrongWithCamera: + self.logger.error("Can't connect to NVR") + self.nvr.close() + + def logout(self): + self.nvr.close() + + def get_channel_statuses(self): + channel_statuses = self.nvr.get_channel_statuses() + if 'Ret' in channel_statuses: + return None + + channel_titles = self.nvr.get_channel_titles() + if 'Ret' in channel_titles: + return None + + for i in range(min(len(channel_statuses), len(channel_titles))): + channel_statuses[i]['Title'] = channel_titles[i] + channel_statuses[i]['Channel'] = i + + return [c for c in channel_statuses if c['Status'] != ''] + + def get_local_files(self, channel, start, end, filetype): + return self.nvr.list_local_files(start, end, filetype, channel) + + def generateTargetFileName(self, filename): + # My NVR's filename example: /idea0/2023-11-19/002/05.38.58-05.39.34[M][@69f17][0].h264 + # You should check file names in your NVR and review the transformation + filenameSplit = filename.replace("][", "/").replace("[", "/").replace("]", "/").split("/") + return f"{filenameSplit[3]}_{filenameSplit[2]}_{filenameSplit[4]}{filenameSplit[-1]}" + + def save_files(self, download_dir, files): + self.logger.info(f"Files downloading: start") + + size_to_download = sum(int(f['FileLength'], 0) for f in files) + + for file in files: + target_file_name = self.generateTargetFileName(file["FileName"]) + target_file_path = f"{download_dir}/{target_file_name}" + + size = int(file['FileLength'], 0) + size_to_download -= size + + if Path(f"{target_file_path}").is_file(): + self.logger.info(f" {target_file_name} file already exists, skipping download") + continue + + self.logger.info(f" {target_file_name} [{size/1024:.1f} MBytes] downloading...") + time_dl = monotonic() + self.nvr.download_file( + file["BeginTime"], file["EndTime"], file["FileName"], target_file_path + ) + time_dl = monotonic() - time_dl + speed = size / time_dl + self.logger.info(f" Done [{speed:.1f} KByte/s] {size_to_download/1024:.1f} MBytes more to download") + + self.logger.info(f"Files downloading: done") + + def list_files(self, files): + self.logger.info(f"Files listing: start") + + for file in files: + target_file_name = self.generateTargetFileName(file["FileName"]) + + size = int(file['FileLength'], 0) + self.logger.info(f" {target_file_name} [{size/1024:.1f} MBytes]") + + self.logger.info(f"Files listing: end") diff --git a/NVRVideoDownloader.json b/NVRVideoDownloader.json new file mode 100644 index 0000000..4a84d2a --- /dev/null +++ b/NVRVideoDownloader.json @@ -0,0 +1,11 @@ +{ + "host_ip": "10.0.0.8", + "user": "admin", + "password": "mypassword", + "channel": 0, + "download_dir": "./download", + "start": "2023-11-19 6:22:34", + "end": "2023-11-19 6:23:09", + "just_list_files": false, + "log_level": "INFO" +} diff --git a/NVRVideoDownloader.py b/NVRVideoDownloader.py new file mode 100644 index 0000000..0cafa19 --- /dev/null +++ b/NVRVideoDownloader.py @@ -0,0 +1,89 @@ +from pathlib import Path +import os +import json +import logging +from collections import namedtuple +from NVR import NVR + + +def init_logger(log_level): + logger = logging.getLogger(__name__) + logger.setLevel(log_level) + ch = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") + ch.setFormatter(formatter) + logger.addHandler(ch) + return logger + + +def load_config(): + def config_decoder(config_dict): + return namedtuple("X", config_dict.keys())(*config_dict.values()) + + config_path = os.environ.get("NVRVIDEODOWNLOADER_CFG") + + if config_path is None or not Path(config_path).exists(): + config_path = "NVRVideoDownloader.json" + + if Path(config_path).exists(): + with open(config_path, "r") as file: + return json.loads(file.read(), object_hook=config_decoder) + + return { + "host_ip": os.environ.get("IP_ADDRESS"), + "user": os.environ.get("USER"), + "password": os.environ.get("PASSWORD"), + "channel": os.environ.get("CHANNEL"), + "download_dir": os.environ.get("DOWNLOAD_DIR"), + "start": os.environ.get("START"), + "end": os.environ.get("END"), + "just_list_files": os.environ.get("DUMP_LOCAL_FILES").lower() in ["true", "1", "y", "yes"], + "log_level": "INFO" + } + + +def main(): + config = load_config() + logger = init_logger(config.log_level) + channel = config.channel; + start = config.start + end = config.end + just_list_files = config.just_list_files; + + nvr = NVR(config.host_ip, config.user, config.password, logger) + + try: + nvr.login() + + channel_statuses = nvr.get_channel_statuses() + if channel_statuses: + channel_statuses_short = [{f"{c['Channel']}:{c['Title']}({c['ChnName']})"} + for c in channel_statuses if c['Status'] != 'NoConfig'] + logger.info(f"Configured channels in NVR: {channel_statuses_short}") + + videos = nvr.get_local_files(channel, start, end, "h264") + if videos: + size = sum(int(f['FileLength'], 0) for f in videos) + logger.info(f"Video files found: {len(videos)}. Total size: {size/1024:.1f}M") + Path(config.download_dir).parent.mkdir( + parents=True, exist_ok=True + ) + if just_list_files: + nvr.list_files(videos) + else: + nvr.save_files(config.download_dir, videos) + else: + logger.info(f"No video files found") + + nvr.logout() + except ConnectionRefusedError: + logger.error(f"Connection can't be established or got disconnected") + except TypeError as e: + print(e) + logger.error(f"Error while downloading a file") + except KeyError: + logger.error(f"Error while getting the file list") + + +if __name__ == "__main__": + main() diff --git a/dvrip.py b/dvrip.py index 2019270..b234ca2 100644 --- a/dvrip.py +++ b/dvrip.py @@ -198,7 +198,7 @@ class DVRIPCam(object): return reply def send_custom( - self, msg, data={}, wait_response=True, download=False, size=None, version=0 + self, msg, data={}, wait_response=True, download=False, version=0 ): if self.socket is None: return {"Ret": 101} @@ -245,9 +245,9 @@ class DVRIPCam(object): reply = None if download: - reply = self.get_file() - elif size: - reply = self.get_specific_size(size) + reply = self.get_file(len_data) + else: + reply = self.get_specific_size(len_data) self.busy.release() return reply @@ -764,20 +764,10 @@ class DVRIPCam(object): return data vprint(f"Upgraded {data['Ret']}%") - def get_file(self): - # recorded with 15 (0x0F) fps - + def get_file(self, first_chunk_size): buf = bytearray() - data = self.receive_with_timeout(16) - ( - static, - dyn1, - dyn2, - len_data, - ) = struct.unpack("IIII", data) - file_length = len_data - - data = self.receive_with_timeout(8176) + + data = self.receive_with_timeout(first_chunk_size) buf.extend(data) while True: @@ -925,7 +915,7 @@ class DVRIPCam(object): def stop_monitor(self): self.monitoring = False - def list_local_files(self, startTime, endTime, filetype): + def list_local_files(self, startTime, endTime, filetype, channel = 0): # 1440 OPFileQuery result = [] data = self.send( @@ -934,7 +924,7 @@ class DVRIPCam(object): "Name": "OPFileQuery", "OPFileQuery": { "BeginTime": startTime, - "Channel": 0, + "Channel": channel, "DriverTypeMask": "0x0000FFFF", "EndTime": endTime, "Event": "*", @@ -944,14 +934,14 @@ class DVRIPCam(object): }, ) - if data == None or data["Ret"] != 100: + if data == None: self.logger.debug("Could not get files.") raise ConnectionRefusedError("Could not get files") - # When no file can be found for the query OPFileQuery is None - if data["OPFileQuery"] == None: + # When no file can be found + if data["Ret"] != 100: self.logger.debug( - f"No files found for this range. Start: {startTime}, End: {endTime}" + f"No files found for channel {channel} for this time range. Start: {startTime}, End: {endTime}" ) return [] @@ -972,7 +962,7 @@ class DVRIPCam(object): "Name": "OPFileQuery", "OPFileQuery": { "BeginTime": newStartTime, - "Channel": 0, + "Channel": channel, "DriverTypeMask": "0x0000FFFF", "EndTime": endTime, "Event": "*", @@ -1122,3 +1112,9 @@ class DVRIPCam(object): }, ) return None + + def get_channel_titles(self): + return self.get_command("ChannelTitle", 1048) + + def get_channel_statuses(self): + return self.get_info("NetWork.ChnStatus")