diff --git a/config_parser.py b/config_parser.py index 9b16554..34d07fe 100644 --- a/config_parser.py +++ b/config_parser.py @@ -2,13 +2,14 @@ import os, sys from json import loads from dvrip import DVRIPCam from nvr_core import NVR +from nvr_types import File def app_dir(): return os.path.dirname(os.path.abspath(__file__)) -def load_config(): +def load_config(config_name): try: - path = os.path.join(app_dir(),"config.json") + path = os.path.join(app_dir(), config_name) print("Looking config file", path) with open(path, "r", encoding="utf8") as f: return loads(f.read()) @@ -17,11 +18,12 @@ def load_config(): sys.exit(1) class Recorder: - def __init__(self, address, port, username, password): + def __init__(self, address, port, username, password, name = ""): self.address = address self.port = int(port) self.username = username self.password = password + self.name = name @property def client(self) -> DVRIPCam: @@ -32,32 +34,39 @@ class Recorder: return NVR(self.client) def __str__(self) -> str: - return f"{self.address}:{self.port}" + if not self.name: + return f"{self.address}:{self.port}" + else: + return self.name class Config: - def __init__(self) -> None: - raw = load_config() + def __init__(self, config_name = "config.json") -> None: + raw = load_config(config_name) self.listen_address = raw.get("backend", {}).get("address", "0.0.0.0") self.listen_port = int(raw.get("backend", {}).get("port", "8080")) self.recorders = [] for raw_server in raw.get("recorders", []): - self.recorders.append(Recorder(raw_server.get("ip"), raw_server.get("port"), raw_server.get("user"), raw_server.get("password"))) + self.recorders.append(Recorder(raw_server.get("ip"), raw_server.get("port"), raw_server.get("user"), raw_server.get("password"), raw_server.get("name", ""))) if (self.recorders.__len__() == 0): print("Recorders not find") else: for recorder in self.recorders: print(recorder) - def getRecorder(self, index = 0): + def getRecorder(self, index = 0) -> Recorder: return self.recorders[index] + def getRecorders(self): + return [str(r) for r in self.recorders] + if __name__ == "__main__": print(app_dir()) config = Config() recorder: Recorder = config.getRecorder() nvr: NVR = recorder.nvr nvr.login() - print(nvr.download_test()) + f: File = File.from_b64("eyJiZWdpbiI6ICIyMDI0LTA4LTA2IDAyOjI3OjQxIiwgImVuZCI6ICIyMDI0LTA4LTA2IDAyOjI5OjQxIiwgIkRpc2tObyI6IDAsICJTZXJpYWxObyI6IDAsICJzaXplIjogMTI2NTMzNjMyLCAiZmlsZW5hbWUiOiAiL2lkZWEwLzIwMjQtMDgtMDYvMDAxLzAyLjI3LjQxLTAyLjI5LjQxW01dW0A3NjRlXVswXS5oMjY0IiwgImZpbGVuYW1lX2NsZWFyZWQiOiAiMDIuMjcuNDEtMDIuMjkuNDFfTV9fXzc2NGVfXzBfLmgyNjQiLCAiY2hhbm5lbCI6IDAsICJzdHJlYW0iOiAwfQ==") + nvr.save_file(f) nvr.logout() #client: DVRIPCam = recorder.client diff --git a/nvr_core.py b/nvr_core.py index 652e92b..08505a7 100644 --- a/nvr_core.py +++ b/nvr_core.py @@ -2,6 +2,9 @@ from datetime import datetime from dvrip import DVRIPCam from nvr_types import File as NvrFile +from nvr_types import list_local_files +from nvr_types import PRIMARY_STREAM, SECONDARY_STREAM +from nvr_types import H264 START = "2024-08-04 6:22:34" END = "2024-08-04 23:23:09" @@ -27,19 +30,34 @@ class NVR: def channels(self): return self.client.get_channel_titles() - def files(self, channel, start = None, end = None, ftype = "h264"): + def files(self, channel, start = None, end = None, ftype = H264, stype = SECONDARY_STREAM, json = False): if not start: start = date_today() if not end: - start = date_today(False) - for raw_file in self.client.list_local_files(startTime=start, endTime=end, filetype=ftype, channel=channel): - yield NvrFile(raw_file) + end = date_today(False) + print("Search files", start, end) + for raw_file in list_local_files(self.client, startTime=start, endTime=end, filetype=ftype, channel=channel, streamType=stype): + if json: + yield NvrFile(raw_file, channel, stype).json + else: + yield NvrFile(raw_file, channel, stype) + + def stream_file(self, file: NvrFile): + return file.generate_bytes(self.client) + + def save_file(self, file:NvrFile, savePath = "out.unknown"): + downloaded_bytes = 0 + with open(savePath, "wb") as f: + for byte in file.generate_bytes(self.client): + f.write(byte) + downloaded_bytes += len(byte) + print("\r", downloaded_bytes, "/", file.size) def download_test(self, filename = "testfile.unknown"): download_file = list(self.files(0))[0] downloaded_bytes = 0 - with open(filename, "wb") as f: - for byte in download_file.download_stream(self.client): - downloaded_bytes += len(byte) - f.write(byte) - print("\r", downloaded_bytes, "/", download_file.size) \ No newline at end of file + #with open(filename, "wb") as f: + # for byte in download_file.download_stream(self.client): + # downloaded_bytes += len(byte) + # f.write(byte) + # print("\r", downloaded_bytes, "/", download_file.size) \ No newline at end of file diff --git a/nvr_types.py b/nvr_types.py index a631506..22c6543 100644 --- a/nvr_types.py +++ b/nvr_types.py @@ -2,26 +2,54 @@ from datetime import datetime from dvrip import DVRIPCam import json import struct +import base64 NVR_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" +PRIMARY_STREAM = 0 +SECONDARY_STREAM = 1 +UNKNOWN_STREAM = 2 + +H264 = "h264" +H265 = "h265" +H265X = "h265x" + class File: - def __init__(self, data, channel = 0) -> None: - self.begin = datetime.strptime(data.get("BeginTime"), NVR_DATETIME_FORMAT) - self.end = datetime.strptime(data.get("EndTime"), NVR_DATETIME_FORMAT) - self.diskNo = data.get("DiskNo") + def __init__(self, data, channel = 0, stream = 0) -> None: + self.begin = datetime.strptime(data.get("BeginTime", data.get("begin")), NVR_DATETIME_FORMAT) + self.end = datetime.strptime(data.get("EndTime", data.get("end")), NVR_DATETIME_FORMAT) + self.DiskNo = data.get("DiskNo") self.SerialNo = data.get("SerialNo") - self.size = int(data.get("FileLength"), 0) - self.filename = data.get("FileName") + self.size = int(data.get("FileLength"), 0) * 1024 if "FileLength" in data else data.get("size", 0) + self.filename = data.get("FileName", data.get("filename")) + self.filename_cleared = self.filename.split("/")[-1].replace("[", "_").replace("]", "_").replace("@","_") self.channel = channel + self.stream = stream def __str__(self) -> str: - return self.filename + return f"{self.filename_cleared}/{self.size}/{self.channel}/{self.stream}" def __repr__(self) -> str: return self.__str__() - def download_stream(self, client:DVRIPCam, stream = 0, version = 0): + @property + def to_b64(self): + dict_obj = dict(self.__dict__) + dict_obj["begin"] = self.begin.strftime(NVR_DATETIME_FORMAT) + dict_obj["end"] = self.end.strftime(NVR_DATETIME_FORMAT) + return base64.b64encode(json.dumps(dict_obj).encode()).decode("utf8") + + @property + def json(self): + return {"filename": self.filename_cleared, "size": self.size, "b64": self.to_b64.replace("==", "")} + + @staticmethod + def from_b64(b64): + data = json.loads(base64.b64decode(b64).decode('utf-8')) + print(data) + return File(data, data.get("channel"), data.get("stream")) + + def generate_bytes(self, client:DVRIPCam, version = 0): #init request client.send( 1424, @@ -32,7 +60,7 @@ class File: "Parameter": { "PlayMode": "ByName", "FileName": self.filename, - "StreamType": stream, + "StreamType": self.stream, "Value": 0, "TransMode": "TCP", }, @@ -51,7 +79,7 @@ class File: "Parameter": { "PlayMode": "ByName", "FileName": self.filename, - "StreamType": stream, + "StreamType": self.stream, "Value": 0, "TransMode": "TCP", }, @@ -100,9 +128,9 @@ class File: msgid, len_data, ) = struct.unpack("BB2xII2xHI", data) - return self.get_file_stream(client, len_data, stream) + return self.get_file_stream(client, len_data) - def get_file_stream(self, client, first_chunk_size, stream): + def get_file_stream(self, client: DVRIPCam, first_chunk_size): yield client.receive_with_timeout(first_chunk_size) while True: header = client.receive_with_timeout(20) @@ -122,7 +150,7 @@ class File: "Parameter": { "FileName": self.filename, "PlayMode": "ByName", - "StreamType": stream, + "StreamType": self.stream, "TransMode": "TCP", "Channel": self.channel, "Value": 0, @@ -134,4 +162,75 @@ class File: ) yield b"" +def list_local_files(client: DVRIPCam, startTime, endTime, filetype, channel = 0, streamType = 0): + # 1440 OPFileQuery + result = [] + data = client.send( + 1440, + { + "Name": "OPFileQuery", + "OPFileQuery": { + "BeginTime": startTime, + "Channel": channel, + "DriverTypeMask": "0x0000FFFF", + "EndTime": endTime, + "Event": "*", + "StreamType": f"0x0000000{streamType}", + "Type": filetype, + }, + }, + ) + + if data == None: + client.logger.debug("Could not get files.") + raise ConnectionRefusedError("Could not get files") + + # When no file can be found + if data["Ret"] != 100: + client.logger.debug( + f"No files found for channel {channel} for this time range. Start: {startTime}, End: {endTime}" + ) + return [] + + # OPFileQuery only returns the first 64 items + # we therefore need to add the results to a list, modify the starttime with the begintime value of the last item we received and query again + # max number of results are 511 + result = data["OPFileQuery"] + + max_event = {"status": "init", "last_num_results": 0} + while max_event["status"] == "init" or max_event["status"] == "limit": + if max_event["status"] == "init": + max_event["status"] = "run" + while len(data["OPFileQuery"]) == 64 or max_event["status"] == "limit": + newStartTime = data["OPFileQuery"][-1]["BeginTime"] + data = client.send( + 1440, + { + "Name": "OPFileQuery", + "OPFileQuery": { + "BeginTime": newStartTime, + "Channel": channel, + "DriverTypeMask": "0x0000FFFF", + "EndTime": endTime, + "Event": "*", + "StreamType": "0x00000000", + "Type": filetype, + }, + }, + ) + result += data["OPFileQuery"] + max_event["status"] = "run" + + if len(result) % 511 == 0 or max_event["status"] == "limit": + client.logger.debug("Max number of events reached...") + if len(result) == max_event["last_num_results"]: + client.logger.debug( + "No new events since last run. All events queried" + ) + return result + + max_event["status"] = "limit" + max_event["last_num_results"] = len(result) + client.logger.debug(f"Found {len(result)} files.") + return result diff --git a/server.py b/server.py index e69de29..fe25682 100644 --- a/server.py +++ b/server.py @@ -0,0 +1,90 @@ +from fastapi import FastAPI, Response +from fastapi.responses import StreamingResponse +import uvicorn +import traceback + +from config_parser import Config as ConfigParser +from nvr_core import NVR +from nvr_types import File + +class Server: + app: FastAPI = FastAPI() + config: ConfigParser = ConfigParser() + + def __init__(self): + self.setup_events() + self.setup_routers() + + def setup_events(self): + @self.app.on_event('startup') + def on_startup(): + print("i am alive") + + def setup_routers(self): + @self.app.get("/api/recorders", status_code=200) + async def getRecorders(response: Response): + try: + return {"ok":True, "data":self.config.getRecorders()} + except Exception as e: + traceback.print_exc() + response.status_code = 400 + return {"ok":False, "error":e} + + #@self.app.get("/{recorder_index}") + #async def getRecorder(recorder_index:int): + # return self.config.getRecorder(recorder_index).nvr + + @self.app.get("/api/recorders/{recorder_index}/channels", status_code=200) + async def getRecorder(response: Response, recorder_index:int): + try: + nvr:NVR = self.config.getRecorder(recorder_index).nvr + nvr.login() + return {"ok":True, "data":nvr.channels} + except Exception as e: + traceback.print_exc() + response.status_code = 400 + return {"ok":False, "error":e} + finally: + nvr.logout() + + @self.app.get("/api/recorders/{recorder_index}/{channel}/{stream}") + async def getHistory(response: Response, recorder_index:int, channel: int, stream: int, start_date:str = None, end_date:str = None): + try: + nvr:NVR = self.config.getRecorder(recorder_index).nvr + nvr.login() + return {"ok":True, "data":list(nvr.files(channel, start_date, end_date, stype=stream, json=False))} + except Exception as e: + traceback.print_exc() + response.status_code = 400 + return {"ok":False, "error":e} + finally: + nvr.logout() + + @self.app.get("/api/recorders/{recorder_index}/file") + async def getFile(response: Response, recorder_index:int, b64:str): + try: + if len(b64) == 0: + response.status_code = 404 + return "" + nvr:NVR = self.config.getRecorder(recorder_index).nvr + nvr.login() + + file: File = File.from_b64(b64 + "==") + print("open") + return StreamingResponse(file.generate_bytes(nvr.client)) + except Exception as e: + traceback.print_exc() + response.status_code = 400 + return {"ok":False, "error":e} + finally: + nvr.logout() + + def run(self): + uvicorn.run( + self.app, + host=self.config.listen_address, + port=self.config.listen_port, + ) + +if __name__ == "__main__": + Server().run() \ No newline at end of file