示例#1
0
    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()
示例#2
0
    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
示例#3
0
    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()
示例#4
0
    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)
示例#5
0
    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)
示例#6
0
    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()
示例#7
0
    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()
示例#8
0
    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
示例#9
0
    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()
示例#10
0
    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
示例#11
0
    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()
示例#12
0
    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()
示例#13
0
    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)
示例#14
0
    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)