def _start_recording(cls): current_date_time_in_utc = datetime.now(pytz.utc) with Database.get_write_lock(): db_session = Database.create_session() try: for scheduled_recording in DatabaseAccess.query_scheduled_recordings( db_session): scheduled_recording_start_date_time_in_utc = scheduled_recording.start_date_time_in_utc if current_date_time_in_utc > scheduled_recording_start_date_time_in_utc: scheduled_recording.status = RecordingStatus.LIVE.value with cls._live_recordings_to_recording_thread_lock: cls._live_recordings_to_recording_thread[ scheduled_recording.id] = RecordingThread( scheduled_recording) cls._live_recordings_to_recording_thread[ scheduled_recording.id].start() db_session.commit() cls._set_start_recording_timer(db_session) except Exception: (type_, value_, traceback_) = sys.exc_info() logger.error('\n'.join( traceback.format_exception(type_, value_, traceback_))) db_session.rollback() finally: db_session.close()
def scrub_password(cls, provider_name, password): if cls._determine_password_state(password) == PasswordState.DECRYPTED: if cls._fernet is None: fernet_key = Fernet.generate_key() cls._fernet = Fernet(fernet_key) with Database.get_write_lock(): db_session = Database.create_session() try: db_session.add( Setting('password_encryption_key', fernet_key.decode())) db_session.commit() except Exception: (type_, value_, traceback_) = sys.exc_info() logger.error('\n'.join( traceback.format_exception(type_, value_, traceback_))) db_session.rollback() finally: db_session.close() encrypted_password = cls._encrypt_password(password) logger.debug('Scrubbed {0} password\n' 'Encrypted password => {1}'.format( provider_name, encrypted_password)) else: if cls._fernet: try: cls.decrypt_password(password) encrypted_password = password logger.debug( 'Decryption key loaded is valid for the encrypted {0} password' .format(provider_name)) except InvalidToken: logger.error( 'Decryption key loaded is not valid for the encrypted {0} password\n' 'Please re-enter your cleartext password in the configuration file\n' 'Configuration file path => {0}\n' 'Exiting...'.format( provider_name, Configuration.get_configuration_file_path)) sys.exit() else: logger.error( '{0} password is encrypted, but no decryption key was found\n' 'Please re-enter your cleartext password in the configuration file\n' 'Configuration file path => {0}\n' 'Exiting...'.format( provider_name, Configuration.get_configuration_file_path)) sys.exit() return encrypted_password
def start_proxy( cls, configuration_file_path, optional_settings_file_path, db_file_path, log_file_path, recordings_directory_path, certificate_file_path, key_file_path, ): Configuration.set_configuration_file_path(configuration_file_path) OptionalSettings.set_optional_settings_file_path( optional_settings_file_path) Database.set_database_file_path(db_file_path) Logging.set_log_file_path(log_file_path) PVR.set_recordings_directory_path(recordings_directory_path) SecurityManager.set_certificate_file_path(certificate_file_path) SecurityManager.set_key_file_path(key_file_path) ProvidersController.initialize() OptionalSettings.read_optional_settings_file() Database.initialize() SecurityManager.initialize() Configuration.read_configuration_file() CacheManager.initialize() HTMLTemplateEngine.initialize() HTTPRequestHandler.initialize() PVR.initialize() Configuration.start_configuration_file_watchdog_observer() OptionalSettings.start_optional_settings_file_watchdog_observer() Logging.start_logging_configuration_file_watchdog_observer() cls.start_http_server() cls.start_https_server() PVR.start() while not cls._shutdown_proxy_event.is_set(): if cls._http_server_thread: cls._http_server_thread.join() if cls._https_server_thread: cls._https_server_thread.join() cls._shutdown_proxy_event.wait(0.5) Configuration.join_configuration_file_watchdog_observer() OptionalSettings.join_optional_settings_file_watchdog_observer() Logging.join_logging_configuration_file_watchdog_observer()
def initialize_temporary(cls): cls._temporary_database_file_path = os.path.join(os.path.dirname(Database.get_database_file_path()), DarkMediaConstants.TEMPORARY_DB_FILE_NAME) super().initialize_temporary() Base.metadata.create_all(cls._temporary_engine)
def initialize(cls): cls._database_file_path = os.path.join(os.path.dirname(Database.get_database_file_path()), DarkMediaConstants.DB_FILE_NAME) super().initialize() Base.metadata.create_all(cls._engine)
def initialize(cls): with Database.get_write_lock(): db_session = Database.create_session() try: cls._initialize_recordings(db_session) db_session.commit() except Exception: (type_, value_, traceback_) = sys.exc_info() logger.error('\n'.join( traceback.format_exception(type_, value_, traceback_))) db_session.rollback() finally: db_session.close()
def generate_vod_recording_playlist_m3u8(cls, client_uuid, recording_id, http_token): db_session = Database.create_session() try: vod_playlist_m3u8_object = M3U8() vod_playlist_m3u8_object.media_sequence = 0 vod_playlist_m3u8_object.version = '3' vod_playlist_m3u8_object.target_duration = 0 vod_playlist_m3u8_object.playlist_type = 'VOD' for segment_row in DatabaseAccess.query_segment_pickle( db_session, recording_id): segment = pickle.loads(segment_row.pickle) if segment.duration > vod_playlist_m3u8_object.target_duration: vod_playlist_m3u8_object.target_duration = math.ceil( segment.duration) vod_playlist_m3u8_object.add_segment(segment) return re.sub( r'(\.ts\?)(.*)', r'\1client_uuid={0}&http_token={1}&\2'.format( client_uuid, urllib.parse.quote(http_token) if http_token else ''), '{0}\n' '{1}'.format(vod_playlist_m3u8_object.dumps(), '#EXT-X-ENDLIST')) finally: db_session.close()
def get_recordings(cls): recordings = [] db_session = Database.create_session() try: for recording in DatabaseAccess.query_recordings(db_session): recordings.append(recording) finally: db_session.close() return recordings
def _initialize_fernet_key(cls): db_session = Database.create_session() try: password_encryption_key_setting = DatabaseAccess.query_setting( db_session, 'password_encryption_key') if password_encryption_key_setting is not None: fernet_key = password_encryption_key_setting.value.encode() cls._fernet = Fernet(fernet_key) finally: db_session.close()
def generate_vod_index_playlist_m3u8(cls, is_server_secure, client_ip_address, client_uuid, http_token): playlist_m3u8 = [] client_ip_address_type = Utility.determine_ip_address_type( client_ip_address) server_hostname = Configuration.get_configuration_parameter( 'SERVER_HOSTNAME_{0}'.format(client_ip_address_type.value)) server_port = Configuration.get_configuration_parameter( 'SERVER_HTTP{0}_PORT'.format('S' if is_server_secure else '')) db_session = Database.create_session() try: for persistent_recording in DatabaseAccess.query_persisted_recordings( db_session): playlist_m3u8.append( '#EXTINF:-1,{0} - [{1} - {2}]\n' '{3}\n'.format( persistent_recording.program_title, persistent_recording.start_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S%z'), persistent_recording.end_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S%z'), cls.generate_vod_recording_playlist_url( is_server_secure, server_hostname, server_port, client_uuid, persistent_recording.id, http_token, ), )) finally: db_session.close() if playlist_m3u8: playlist_m3u8 = '#EXTM3U\n{0}'.format(''.join(playlist_m3u8)) logger.debug('Generated VOD playlist.m3u8') else: logger.debug( 'No persistent recordings found. VOD playlist.m3u8 will not be generated' ) return playlist_m3u8
def get_recording_program_title(cls, recording_id): db_session = Database.create_session() try: recording = DatabaseAccess.query_recording(db_session, recording_id) if recording is not None: program_title = recording.program_title else: program_title = 'Recording {0}'.format(recording_id) return program_title finally: db_session.close()
def load_ts_file(cls, path, recording_id): db_session = Database.create_session() try: segment_name = re.sub(r'/vod/(.*)\?.*', r'\1', path) segment_row = DatabaseAccess.query_segment_directory_path( db_session, segment_name, recording_id) if segment_row is not None: return Utility.read_file(os.path.join( segment_row.directory_path, segment_name), in_binary=True) else: raise SegmentNotFoundError finally: db_session.close()
def process_configuration_file_updates(cls): with cls._lock.writer_lock: message_to_log = [] purge_http_sessions = False restart_http_server = False restart_https_server = False # <editor-fold desc="Detect and handle SERVER_PASSWORD change"> if cls._configuration[ 'SERVER_PASSWORD'] != cls._previous_configuration[ 'SERVER_PASSWORD']: purge_http_sessions = True message_to_log.append( 'Detected a change in the password option in the [Server] section\n' 'Old value => {0}\n' 'New value => {1}\n'.format( cls._previous_configuration['SERVER_PASSWORD'], cls._configuration['SERVER_PASSWORD'])) # </editor-fold> # <editor-fold desc="Detect and handle SERVER_HOSTNAME_<LOOPBACK,PRIVATE,PUBLIC> change"> loopback_hostname_updated = False private_hostname_updated = False public_hostname_updated = False if cls._configuration['SERVER_HOSTNAME_LOOPBACK'] != \ cls._previous_configuration['SERVER_HOSTNAME_LOOPBACK']: loopback_hostname_updated = True message_to_log.append( 'Detected a change in the loopback option in the [Hostnames] section\n' 'Old value => {0}\n' 'New value => {1}\n'.format( cls. _previous_configuration['SERVER_HOSTNAME_LOOPBACK'], cls._configuration['SERVER_HOSTNAME_LOOPBACK'])) if cls._configuration[ 'SERVER_HOSTNAME_PRIVATE'] != cls._previous_configuration[ 'SERVER_HOSTNAME_PRIVATE']: private_hostname_updated = True message_to_log.append( 'Detected a change in the private option in the [Hostnames] section\n' 'Old value => {0}\n' 'New value => {1}\n'.format( cls._previous_configuration['SERVER_HOSTNAME_PRIVATE'], cls._configuration['SERVER_HOSTNAME_PRIVATE'])) if cls._configuration[ 'SERVER_HOSTNAME_PUBLIC'] != cls._previous_configuration[ 'SERVER_HOSTNAME_PUBLIC']: public_hostname_updated = True message_to_log.append( 'Detected a change in the public option in the [Hostnames] section\n' 'Old value => {0}\n' 'New value => {1}\n'.format( cls._previous_configuration['SERVER_HOSTNAME_PUBLIC'], cls._configuration['SERVER_HOSTNAME_PUBLIC'])) if loopback_hostname_updated or private_hostname_updated or public_hostname_updated: restart_https_server = True # </editor-fold> # <editor-fold desc="Detect and handle SERVER_HTTP_PORT change"> if cls._configuration[ 'SERVER_HTTP_PORT'] != cls._previous_configuration[ 'SERVER_HTTP_PORT']: restart_http_server = True message_to_log.append( 'Detected a change in the http option in the [Ports] section\n' 'Old value => {0}\n' 'New value => {1}\n'.format( cls._previous_configuration['SERVER_HTTP_PORT'], cls._configuration['SERVER_HTTP_PORT'])) # </editor-fold> # <editor-fold desc="Detect and handle SERVER_HTTPS_PORT change"> if cls._configuration[ 'SERVER_HTTPS_PORT'] != cls._previous_configuration[ 'SERVER_HTTPS_PORT']: restart_https_server = True message_to_log.append( 'Detected a change in the https option in the [Ports] section\n' 'Old value => {0}\n' 'New value => {1}\n'.format( cls._previous_configuration['SERVER_HTTPS_PORT'], cls._configuration['SERVER_HTTPS_PORT'])) # </editor-fold> if purge_http_sessions: from iptv_proxy.http_server import HTTPRequestHandler message_to_log.append( 'Action => Purge all user HTTP/S sessions') with Database.get_write_lock(): db_session = Database.create_session() try: HTTPRequestHandler.purge_http_sessions(db_session) db_session.commit() except Exception: (type_, value_, traceback_) = sys.exc_info() logger.error('\n'.join( traceback.format_exception(type_, value_, traceback_))) db_session.rollback() finally: db_session.close() if restart_http_server: from iptv_proxy.controller import Controller message_to_log.append('Action => Restart HTTP server') Controller.shutdown_http_server() Controller.start_http_server() if restart_https_server: from iptv_proxy.security import SecurityManager message_to_log.append('Action => Restart HTTPS server') if SecurityManager.get_auto_generate_self_signed_certificate(): SecurityManager.generate_self_signed_certificate() from iptv_proxy.controller import Controller Controller.shutdown_https_server() Controller.start_https_server() if message_to_log: logger.debug('\n'.join(message_to_log)) for provider_name in sorted( ProvidersController.get_providers_map_class()): ProvidersController.get_provider_map_class( provider_name).configuration_class( ).process_configuration_file_updates( cls._configuration, cls._previous_configuration)
def run(self): logger.info( 'Starting recording\n' 'Provider => {0}\n' 'Channel number => {1}\n' 'Channel name => {2}\n' 'Program title => {3}\n' 'Start date & time => {4}\n' 'End date & time => {5}'.format( self._recording.provider, self._recording.channel_number, self._recording.channel_name, self._recording.program_title, self._recording.start_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime('%Y-%m-%d %H:%M:%S'), self._recording.end_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime('%Y-%m-%d %H:%M:%S'))) self._create_recording_directory_tree() try: hls_client = HLSClient(self._id, self._recording.provider.lower(), self._recording.channel_number) playlist_m3u8_object = m3u8.loads( hls_client.download_playlist_m3u8()) chunks_m3u8_object = None try: chunks_url = '/live/{0}/{1}'.format( self._recording.provider.lower(), playlist_m3u8_object.data['playlists'][0]['uri']) except IndexError: chunks_m3u8_object = playlist_m3u8_object downloaded_segment_file_names = [] while not self._stop_recording_event.is_set(): try: chunks_m3u8_object = m3u8.loads( hls_client.download_chunks_m3u8(chunks_url)) except NameError: if chunks_m3u8_object is None: chunks_m3u8_object = m3u8.loads( hls_client.download_playlist_m3u8()) chunks_m3u8_download_date_time_in_utc = datetime.now(pytz.utc) chunks_m3u8_total_duration = 0 for (segment_index, segment) in enumerate(chunks_m3u8_object.segments): segment_url = '/live/{0}'.format(segment.uri) segment_url_components = urllib.parse.urlparse(segment_url) segment_file_name = re.sub(r'(/.*)?(/)(.*\.ts)', r'\3', segment_url_components.path) if segment_file_name not in downloaded_segment_file_names: try: ts_file_content = CacheManager.query_cache( self._recording.channel_number, segment_file_name.lower()) if ts_file_content is None: ts_file_content = hls_client.download_ts_file( segment_url) CacheManager.update_cache( self._recording.channel_number, segment_file_name.lower(), ts_file_content) logger.debug( 'Downloaded segment\n' 'Segment => {0}'.format(segment_file_name)) segment.uri = '{0}?recording_id={1}'.format( segment_file_name, urllib.parse.quote(self._recording.id)) downloaded_segment_file_names.append( segment_file_name) Utility.write_file( ts_file_content, os.path.join(self._recording_directory_path, segment_file_name), in_binary=True) with Database.get_write_lock(): db_session = Database.create_session() try: db_session.add( Segment( segment_file_name, self._recording.id, pickle.dumps(segment, protocol=pickle. HIGHEST_PROTOCOL), self._recording_directory_path)) db_session.commit() except Exception: (type_, value_, traceback_) = sys.exc_info() logger.error('\n'.join( traceback.format_exception( type_, value_, traceback_))) db_session.rollback() finally: db_session.close() except requests.exceptions.HTTPError: logger.error( 'Failed to download segment\n' 'Segment => {0}'.format(segment_file_name)) else: logger.debug( 'Skipped segment since it was already downloaded\n' 'Segment => {0} '.format(segment_file_name)) chunks_m3u8_total_duration += segment.duration current_date_time_in_utc = datetime.now(pytz.utc) wait_duration = chunks_m3u8_total_duration - ( current_date_time_in_utc - chunks_m3u8_download_date_time_in_utc).total_seconds() if wait_duration > 0: self._stop_recording_event.wait(wait_duration) chunks_m3u8_object = None self._recording.status = RecordingStatus.PERSISTED.value db_session.merge(self._recording) db_session.commit() logger.info( 'Finished recording\n' 'Provider => {0}\n' 'Channel number => {1}\n' 'Channel name => {2}\n' 'Program title => {3}\n' 'Start date & time => {4}\n' 'End date & time => {5}'.format( self._recording.provider, self._recording.channel_number, self._recording.channel_name, self._recording.program_title, self._recording.start_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime('%Y-%m-%d %H:%M:%S'), self._recording.end_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S'))) except (HLSPlaylistDownloadError, ProviderNotFoundError): if self._stop_recording_event.is_set(): self._recording.status = RecordingStatus.PERSISTED.value db_session.merge(self._recording) db_session.commit() logger.info( 'Finished recording\n' 'Provider => {0}\n' 'Channel number => {1}\n' 'Channel name => {2}\n' 'Program title => {3}\n' 'Start date & time => {4}\n' 'End date & time => {5}'.format( self._recording.provider, self._recording.channel_number, self._recording.channel_name, self._recording.program_title, self._recording.start_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S'), self._recording.end_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S'))) else: logger.info( 'Canceling recording\n' 'Provider => {0}\n' 'Channel number => {1}\n' 'Channel name => {2}\n' 'Program title => {3}\n' 'Start date & time => {4}\n' 'End date & time => {5}'.format( self._recording.provider, self._recording.channel_number, self._recording.channel_name, self._recording.program_title, self._recording.start_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S'), self._recording.end_date_time_in_utc.astimezone( tzlocal.get_localzone()).strftime( '%Y-%m-%d %H:%M:%S'))) finally: PVR.cleanup_live_recording(self._recording)