class SplPlugin(SplThread): plugin_id = 'record_hd' plugin_names = ['HD Recorder'] def __init__(self, modref): ''' creates the plugin ''' self.modref = modref super().__init__(modref.message_handler, self) modref.message_handler.add_event_handler(self.plugin_id, 0, self.event_listener) modref.message_handler.add_query_handler(self.plugin_id, 0, self.query_handler) self.runFlag = True # plugin specific stuff self.origin_dir = os.path.dirname(__file__) self.config = JsonStorage(os.path.join(self.origin_dir, "config.json"), { 'path': '/var/schnipsl', 'www-root': 'http://schnipsl:9092/' }) self.records = JsonStorage( os.path.join(self.origin_dir, "records.json"), {}) self.record_threats = { } # we need to store the thread pointers seperate from self.records, as we can't store them as json self.last_recorded_time = 0 # remembers how long the last recording action is away def event_listener(self, queue_event): if queue_event.type == defaults.TIMER_RECORD_REQUEST: self.timer_record_request(queue_event.data) # for further pocessing, do not forget to return the queue event return queue_event def query_handler(self, queue_event, max_result_count): ''' try to send simulated answers ''' # print("hd_recorder query handler", queue_event.type, queue_event.user, max_result_count) if queue_event.type == defaults.QUERY_MOVIE_ID: new_uri = queue_event.params for record_movie in self.records.read( 'all', {}).values(): # 'all': read the whole config if record_movie['new_uri'] == new_uri: return [ Movie(source=self.plugin_names[0], source_type=defaults.MOVIE_TYPE_RECORD, provider=self.plugin_names[0], category=record_movie['category'], title=record_movie['title'], timestamp=record_movie['timestamp'], duration=record_movie['duration'], description=record_movie['description'], url=record_movie['new_url']) ] return [] def _run(self): ''' starts the server ''' scheduler = Scheduler([(self.check_for_records, 10), (self.cleanup_records, 60)]) while self.runFlag: scheduler.execute() time.sleep(2) def _stop(self): self.runFlag = False def timer_record_request(self, data): uri = data['uri'] uuid = data['uuid'] movie_info_list = self.modref.message_handler.query( Query(None, defaults.QUERY_MOVIE_ID, uri)) if movie_info_list: movie = movie_info_list[0] uri = movie.uri() # do we have that record request already existing_record = self.records.read(uri) if not existing_record: path = urlparse(movie.url).path ext = os.path.splitext(path)[1] if ext.lower() == '.ts': ext = '.mp4' uri_base64 = base64_encode(uri) file_path = os.path.join(self.config.read('path'), uri_base64 + ext) if movie.source_type == defaults.MOVIE_TYPE_RECORD: self.records.write( uri, { # in case of a record we set start and duration to 0 to indicate that the recording can start immediadly & has no duration 'record_starttime': 0, 'record_duration': 0, 'provider': movie.provider, 'category': movie.category, 'title': movie.title, 'timestamp': movie.timestamp, 'duration': movie.duration, 'description': movie.description, 'url': movie.url, 'uri': uri, 'new_uri': self.plugin_names[0] + ':' + uri_base64, 'new_url': self.config.read('www-root') + uri_base64 + ext, 'uuid': uuid, 'ext': ext, 'file_path': file_path, 'state': record_states.WAIT_FOR_RECORDING }) if movie.source_type == defaults.MOVIE_TYPE_STREAM: # recording a stream with a duration of 0 is a very bad idea, because it would never stop.. if movie.duration: self.records.write( uri, { 'record_starttime': int(movie.timestamp), 'record_duration': movie.duration, 'category': movie.category, 'title': movie.title, 'timestamp': movie.timestamp, 'duration': movie.duration, 'description': movie.description, 'url': movie.url, 'uri': uri, 'new_uri': self.plugin_names[0] + ':' + uri_base64, 'new_url': self.config.read('www-root') + uri_base64 + ext, 'uuid': uuid, 'ext': ext, 'file_path': file_path, 'state': record_states.WAIT_FOR_RECORDING }) def check_for_records(self): act_time = time.time() for uri, record in self.records.config.items(): if record['state'] == record_states.WAIT_FOR_RECORDING: if record[ 'record_duration'] == 0: # this is a record, which can be recorded immediadly record['state'] = record_states.ACTUAL_RECORDING self.records.write(uri, record) self.recording(record) continue # something went wrong, the record time was in the past if record['record_starttime'] + record[ 'record_duration'] < act_time: record['state'] = record_states.RECORDING_FAILED self.records.write(uri, record) self.deploy_record_result(record, False) continue # it's time to start if record['record_starttime'] - self.config.read( 'padding_secs', 300 ) <= act_time and record['record_starttime'] + record[ 'record_duration'] > act_time: record['state'] = record_states.ACTUAL_RECORDING self.records.write(uri, record) self.recording(record) continue def cleanup_records(self): records_to_delete = {} act_time = time.time() # request which movies are still in the UI list valid_movieuri_list = self.modref.message_handler.query( Query(None, defaults.QUERY_VALID_MOVIE_RECORDS, {'source': self.plugin_names[0]})) for uri, record in self.records.config.items(): if uri in self.record_threats: # recording is finished, so deploy the result if not self.record_threats[uri].is_alive(): del (self.record_threats[uri]) # we destroy the thread self.deploy_record_result( record, record['state'] == record_states.RECORDING_FINISHED) self.last_recorded_time = act_time if self.last_recorded_time > act_time - 5 * 60: return # don't do any delete action if the last record is just 5 mins ago to give the UI some time to adapt the new movie if record['state'] == record_states.RECORDING_FINISHED or record[ 'state'] == record_states.RECORDING_FAILED: new_uri = record['new_uri'] print('Record on disk:', new_uri) if not new_uri in valid_movieuri_list: records_to_delete[uri] = record # some debug output for uri in valid_movieuri_list: print('recoder uri:', uri) if records_to_delete: # go through the list of records to be deleted for uri, record in records_to_delete.items(): # delete the file file_path = record['file_path'] print('try to delete file', file_path) if os.path.exists(file_path): try: os.remove(file_path) del (self.records.config[uri]) self.records.save() print('file deleted', file_path) except Exception as ex: print("Cant delete record file {0}. Error: {1}".format( file_path, str(ex))) else: # remove the entry print('remove the entry', uri) del (self.records.config[uri]) self.records.save() def deploy_record_result(self, record, sucess): self.modref.message_handler.queue_event( None, defaults.TIMER_RECORD_RESULT, { 'new_uri': record['new_uri'], 'new_url': record['new_url'], 'uuid': record['uuid'], 'sucess': sucess }) def recording(self, record): uri = record['uri'] print('try to record ', uri) threat = threading.Thread(target=record_thread, args=(record, self.config.read('padding_secs', 300))) self.record_threats[uri] = threat threat.start()
class SplPlugin(SplThread): plugin_id = 'record_hd' plugin_names = ['HD Recorder'] def __init__(self, modref): ''' inits the plugin ''' self.modref = modref # do the plugin specific initialisation first self.origin_dir = os.path.dirname(__file__) self.config = JsonStorage(self.plugin_id, 'backup', "config.json", { 'path': '/var/schnipsl', 'www-root': 'http://schnipsl:9092/' }) self.records = JsonStorage(self.plugin_id, 'runtime', "records.json", {}) self.record_threats = { } # we need to store the thread pointers seperate from self.records, as we can't store them as json self.last_recorded_time = 0 # remembers how long the last recording action is away # at last announce the own plugin super().__init__(modref.message_handler, self) modref.message_handler.add_event_handler(self.plugin_id, 0, self.event_listener) modref.message_handler.add_query_handler(self.plugin_id, 0, self.query_handler) self.runFlag = True def event_listener(self, queue_event): if queue_event.type == defaults.TIMER_RECORD_REQUEST: self.timer_record_request(queue_event.data) # for further pocessing, do not forget to return the queue event return queue_event def query_handler(self, queue_event, max_result_count): ''' try to send simulated answers ''' # logger.info(f"hd_recorder query handler" {queue_event.type} {queue_event.user} {max_result_count"}) if queue_event.type == defaults.QUERY_MOVIE_ID: new_uri = queue_event.params for record_movie in self.records.read( 'all', {}).values(): # 'all': read the whole config if record_movie['new_uri'] == new_uri: return [ MovieInfo( source=self.plugin_names[0], source_type=defaults.MOVIE_TYPE_RECORD, provider=record_movie['new_uri'].split(':') [1], # extracts the original provider back out of the uri category=record_movie['category'], title=record_movie['title'], timestamp=record_movie['timestamp'], duration=record_movie['duration'], description=record_movie['description'], url=record_movie['new_url'], mime=record_movie['mime']) ] return [] def _run(self): ''' starts the server ''' scheduler = Scheduler([(self.check_for_records, 10), (self.cleanup_records, 60)]) while self.runFlag: scheduler.execute() time.sleep(2) def _stop(self): self.runFlag = False def timer_record_request(self, data): uri = data['uri'] uuid = data['uuid'] movie_info_list = self.modref.message_handler.query( Query(None, defaults.QUERY_MOVIE_ID, uri)) if movie_info_list: movie_info = movie_info_list[0] uri = movie_info['uri'] # do we have that record request already existing_record = self.records.read(uri) if not existing_record: uri_base64 = base64_encode(uri) ext = '.mp4' if movie_info['mime'] == 'video/MP2T': ext = '.mp4' file_path = DirectoryMapper.abspath( '', 'videos', self.config.read('path') + uri_base64 + ext) if movie_info['source_type'] == defaults.MOVIE_TYPE_RECORD: self.records.write( uri, { # in case of a record we set start and duration to 0 to indicate that the recording can start immediadly & has no duration 'record_starttime': 0, 'record_duration': 0, 'provider': movie_info['provider'], 'category': movie_info['category'], 'title': movie_info['title'], 'timestamp': movie_info['timestamp'], 'duration': movie_info['duration'], 'description': movie_info['description'], 'url': movie_info['url'], 'uri': uri, 'new_uri': self.plugin_names[0] + ':' + ':'.join(movie_info['uri'].split(':')[1:]), 'new_url': self.config.read('www-root') + uri_base64 + ext, 'uuid': uuid, 'file_path': file_path, 'state': Record_States.WAIT_FOR_RECORDING, 'errorcount': 4 # try to start the record up to 4 times before it finally failes }) if movie_info['source_type'] == defaults.MOVIE_TYPE_STREAM: # recording a stream with a duration of 0 is a very bad idea, because it would never stop.. if movie_info['duration']: self.records.write( uri, { 'record_starttime': movie_info['timestamp'], 'record_duration': movie_info['duration'], 'category': movie_info['category'], 'title': movie_info['title'], 'timestamp': movie_info['timestamp'], 'duration': movie_info['duration'], 'description': movie_info['description'], 'url': movie_info['url'], 'mime': movie_info['mime'], 'uri': uri, 'new_uri': self.plugin_names[0] + ':' + ':'.join(movie_info['uri'].split(':')[1:]), 'new_url': self.config.read('www-root') + uri_base64 + ext, 'uuid': uuid, 'file_path': file_path, 'state': Record_States.WAIT_FOR_RECORDING, 'errorcount': 4 # try to start the record up to 4 times before it finally failes }) def check_for_records(self): act_time = time.time() for uri, record in self.records.read('all', '').items(): if record['state'] == Record_States.WAIT_FOR_RECORDING: if record[ 'record_duration'] == 0: # this is a record, which can be recorded immediadly record['state'] = Record_States.ACTUAL_RECORDING self.records.write(uri, record) self.recording(record) continue # something went wrong, the record time was in the past. Mark the entry as failed if record['record_starttime'] + record[ 'record_duration'] < act_time: record['state'] = Record_States.RECORDING_FAILED # something went wrong during recording if record['state'] == Record_States.RECORDING_FAILED: self.records.write(uri, record) self.deploy_record_result(record, record['state']) continue # it's time to start if record['record_starttime'] - self.config.read( 'padding_secs', 300 ) <= act_time and record['record_starttime'] + record[ 'record_duration'] > act_time: # in case the movie has already started, we correct starttime and duration to show the real values if record['record_starttime'] < act_time: record['starttime'] = str(act_time) record['duration'] = record['duration'] - ( act_time - record['record_starttime']) record['state'] = Record_States.ACTUAL_RECORDING self.records.write(uri, record) self.recording(record) continue def cleanup_records(self): records_to_delete = {} act_time = time.time() # request which movies are still in the UI list valid_movieuri_list = self.modref.message_handler.query( Query(None, defaults.QUERY_VALID_MOVIE_RECORDS, {'source': self.plugin_names[0]})) for uri, record in self.records.config.items(): if uri in self.record_threats: # recording is finished, so deploy the result if not self.record_threats[uri].is_alive(): del (self.record_threats[uri]) # we destroy the thread self.deploy_record_result(record, record['state']) self.last_recorded_time = act_time if self.last_recorded_time > act_time - 5 * 60: return # don't do any delete action if the last record is just 5 mins ago to give the UI some time to adapt the new movie if record[ 'state'] == Record_States.ACTUAL_RECORDING and not uri in self.record_threats: # seems to be a zombie record records_to_delete[uri] = record self.deploy_record_result(record, Record_States.RECORDING_FAILED) if record['state'] == Record_States.RECORDING_FINISHED or record[ 'state'] == Record_States.RECORDING_FAILED: new_uri = record['new_uri'] #logger.info(f'Record on disk: {new_uri}') if not new_uri in valid_movieuri_list: records_to_delete[uri] = record # some debug output #for uri in valid_movieuri_list: # logger.info(f'recoder uri: {uri}') if records_to_delete: # go through the list of records to be deleted for uri, record in records_to_delete.items(): # delete the file file_path = record['file_path'] logger.info(f'try to delete file {file_path}') if os.path.exists(file_path): try: os.remove(file_path) logger.info(f'file deleted {file_path}') except Exception as ex: logger.warning( "Cant delete record file {0}. Error: {1}".format( file_path, str(ex))) else: # remove the entry logger.info(f'file not found, just remove the entry {uri}') del (self.records.config[uri]) self.records.save() def deploy_record_result(self, record, record_state): # save changes self.records.write(record['uri'], record) self.modref.message_handler.queue_event( None, defaults.TIMER_RECORD_RESULT, { 'new_uri': record['new_uri'], 'new_url': record['new_url'], 'uuid': record['uuid'], 'record_state': record_state }) def recording(self, record): uri = record['uri'] logger.info(f'try to record {uri}') threat = threading.Thread(target=record_thread, args=(record, self.config.read('padding_secs', 300))) self.record_threats[uri] = threat threat.start()