From 5963c5bf22821376e37a006bcf73dc3af2e03d5b Mon Sep 17 00:00:00 2001
From: gsd <mamu@ebal.net>
Date: Wed, 7 Aug 2024 00:00:55 +0300
Subject: [PATCH] server init

---
 config_parser.py |  27 ++++++----
 nvr_core.py      |  36 ++++++++++----
 nvr_types.py     | 125 ++++++++++++++++++++++++++++++++++++++++++-----
 server.py        |  90 ++++++++++++++++++++++++++++++++++
 4 files changed, 247 insertions(+), 31 deletions(-)

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