aboutsummaryrefslogtreecommitdiff
path: root/include/py/homekit/media/storage.py
diff options
context:
space:
mode:
Diffstat (limited to 'include/py/homekit/media/storage.py')
-rw-r--r--include/py/homekit/media/storage.py210
1 files changed, 210 insertions, 0 deletions
diff --git a/include/py/homekit/media/storage.py b/include/py/homekit/media/storage.py
new file mode 100644
index 0000000..dd74ff8
--- /dev/null
+++ b/include/py/homekit/media/storage.py
@@ -0,0 +1,210 @@
+import os
+import re
+import shutil
+import logging
+
+from typing import Optional, Union, List
+from datetime import datetime
+from ..util import strgen
+
+logger = logging.getLogger(__name__)
+
+
+# record file
+# -----------
+
+class RecordFile:
+ EXTENSION = None
+
+ start_time: Optional[datetime]
+ stop_time: Optional[datetime]
+ record_id: Optional[int]
+ name: str
+ file_id: Optional[str]
+ remote: bool
+ remote_filesize: int
+ storage_root: str
+
+ human_date_dmt = '%d.%m.%y'
+ human_time_fmt = '%H:%M:%S'
+
+ @staticmethod
+ def create(filename: str, *args, **kwargs):
+ if filename.endswith(f'.{SoundRecordFile.EXTENSION}'):
+ return SoundRecordFile(filename, *args, **kwargs)
+ elif filename.endswith(f'.{CameraRecordFile.EXTENSION}'):
+ return CameraRecordFile(filename, *args, **kwargs)
+ else:
+ raise RuntimeError(f'unsupported file extension: {filename}')
+
+ def __init__(self, filename: str, remote=False, remote_filesize=None, storage_root='/'):
+ if self.EXTENSION is None:
+ raise RuntimeError('this is abstract class')
+
+ self.name = filename
+ self.storage_root = storage_root
+
+ self.remote = remote
+ self.remote_filesize = remote_filesize
+
+ m = re.match(r'^(\d{6}-\d{6})_(\d{6}-\d{6})_id(\d+)(_\w+)?\.'+self.EXTENSION+'$', filename)
+ if m:
+ self.start_time = datetime.strptime(m.group(1), RecordStorage.time_fmt)
+ self.stop_time = datetime.strptime(m.group(2), RecordStorage.time_fmt)
+ self.record_id = int(m.group(3))
+ self.file_id = (m.group(1) + '_' + m.group(2)).replace('-', '_')
+ else:
+ logger.warning(f'unexpected filename: {filename}')
+ self.start_time = None
+ self.stop_time = None
+ self.record_id = None
+ self.file_id = None
+
+ @property
+ def path(self):
+ if self.remote:
+ return RuntimeError('remote recording, can\'t get real path')
+
+ return os.path.realpath(os.path.join(
+ self.storage_root, self.name
+ ))
+
+ @property
+ def start_humantime(self) -> str:
+ if self.start_time is None:
+ return '?'
+ fmt = f'{RecordFile.human_date_dmt} {RecordFile.human_time_fmt}'
+ return self.start_time.strftime(fmt)
+
+ @property
+ def stop_humantime(self) -> str:
+ if self.stop_time is None:
+ return '?'
+ fmt = RecordFile.human_time_fmt
+ if self.start_time.date() != self.stop_time.date():
+ fmt = f'{RecordFile.human_date_dmt} {fmt}'
+ return self.stop_time.strftime(fmt)
+
+ @property
+ def start_unixtime(self) -> int:
+ if self.start_time is None:
+ return 0
+ return int(self.start_time.timestamp())
+
+ @property
+ def stop_unixtime(self) -> int:
+ if self.stop_time is None:
+ return 0
+ return int(self.stop_time.timestamp())
+
+ @property
+ def filesize(self):
+ if self.remote:
+ if self.remote_filesize is None:
+ raise RuntimeError('file is remote and remote_filesize is not set')
+ return self.remote_filesize
+ return os.path.getsize(self.path)
+
+ def __dict__(self) -> dict:
+ return {
+ 'start_unixtime': self.start_unixtime,
+ 'stop_unixtime': self.stop_unixtime,
+ 'filename': self.name,
+ 'filesize': self.filesize,
+ 'fileid': self.file_id,
+ 'record_id': self.record_id or 0,
+ }
+
+
+class PseudoRecordFile(RecordFile):
+ EXTENSION = 'null'
+
+ def __init__(self):
+ super().__init__('pseudo.null')
+
+ @property
+ def filesize(self):
+ return 0
+
+
+class SoundRecordFile(RecordFile):
+ EXTENSION = 'mp3'
+
+
+class CameraRecordFile(RecordFile):
+ EXTENSION = 'mp4'
+
+
+# record storage
+# --------------
+
+class RecordStorage:
+ EXTENSION = None
+
+ time_fmt = '%d%m%y-%H%M%S'
+
+ def __init__(self, root: str):
+ if self.EXTENSION is None:
+ raise RuntimeError('this is abstract class')
+
+ self.root = root
+
+ def getfiles(self, as_objects=False) -> Union[List[str], List[RecordFile]]:
+ files = []
+ for name in os.listdir(self.root):
+ path = os.path.join(self.root, name)
+ if os.path.isfile(path) and name.endswith(f'.{self.EXTENSION}'):
+ files.append(name if not as_objects else RecordFile.create(name, storage_root=self.root))
+ return files
+
+ def find(self, file_id: str) -> Optional[RecordFile]:
+ for name in os.listdir(self.root):
+ if os.path.isfile(os.path.join(self.root, name)) and name.endswith(f'.{self.EXTENSION}'):
+ item = RecordFile.create(name, storage_root=self.root)
+ if item.file_id == file_id:
+ return item
+ return None
+
+ def purge(self):
+ files = self.getfiles()
+ if files:
+ logger = logging.getLogger(self.__name__)
+ for f in files:
+ try:
+ path = os.path.join(self.root, f)
+ logger.debug(f'purge: deleting {path}')
+ os.unlink(path)
+ except OSError as exc:
+ logger.exception(exc)
+
+ def delete(self, file: RecordFile):
+ os.unlink(file.path)
+
+ def save(self,
+ fn: str,
+ record_id: int,
+ start_time: int,
+ stop_time: int) -> RecordFile:
+
+ start_time_s = datetime.fromtimestamp(start_time).strftime(self.time_fmt)
+ stop_time_s = datetime.fromtimestamp(stop_time).strftime(self.time_fmt)
+
+ dst_fn = f'{start_time_s}_{stop_time_s}_id{record_id}'
+ if os.path.exists(os.path.join(self.root, dst_fn)):
+ dst_fn += strgen(4)
+ dst_fn += f'.{self.EXTENSION}'
+ dst_path = os.path.join(self.root, dst_fn)
+
+ shutil.move(fn, dst_path)
+ return RecordFile.create(dst_fn, storage_root=self.root)
+
+
+class SoundRecordStorage(RecordStorage):
+ EXTENSION = 'mp3'
+
+
+class ESP32CameraRecordStorage(RecordStorage):
+ EXTENSION = 'jpg' # not used anyway
+
+ def save(self, *args, **kwargs):
+ return PseudoRecordFile() \ No newline at end of file