diff options
Diffstat (limited to 'bin/ipcam_server.py')
-rwxr-xr-x | bin/ipcam_server.py | 205 |
1 files changed, 82 insertions, 123 deletions
diff --git a/bin/ipcam_server.py b/bin/ipcam_server.py index a9d6a0b..71d5ea1 100755 --- a/bin/ipcam_server.py +++ b/bin/ipcam_server.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import logging import os -import re import asyncio import time import shutil @@ -9,49 +8,41 @@ import __py_include import homekit.telegram.aio as telegram +from socket import gethostname from argparse import ArgumentParser from apscheduler.schedulers.asyncio import AsyncIOScheduler from asyncio import Lock -from homekit.config import config +from homekit.config import config as homekit_config, LinuxBoardsConfig +from homekit.util import Addr from homekit import http from homekit.database.sqlite import SQLiteBase -from homekit.camera import util as camutil +from homekit.camera import util as camutil, IpcamConfig +from homekit.camera.types import ( + TimeFilterType, + TelegramLinkType, + VideoContainerType +) +from homekit.camera.util import ( + get_recordings_path, + get_motion_path, + is_valid_recording_name, + datetime_from_filename +) -from enum import Enum from typing import Optional, Union, List, Tuple from datetime import datetime, timedelta from functools import cmp_to_key -class TimeFilterType(Enum): - FIX = 'fix' - MOTION = 'motion' - MOTION_START = 'motion_start' - - -class TelegramLinkType(Enum): - FRAGMENT = 'fragment' - ORIGINAL_FILE = 'original_file' - - -def valid_recording_name(filename: str) -> bool: - return filename.startswith('record_') and filename.endswith('.mp4') - - -def filename_to_datetime(filename: str) -> datetime: - filename = os.path.basename(filename).replace('record_', '').replace('.mp4', '') - return datetime.strptime(filename, datetime_format) - - -def get_all_cams() -> list: - return [cam for cam in config['camera'].keys()] +ipcam_config = IpcamConfig() +lbc_config = LinuxBoardsConfig() # ipcam database # -------------- -class IPCamServerDatabase(SQLiteBase): +class IpcamServerDatabase(SQLiteBase): SCHEMA = 4 def __init__(self, path=None): @@ -67,7 +58,7 @@ class IPCamServerDatabase(SQLiteBase): fix_time INTEGER NOT NULL, motion_time INTEGER NOT NULL )""") - for cam in config['camera'].keys(): + for cam in ipcam_config.get_all_cam_names_for_this_server(): self.add_camera(cam) if version < 2: @@ -135,7 +126,7 @@ class IPCamServerDatabase(SQLiteBase): # ipcam web api # ------------- -class IPCamWebServer(http.HTTPServer): +class IpcamWebServer(http.HTTPServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -146,16 +137,16 @@ class IPCamWebServer(http.HTTPServer): self.get('/api/timestamp/{name}/{type}', self.get_timestamp) self.get('/api/timestamp/all', self.get_all_timestamps) - self.post('/api/debug/migrate-mtimes', self.debug_migrate_mtimes) self.post('/api/debug/fix', self.debug_fix) self.post('/api/debug/cleanup', self.debug_cleanup) + self.post('/api/timestamp/{name}/{type}', self.set_timestamp) self.post('/api/motion/done/{name}', self.submit_motion) self.post('/api/motion/fail/{name}', self.submit_motion_failure) - self.get('/api/motion/params/{name}', self.get_motion_params) - self.get('/api/motion/params/{name}/roi', self.get_motion_roi_params) + # self.get('/api/motion/params/{name}', self.get_motion_params) + # self.get('/api/motion/params/{name}/roi', self.get_motion_roi_params) self.queue_lock = Lock() @@ -173,7 +164,7 @@ class IPCamWebServer(http.HTTPServer): files = get_recordings_files(camera, filter, limit) if files: - time = filename_to_datetime(files[len(files)-1]['name']) + time = datetime_from_filename(files[len(files)-1]['name']) db.set_timestamp(camera, TimeFilterType.MOTION_START, time) return self.ok({'files': files}) @@ -188,7 +179,7 @@ class IPCamWebServer(http.HTTPServer): if files: times_by_cam = {} for file in files: - time = filename_to_datetime(file['name']) + time = datetime_from_filename(file['name']) if file['cam'] not in times_by_cam or times_by_cam[file['cam']] < time: times_by_cam[file['cam']] = time for cam, time in times_by_cam.items(): @@ -200,14 +191,14 @@ class IPCamWebServer(http.HTTPServer): cam = int(req.match_info['name']) file = req.match_info['file'] - fullpath = os.path.join(config['camera'][cam]['recordings_path'], file) + fullpath = os.path.join(get_recordings_path(cam), file) if not os.path.isfile(fullpath): raise ValueError(f'file "{fullpath}" does not exists') return http.FileResponse(fullpath) async def camlist(self, req: http.Request): - return self.ok(config['camera']) + return self.ok(ipcam_config.get_all_cam_names_for_this_server()) async def submit_motion(self, req: http.Request): data = await req.post() @@ -216,7 +207,7 @@ class IPCamWebServer(http.HTTPServer): timecodes = data['timecodes'] filename = data['filename'] - time = filename_to_datetime(filename) + time = datetime_from_filename(filename) try: if timecodes != '': @@ -239,27 +230,10 @@ class IPCamWebServer(http.HTTPServer): message = data['message'] db.add_motion_failure(camera, filename, message) - db.set_timestamp(camera, TimeFilterType.MOTION, filename_to_datetime(filename)) + db.set_timestamp(camera, TimeFilterType.MOTION, datetime_from_filename(filename)) return self.ok() - async def debug_migrate_mtimes(self, req: http.Request): - written = {} - for cam in config['camera'].keys(): - confdir = os.path.join(os.getenv('HOME'), '.config', f'video-util-{cam}') - for time_type in TimeFilterType: - txt_file = os.path.join(confdir, f'{time_type.value}_mtime') - if os.path.isfile(txt_file): - with open(txt_file, 'r') as fd: - data = fd.read() - db.set_timestamp(cam, time_type, int(data.strip())) - - if cam not in written: - written[cam] = [] - written[cam].append(time_type) - - return self.ok({'written': written}) - async def debug_fix(self, req: http.Request): asyncio.ensure_future(fix_job()) return self.ok() @@ -280,26 +254,26 @@ class IPCamWebServer(http.HTTPServer): async def get_all_timestamps(self, req: http.Request): return self.ok(db.get_all_timestamps()) - async def get_motion_params(self, req: http.Request): - data = config['motion_params'][int(req.match_info['name'])] - lines = [ - f'threshold={data["threshold"]}', - f'min_event_length=3s', - f'frame_skip=2', - f'downscale_factor=3', - ] - return self.plain('\n'.join(lines)+'\n') - - async def get_motion_roi_params(self, req: http.Request): - data = config['motion_params'][int(req.match_info['name'])] - return self.plain('\n'.join(data['roi'])+'\n') + # async def get_motion_params(self, req: http.Request): + # data = config['motion_params'][int(req.match_info['name'])] + # lines = [ + # f'threshold={data["threshold"]}', + # f'min_event_length=3s', + # f'frame_skip=2', + # f'downscale_factor=3', + # ] + # return self.plain('\n'.join(lines)+'\n') + # + # async def get_motion_roi_params(self, req: http.Request): + # data = config['motion_params'][int(req.match_info['name'])] + # return self.plain('\n'.join(data['roi'])+'\n') @staticmethod def _getset_timestamp_params(req: http.Request, need_time=False): values = [] cam = int(req.match_info['name']) - assert cam in config['camera'], 'invalid camera' + assert cam in ipcam_config.get_all_cam_names_for_this_server(), 'invalid camera' values.append(cam) values.append(TimeFilterType(req.match_info['type'])) @@ -307,7 +281,7 @@ class IPCamWebServer(http.HTTPServer): if need_time: time = req.query['time'] if time.startswith('record_'): - time = filename_to_datetime(time) + time = datetime_from_filename(time) elif time.isnumeric(): time = int(time) else: @@ -322,30 +296,22 @@ class IPCamWebServer(http.HTTPServer): def open_database(database_path: str): global db - db = IPCamServerDatabase(database_path) + db = IpcamServerDatabase(database_path) # update cams list in database, if needed - cams = db.get_all_timestamps().keys() - for cam in config['camera']: - if cam not in cams: + stored_cams = db.get_all_timestamps().keys() + for cam in ipcam_config.get_all_cam_names_for_this_server(): + if cam not in stored_cams: db.add_camera(cam) -def get_recordings_path(cam: int) -> str: - return config['camera'][cam]['recordings_path'] - - -def get_motion_path(cam: int) -> str: - return config['camera'][cam]['motion_path'] - - def get_recordings_files(cam: Optional[int] = None, time_filter_type: Optional[TimeFilterType] = None, limit=0) -> List[dict]: from_time = 0 to_time = int(time.time()) - cams = [cam] if cam is not None else get_all_cams() + cams = [cam] if cam is not None else ipcam_config.get_all_cam_names_for_this_server() files = [] for cam in cams: if time_filter_type: @@ -362,7 +328,7 @@ def get_recordings_files(cam: Optional[int] = None, 'name': file, 'size': os.path.getsize(os.path.join(recdir, file))} for file in os.listdir(recdir) - if valid_recording_name(file) and from_time < filename_to_datetime(file) <= to_time] + if is_valid_recording_name(file) and from_time < datetime_from_filename(file) <= to_time] cam_files.sort(key=lambda file: file['name']) if cam_files: @@ -382,7 +348,7 @@ def get_recordings_files(cam: Optional[int] = None, async def process_fragments(camera: int, filename: str, fragments: List[Tuple[int, int]]) -> None: - time = filename_to_datetime(filename) + time = datetime_from_filename(filename) rec_dir = get_recordings_path(camera) motion_dir = get_motion_path(camera) @@ -392,8 +358,8 @@ async def process_fragments(camera: int, for fragment in fragments: start, end = fragment - start -= config['motion']['padding'] - end += config['motion']['padding'] + start -= ipcam_config['motion_padding'] + end += ipcam_config['motion_padding'] if start < 0: start = 0 @@ -408,14 +374,14 @@ async def process_fragments(camera: int, start_pos=start, duration=duration) - if fragments and 'telegram' in config['motion'] and config['motion']['telegram']: + if fragments and ipcam_config['motion_telegram']: asyncio.ensure_future(motion_notify_tg(camera, filename, fragments)) async def motion_notify_tg(camera: int, filename: str, fragments: List[Tuple[int, int]]): - dt_file = filename_to_datetime(filename) + dt_file = datetime_from_filename(filename) fmt = '%H:%M:%S' text = f'Camera: <b>{camera}</b>\n' @@ -423,8 +389,8 @@ async def motion_notify_tg(camera: int, text += _tg_links(TelegramLinkType.ORIGINAL_FILE, camera, filename) for start, end in fragments: - start -= config['motion']['padding'] - end += config['motion']['padding'] + start -= ipcam_config['motion_padding'] + end += ipcam_config['motion_padding'] if start < 0: start = 0 @@ -446,7 +412,7 @@ def _tg_links(link_type: TelegramLinkType, camera: int, file: str) -> str: links = [] - for link_name, link_template in config['telegram'][f'{link_type.value}_url_templates']: + for link_name, link_template in ipcam_config[f'{link_type.value}_url_templates']: link = link_template.replace('{camera}', str(camera)).replace('{file}', file) links.append(f'<a href="{link}">{link_name}</a>') return ' '.join(links) @@ -462,7 +428,7 @@ async def fix_job() -> None: try: fix_job_running = True - for cam in config['camera'].keys(): + for cam in ipcam_config.get_all_cam_names_for_this_server(): files = get_recordings_files(cam, TimeFilterType.FIX) if not files: logger.debug(f'fix_job: no files for camera {cam}') @@ -473,7 +439,7 @@ async def fix_job() -> None: for file in files: fullpath = os.path.join(get_recordings_path(cam), file['name']) await camutil.ffmpeg_recreate(fullpath) - timestamp = filename_to_datetime(file['name']) + timestamp = datetime_from_filename(file['name']) if timestamp: db.set_timestamp(cam, TimeFilterType.FIX, timestamp) @@ -482,21 +448,9 @@ async def fix_job() -> None: async def cleanup_job() -> None: - def fn2dt(name: str) -> datetime: - name = os.path.basename(name) - - if name.startswith('record_'): - return datetime.strptime(re.match(r'record_(.*?)\.mp4', name).group(1), datetime_format) - - m = re.match(rf'({datetime_format_re})__{datetime_format_re}\.mp4', name) - if m: - return datetime.strptime(m.group(1), datetime_format) - - raise ValueError(f'unrecognized filename format: {name}') - def compare(i1: str, i2: str) -> int: - dt1 = fn2dt(i1) - dt2 = fn2dt(i2) + dt1 = datetime_from_filename(i1) + dt2 = datetime_from_filename(i2) if dt1 < dt2: return -1 @@ -516,18 +470,19 @@ async def cleanup_job() -> None: cleanup_job_running = True gb = float(1 << 30) - for storage in config['storages']: + disk_number = 0 + for storage in lbc_config.get_board_disks(gethostname()): + disk_number += 1 if os.path.exists(storage['mountpoint']): total, used, free = shutil.disk_usage(storage['mountpoint']) free_gb = free // gb - if free_gb < config['cleanup_min_gb']: - # print(f"{storage['mountpoint']}: free={free}, free_gb={free_gb}") + if free_gb < ipcam_config['cleanup_min_gb']: cleaned = 0 files = [] - for cam in storage['cams']: - for _dir in (config['camera'][cam]['recordings_path'], config['camera'][cam]['motion_path']): + for cam in ipcam_config.get_all_cam_names_for_this_server(filter_by_disk=disk_number): + for _dir in (get_recordings_path(cam), get_motion_path(cam)): files += list(map(lambda file: os.path.join(_dir, file), os.listdir(_dir))) - files = list(filter(lambda path: os.path.isfile(path) and path.endswith('.mp4'), files)) + files = list(filter(lambda path: os.path.isfile(path) and path.endswith(tuple([f'.{t.value}' for t in VideoContainerType])), files)) files.sort(key=cmp_to_key(compare)) for file in files: @@ -537,7 +492,7 @@ async def cleanup_job() -> None: cleaned += size except OSError as e: logger.exception(e) - if (free + cleaned) // gb >= config['cleanup_min_gb']: + if (free + cleaned) // gb >= ipcam_config['cleanup_min_gb']: break else: logger.error(f"cleanup_job: {storage['mountpoint']} not found") @@ -550,8 +505,8 @@ cleanup_job_running = False datetime_format = '%Y-%m-%d-%H.%M.%S' datetime_format_re = r'\d{4}-\d{2}-\d{2}-\d{2}\.\d{2}.\d{2}' -db: Optional[IPCamServerDatabase] = None -server: Optional[IPCamWebServer] = None +db: Optional[IpcamServerDatabase] = None +server: Optional[IpcamWebServer] = None logger = logging.getLogger(__name__) @@ -562,7 +517,7 @@ if __name__ == '__main__': parser = ArgumentParser() parser.add_argument('--listen', type=str, required=True) parser.add_argument('--database-path', type=str, required=True) - arg = config.load_app(no_config=True, parser=parser) + arg = homekit_config.load_app(no_config=True, parser=parser) open_database(arg.database_path) @@ -570,10 +525,14 @@ if __name__ == '__main__': try: scheduler = AsyncIOScheduler(event_loop=loop) - if config['fix_enabled']: - scheduler.add_job(fix_job, 'interval', seconds=config['fix_interval'], misfire_grace_time=None) - - scheduler.add_job(cleanup_job, 'interval', seconds=config['cleanup_interval'], misfire_grace_time=None) + if ipcam_config['fix_enabled']: + scheduler.add_job(fix_job, 'interval', + seconds=ipcam_config['fix_interval'], + misfire_grace_time=None) + + scheduler.add_job(cleanup_job, 'interval', + seconds=ipcam_config['cleanup_interval'], + misfire_grace_time=None) scheduler.start() except KeyError: pass @@ -581,5 +540,5 @@ if __name__ == '__main__': asyncio.ensure_future(fix_job()) asyncio.ensure_future(cleanup_job()) - server = IPCamWebServer(config.get_addr('server.listen')) + server = IpcamWebServer(Addr.fromstring(arg.listen)) server.run() |