class SoundpacksTab(QTabWidget):
    def __init__(self):
        super(SoundpacksTab, self).__init__()

        self.tab_disabled = False

        self.qnam = QNetworkAccessManager()

        self.http_reply = None
        self.download_http_reply = None
        self.current_repo_info = None

        self.soundpacks = []
        self.soundpacks_model = None

        self.installing_new_soundpack = False
        self.downloading_new_soundpack = False
        self.extracting_new_soundpack = False

        self.close_after_install = False

        self.game_dir = None
        self.soundpacks_dir = None

        layout = QVBoxLayout()

        top_part = QWidget()
        tp_layout = QHBoxLayout()
        tp_layout.setContentsMargins(0, 0, 0, 0)
        self.tp_layout = tp_layout

        installed_gb = QGroupBox()
        tp_layout.addWidget(installed_gb)
        self.installed_gb = installed_gb

        installed_gb_layout = QVBoxLayout()
        installed_gb.setLayout(installed_gb_layout)
        self.installed_gb_layout = installed_gb_layout

        installed_lv = QListView()
        installed_lv.clicked.connect(self.installed_clicked)
        installed_lv.setEditTriggers(QAbstractItemView.NoEditTriggers)
        installed_gb_layout.addWidget(installed_lv)
        self.installed_lv = installed_lv

        installed_buttons = QWidget()
        ib_layout = QHBoxLayout()
        installed_buttons.setLayout(ib_layout)
        ib_layout.setContentsMargins(0, 0, 0, 0)
        self.ib_layout = ib_layout
        self.installed_buttons = installed_buttons
        installed_gb_layout.addWidget(installed_buttons)

        disable_existing_button = QPushButton()
        disable_existing_button.clicked.connect(self.disable_existing)
        disable_existing_button.setEnabled(False)
        ib_layout.addWidget(disable_existing_button)
        self.disable_existing_button = disable_existing_button

        delete_existing_button = QPushButton()
        delete_existing_button.clicked.connect(self.delete_existing)
        delete_existing_button.setEnabled(False)
        ib_layout.addWidget(delete_existing_button)
        self.delete_existing_button = delete_existing_button

        repository_gb = QGroupBox()
        tp_layout.addWidget(repository_gb)
        self.repository_gb = repository_gb

        repository_gb_layout = QVBoxLayout()
        repository_gb.setLayout(repository_gb_layout)
        self.repository_gb_layout = repository_gb_layout

        repository_lv = QListView()
        repository_lv.clicked.connect(self.repository_clicked)
        repository_lv.setEditTriggers(QAbstractItemView.NoEditTriggers)
        repository_gb_layout.addWidget(repository_lv)
        self.repository_lv = repository_lv

        suggest_new_label = QLabel()
        suggest_new_label.setOpenExternalLinks(True)
        repository_gb_layout.addWidget(suggest_new_label)
        self.suggest_new_label = suggest_new_label

        install_new_button = QPushButton()
        install_new_button.clicked.connect(self.install_new)
        install_new_button.setEnabled(False)
        repository_gb_layout.addWidget(install_new_button)
        self.install_new_button = install_new_button

        top_part.setLayout(tp_layout)
        layout.addWidget(top_part)
        self.top_part = top_part

        details_gb = QGroupBox()
        layout.addWidget(details_gb)
        self.details_gb = details_gb

        details_gb_layout = QGridLayout()

        viewname_label = QLabel()
        details_gb_layout.addWidget(viewname_label, 0, 0, Qt.AlignRight)
        self.viewname_label = viewname_label

        viewname_le = QLineEdit()
        viewname_le.setReadOnly(True)
        details_gb_layout.addWidget(viewname_le, 0, 1)
        self.viewname_le = viewname_le

        name_label = QLabel()
        details_gb_layout.addWidget(name_label, 1, 0, Qt.AlignRight)
        self.name_label = name_label

        name_le = QLineEdit()
        name_le.setReadOnly(True)
        details_gb_layout.addWidget(name_le, 1, 1)
        self.name_le = name_le

        path_label = QLabel()
        details_gb_layout.addWidget(path_label, 2, 0, Qt.AlignRight)
        self.path_label = path_label

        path_le = QLineEdit()
        path_le.setReadOnly(True)
        details_gb_layout.addWidget(path_le, 2, 1)
        self.path_le = path_le

        size_label = QLabel()
        details_gb_layout.addWidget(size_label, 3, 0, Qt.AlignRight)
        self.size_label = size_label

        size_le = QLineEdit()
        size_le.setReadOnly(True)
        details_gb_layout.addWidget(size_le, 3, 1)
        self.size_le = size_le

        homepage_label = QLabel()
        details_gb_layout.addWidget(homepage_label, 4, 0, Qt.AlignRight)
        self.homepage_label = homepage_label

        homepage_tb = QTextBrowser()
        homepage_tb.setReadOnly(True)
        homepage_tb.setOpenExternalLinks(True)
        homepage_tb.setMaximumHeight(23)
        homepage_tb.setLineWrapMode(QTextEdit.NoWrap)
        homepage_tb.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        details_gb_layout.addWidget(homepage_tb, 4, 1)
        self.homepage_tb = homepage_tb

        details_gb.setLayout(details_gb_layout)
        self.details_gb_layout = details_gb_layout

        self.setLayout(layout)

        self.load_repository()
        self.set_text()

    def set_text(self):
        self.installed_gb.setTitle(_('Installed'))
        self.disable_existing_button.setText(_('Disable'))
        self.delete_existing_button.setText(_('Delete'))
        self.repository_gb.setTitle(_('Repository'))
        suggest_url = cons.NEW_ISSUE_URL + '?' + urlencode({
            'title': _('Add this new soundpack to the repository'),
            'body': _('''* Name: [Enter the name of the soundpack]
* Url: [Enter the Url where we can find the soundpack]
* Author: [Enter the name of the author]
* Homepage: [Enter the Url of the author website or where the soundpack was published]
* Soundpack not found in version: {version}
''').format(version=version)
        })
        self.suggest_new_label.setText(
            _('<a href="{url}">Suggest a new soundpack '
            'on GitHub</a>').format(url=suggest_url))
        self.install_new_button.setText(_('Install this soundpack'))
        self.details_gb.setTitle(_('Details'))
        self.viewname_label.setText(_('View name:'))
        self.name_label.setText(_('Name:'))

        selection_model = self.repository_lv.selectionModel()
        if selection_model is not None and selection_model.hasSelection():
            self.path_label.setText(_('Url:'))
        else:
            self.path_label.setText(_('Path:'))

        self.size_label.setText(_('Size:'))
        self.homepage_label.setText(_('Home page:'))

    def get_main_window(self):
        return self.parentWidget().parentWidget().parentWidget()

    def get_main_tab(self):
        return self.parentWidget().parentWidget().main_tab

    def get_mods_tab(self):
        return self.get_main_tab().get_mods_tab()

    def get_settings_tab(self):
        return self.get_main_tab().get_settings_tab()

    def get_backups_tab(self):
        return self.get_main_tab().get_backups_tab()

    def disable_tab(self):
        self.tab_disabled = True

        self.disable_existing_button.setEnabled(False)
        self.delete_existing_button.setEnabled(False)

        self.install_new_button.setEnabled(False)

        installed_selection = self.installed_lv.selectionModel()
        if installed_selection is not None:
            installed_selection.clearSelection()

        repository_selection = self.repository_lv.selectionModel()
        if repository_selection is not None:
            repository_selection.clearSelection()

    def enable_tab(self):
        self.tab_disabled = False

        installed_selection = self.installed_lv.selectionModel()
        if installed_selection is None:
            installed_selected = False
        else:
            installed_selected = installed_selection.hasSelection()

        self.disable_existing_button.setEnabled(installed_selected)
        self.delete_existing_button.setEnabled(installed_selected)

        repository_selection = self.repository_lv.selectionModel()
        if repository_selection is None:
            repository_selected = False
        else:
            repository_selected = repository_selection.hasSelection()

        self.install_new_button.setEnabled(repository_selected)

    def load_repository(self):
        self.repo_soundpacks = []

        self.install_new_button.setEnabled(False)

        self.repo_soundpacks_model = QStringListModel()
        self.repository_lv.setModel(self.repo_soundpacks_model)
        self.repository_lv.selectionModel().currentChanged.connect(
            self.repository_selection)

        json_file = get_data_path('soundpacks.json')

        if os.path.isfile(json_file):
            with open(json_file, 'r', encoding='utf8') as f:
                try:
                    values = json.load(f)
                    if isinstance(values, list):
                        values.sort(key=lambda x: x['name'])
                        self.repo_soundpacks = values

                        self.repo_soundpacks_model.insertRows(
                            self.repo_soundpacks_model.rowCount(),
                            len(self.repo_soundpacks))
                        for index, soundpack_info in enumerate(
                            self.repo_soundpacks):
                            self.repo_soundpacks_model.setData(
                                self.repo_soundpacks_model.index(index),
                                soundpack_info['viewname'])
                except ValueError:
                    pass

    def install_new(self):
        if not self.installing_new_soundpack:
            selection_model = self.repository_lv.selectionModel()
            if selection_model is None or not selection_model.hasSelection():
                return

            selected = selection_model.currentIndex()
            selected_info = self.repo_soundpacks[selected.row()]

            # Is it already installed?
            for soundpack in self.soundpacks:
                if soundpack['NAME'] == selected_info['name']:
                    confirm_msgbox = QMessageBox()
                    confirm_msgbox.setWindowTitle(_('Soundpack already present'
                        ))
                    confirm_msgbox.setText(_('It seems this soundpack is '
                        'already installed. The launcher will not overwrite '
                        'the soundpack if it has the same directory name. You '
                        'might want to delete the soundpack first if you want '
                        'to update it. Also, there can only be a single '
                        'soundpack with the same name value available in the '
                        'game.'))
                    confirm_msgbox.setInformativeText(_('Are you sure you want '
                        'to install the {view} soundpack?').format(
                            view=selected_info['viewname']))
                    confirm_msgbox.addButton(_('Install the soundpack'),
                        QMessageBox.YesRole)
                    confirm_msgbox.addButton(_('Do not install again'),
                        QMessageBox.NoRole)
                    confirm_msgbox.setIcon(QMessageBox.Warning)

                    if confirm_msgbox.exec() == 1:
                        return
                    break

            self.install_type = selected_info['type']

            if selected_info['type'] == 'direct_download':
                if self.http_reply is not None and self.http_reply.isRunning():
                    self.http_reply_aborted = True
                    self.http_reply.abort()

                self.installing_new_soundpack = True
                self.download_aborted = False

                download_dir = tempfile.mkdtemp(prefix=cons.TEMP_PREFIX)

                download_url = selected_info['url']

                url = QUrl(download_url)
                file_info = QFileInfo(url.path())
                file_name = file_info.fileName()

                self.downloaded_file = os.path.join(download_dir, file_name)
                self.downloading_file = open(self.downloaded_file, 'wb')

                main_window = self.get_main_window()

                status_bar = main_window.statusBar()
                status_bar.clearMessage()

                status_bar.busy += 1

                downloading_label = QLabel()
                downloading_label.setText(_('Downloading: {0}').format(
                    selected_info['url']))
                status_bar.addWidget(downloading_label, 100)
                self.downloading_label = downloading_label

                dowloading_speed_label = QLabel()
                status_bar.addWidget(dowloading_speed_label)
                self.dowloading_speed_label = dowloading_speed_label

                downloading_size_label = QLabel()
                status_bar.addWidget(downloading_size_label)
                self.downloading_size_label = downloading_size_label

                progress_bar = QProgressBar()
                status_bar.addWidget(progress_bar)
                self.downloading_progress_bar = progress_bar
                progress_bar.setMinimum(0)

                self.download_last_read = datetime.utcnow()
                self.download_last_bytes_read = 0
                self.download_speed_count = 0

                self.downloading_new_soundpack = True

                request = QNetworkRequest(QUrl(url))
                request.setRawHeader(b'User-Agent', cons.FAKE_USER_AGENT)

                self.download_http_reply = self.qnam.get(request)
                self.download_http_reply.finished.connect(
                    self.download_http_finished)
                self.download_http_reply.readyRead.connect(
                    self.download_http_ready_read)
                self.download_http_reply.downloadProgress.connect(
                    self.download_dl_progress)

                self.install_new_button.setText(_('Cancel soundpack '
                    'installation'))
                self.installed_lv.setEnabled(False)
                self.repository_lv.setEnabled(False)

                self.get_main_tab().disable_tab()
                self.get_mods_tab().disable_tab()
                self.get_settings_tab().disable_tab()
                self.get_backups_tab().disable_tab()
            elif selected_info['type'] == 'browser_download':
                bd_dialog = BrowserDownloadDialog('soundpack',
                    selected_info['url'], selected_info.get('expected_filename',
                        None))
                bd_dialog.exec()

                if bd_dialog.downloaded_path is not None:
                    self.installing_new_soundpack = True
                    self.downloaded_file = bd_dialog.downloaded_path

                    self.install_new_button.setText(_('Cancel soundpack '
                        'installation'))
                    self.installed_lv.setEnabled(False)
                    self.repository_lv.setEnabled(False)

                    self.get_main_tab().disable_tab()
                    self.get_mods_tab().disable_tab()
                    self.get_settings_tab().disable_tab()
                    self.get_backups_tab().disable_tab()

                    main_window = self.get_main_window()
                    status_bar = main_window.statusBar()

                    # Test downloaded file
                    status_bar.showMessage(_('Testing downloaded file archive'))

                    if self.downloaded_file.lower().endswith('.7z'):
                        try:
                            with open(self.downloaded_file, 'rb') as f:
                                archive = Archive7z(f)
                        except FormatError:
                            status_bar.clearMessage()
                            status_bar.showMessage(_('Selected file is a '
                                'bad archive file'))

                            self.finish_install_new_soundpack()
                            return
                        except NoPasswordGivenError:
                            status_bar.clearMessage()
                            status_bar.showMessage(_('Selected file is a '
                                'password protected archive file'))

                            self.finish_install_new_soundpack()
                            return
                    else:
                        archive_exception = None
                        if self.downloaded_file.lower().endswith('.zip'):
                            archive_class = zipfile.ZipFile
                            archive_exception = zipfile.BadZipFile
                            test_method = 'testzip'
                        elif self.downloaded_file.lower().endswith('.rar'):
                            archive_class = rarfile.RarFile
                            archive_exception = rarfile.Error
                            test_method = 'testrar'
                        else:
                            extension = os.path.splitext(self.downloaded_file
                                )[1]
                            status_bar.clearMessage()
                            status_bar.showMessage(
                                _('Unknown downloaded archive format '
                                '({extension})').format(extension=extension))

                            self.finish_install_new_soundpack()
                            return

                        try:
                            with archive_class(self.downloaded_file) as z:
                                test = getattr(z, test_method)
                                if test() is not None:
                                    status_bar.clearMessage()
                                    status_bar.showMessage(
                                        _('Downloaded archive is invalid'))

                                    self.finish_install_new_soundpack()
                                    return
                        except archive_exception:
                            status_bar.clearMessage()
                            status_bar.showMessage(_('Selected file is a '
                                'bad archive file'))

                            self.finish_install_new_soundpack()
                            return

                    status_bar.clearMessage()
                    self.extract_new_soundpack()

        else:
            main_window = self.get_main_window()
            status_bar = main_window.statusBar()

            # Cancel installation
            if self.downloading_new_soundpack:
                self.download_aborted = True
                self.download_http_reply.abort()
            elif self.extracting_new_soundpack:
                self.extracting_timer.stop()

                status_bar.removeWidget(self.extracting_label)
                status_bar.removeWidget(self.extracting_progress_bar)

                status_bar.busy -= 1

                self.extracting_new_soundpack = False

                self.extracting_zipfile.close()

                download_dir = os.path.dirname(self.downloaded_file)
                delete_path(download_dir)

                if os.path.isdir(self.extract_dir):
                    delete_path(self.extract_dir)

            status_bar.showMessage(_('Soundpack installation cancelled'))

            self.finish_install_new_soundpack()

    def download_http_finished(self):
        self.downloading_file.close()

        main_window = self.get_main_window()

        status_bar = main_window.statusBar()
        status_bar.removeWidget(self.downloading_label)
        status_bar.removeWidget(self.dowloading_speed_label)
        status_bar.removeWidget(self.downloading_size_label)
        status_bar.removeWidget(self.downloading_progress_bar)

        status_bar.busy -= 1

        if self.download_aborted:
            download_dir = os.path.dirname(self.downloaded_file)
            delete_path(download_dir)

            self.downloading_new_soundpack = False
        else:
            redirect = self.download_http_reply.attribute(
                QNetworkRequest.RedirectionTargetAttribute)
            if redirect is not None:
                download_dir = os.path.dirname(self.downloaded_file)
                delete_path(download_dir)
                os.makedirs(download_dir)

                self.downloading_file = open(self.downloaded_file, 'wb')

                status_bar.busy += 1

                redirected_url = urljoin(
                    self.download_http_reply.request().url().toString(),
                    redirect.toString())

                downloading_label = QLabel()
                downloading_label.setText(_('Downloading: {0}').format(
                    redirected_url))
                status_bar.addWidget(downloading_label, 100)
                self.downloading_label = downloading_label

                dowloading_speed_label = QLabel()
                status_bar.addWidget(dowloading_speed_label)
                self.dowloading_speed_label = dowloading_speed_label

                downloading_size_label = QLabel()
                status_bar.addWidget(downloading_size_label)
                self.downloading_size_label = downloading_size_label

                progress_bar = QProgressBar()
                status_bar.addWidget(progress_bar)
                self.downloading_progress_bar = progress_bar
                progress_bar.setMinimum(0)

                self.download_last_read = datetime.utcnow()
                self.download_last_bytes_read = 0
                self.download_speed_count = 0

                progress_bar.setValue(0)

                request = QNetworkRequest(QUrl(redirected_url))
                request.setRawHeader(b'User-Agent', cons.FAKE_USER_AGENT)

                self.download_http_reply = self.qnam.get(request)
                self.download_http_reply.finished.connect(
                    self.download_http_finished)
                self.download_http_reply.readyRead.connect(
                    self.download_http_ready_read)
                self.download_http_reply.downloadProgress.connect(
                    self.download_dl_progress)
            else:
                # Test downloaded file
                status_bar.showMessage(_('Testing downloaded file archive'))

                if self.downloaded_file.lower().endswith('.7z'):
                    try:
                        with open(self.downloaded_file, 'rb') as f:
                            archive = Archive7z(f)
                    except FormatError:
                        status_bar.clearMessage()
                        status_bar.showMessage(_('Selected file is a '
                            'bad archive file'))

                        self.finish_install_new_soundpack()
                        return
                    except NoPasswordGivenError:
                        status_bar.clearMessage()
                        status_bar.showMessage(_('Selected file is a '
                            'password protected archive file'))

                        self.finish_install_new_soundpack()
                        return
                else:
                    archive_exception = None
                    if self.downloaded_file.lower().endswith('.zip'):
                        archive_class = zipfile.ZipFile
                        archive_exception = zipfile.BadZipFile
                        test_method = 'testzip'
                    elif self.downloaded_file.lower().endswith('.rar'):
                        archive_class = rarfile.RarFile
                        archive_exception = rarfile.Error
                        test_method = 'testrar'
                    else:
                        extension = os.path.splitext(self.downloaded_file)[1]
                        status_bar.clearMessage()
                        status_bar.showMessage(
                            _('Unknown downloaded archive format ({extension})'
                            ).format(extension=extension))

                        self.finish_install_new_soundpack()
                        return

                    try:
                        with archive_class(self.downloaded_file) as z:
                            test = getattr(z, test_method)
                            if test() is not None:
                                status_bar.clearMessage()
                                status_bar.showMessage(
                                    _('Downloaded archive is invalid'))

                                self.finish_install_new_soundpack()
                                return
                    except archive_exception:
                        status_bar.clearMessage()
                        status_bar.showMessage(_('Selected file is a '
                            'bad archive file'))

                        self.finish_install_new_soundpack()
                        return

                status_bar.clearMessage()
                self.downloading_new_soundpack = False
                self.extract_new_soundpack()

    def finish_install_new_soundpack(self):
        self.installing_new_soundpack = False

        self.installed_lv.setEnabled(True)
        self.repository_lv.setEnabled(True)

        self.install_new_button.setText(_('Install this soundpack'))

        self.get_main_tab().enable_tab()
        self.get_mods_tab().enable_tab()
        self.get_settings_tab().enable_tab()
        self.get_backups_tab().enable_tab()

        if self.close_after_install:
            self.get_main_window().close()

    def download_http_ready_read(self):
        self.downloading_file.write(self.download_http_reply.readAll())

    def download_dl_progress(self, bytes_read, total_bytes):
        self.downloading_progress_bar.setMaximum(total_bytes)
        self.downloading_progress_bar.setValue(bytes_read)

        self.download_speed_count += 1

        self.downloading_size_label.setText(
            '{bytes_read}/{total_bytes}'
            .format(bytes_read=sizeof_fmt(bytes_read), total_bytes=sizeof_fmt(total_bytes))
        )

        if self.download_speed_count % 5 == 0:
            delta_bytes = bytes_read - self.download_last_bytes_read
            delta_time = datetime.utcnow() - self.download_last_read

            bytes_secs = delta_bytes / delta_time.total_seconds()
            self.dowloading_speed_label.setText(_('{bytes_sec}/s').format(
                bytes_sec=sizeof_fmt(bytes_secs)))

            self.download_last_bytes_read = bytes_read
            self.download_last_read = datetime.utcnow()

    def extract_new_soundpack(self):
        self.extracting_new_soundpack = True

        if self.downloaded_file.lower().endswith('.7z'):
            self.extracting_zipfile = open(self.downloaded_file, 'rb')
            self.extracting_archive = Archive7z(self.extracting_zipfile)

            self.extracting_infolist = self.extracting_archive.getmembers()
        else:
            if self.downloaded_file.lower().endswith('.zip'):
                archive_class = zipfile.ZipFile
            elif self.downloaded_file.lower().endswith('.rar'):
                archive_class = rarfile.RarFile

            z = archive_class(self.downloaded_file)
            self.extracting_zipfile = z

            self.extracting_infolist = z.infolist()

        self.extract_dir = os.path.join(self.game_dir, 'newsoundpack')
        while os.path.exists(self.extract_dir):
            self.extract_dir = os.path.join(self.game_dir,
                'newsoundpack-{0}'.format('%08x' % random.randrange(16**8)))
        os.makedirs(self.extract_dir)

        self.extracting_index = 0

        main_window = self.get_main_window()
        status_bar = main_window.statusBar()

        status_bar.busy += 1

        extracting_label = QLabel()
        status_bar.addWidget(extracting_label, 100)
        self.extracting_label = extracting_label

        progress_bar = QProgressBar()
        status_bar.addWidget(progress_bar)
        self.extracting_progress_bar = progress_bar

        timer = QTimer(self)
        self.extracting_timer = timer

        progress_bar.setRange(0, len(self.extracting_infolist))

        def timeout():
            self.extracting_progress_bar.setValue(self.extracting_index)

            if self.extracting_index == len(self.extracting_infolist):
                self.extracting_timer.stop()

                main_window = self.get_main_window()
                status_bar = main_window.statusBar()

                status_bar.removeWidget(self.extracting_label)
                status_bar.removeWidget(self.extracting_progress_bar)

                status_bar.busy -= 1

                self.extracting_new_soundpack = False

                self.extracting_zipfile.close()
                self.extracting_zipfile = None

                if self.downloaded_file.lower().endswith('.7z'):
                    self.extracting_archive = None

                if self.install_type == 'direct_download':
                    download_dir = os.path.dirname(self.downloaded_file)
                    delete_path(download_dir)

                self.move_new_soundpack()

            else:
                extracting_element = self.extracting_infolist[
                    self.extracting_index]

                self.extracting_label.setText(_('Extracting {0}').format(
                    extracting_element.filename))

                if self.downloaded_file.lower().endswith('.7z'):
                    destination = os.path.join(self.extract_dir,
                        *extracting_element.filename.split('/'))
                    dest_dir = os.path.dirname(destination)
                    if not os.path.isdir(dest_dir):
                        os.makedirs(dest_dir)
                    with open(destination, 'wb') as f:
                        f.write(extracting_element.read())
                else:
                    self.extracting_zipfile.extract(extracting_element,
                        self.extract_dir)

                self.extracting_index += 1

        timer.timeout.connect(timeout)
        timer.start(0)

    def move_new_soundpack(self):
        # Find the soundpack in the self.extract_dir
        # Move the soundpack from that location into self.soundpacks_dir

        self.moving_new_soundpack = True

        main_window = self.get_main_window()
        status_bar = main_window.statusBar()

        status_bar.showMessage(_('Finding the soundpack'))

        next_scans = deque()
        current_scan = scandir(self.extract_dir)

        soundpack_dir = None

        while True:
            try:
                entry = next(current_scan)
                if entry.is_dir():
                    next_scans.append(entry.path)
                elif entry.is_file():
                    dirname, basename = os.path.split(entry.path)
                    if basename == 'soundpack.txt':
                        soundpack_dir = dirname
                        entry = None
                        break
            except StopIteration:
                if len(next_scans) > 0:
                    current_scan = scandir(next_scans.popleft())
                else:
                    break

        for item in current_scan:
            pass

        if soundpack_dir is None:
            status_bar.showMessage(_('Soundpack installation cancelled - There '
                'is no soundpack in the downloaded archive'))
            delete_path(self.extract_dir)
            self.moving_new_soundpack = False

            self.finish_install_new_soundpack()
        else:
            soundpack_dir_name = os.path.basename(soundpack_dir)
            target_dir = os.path.join(self.soundpacks_dir, soundpack_dir_name)
            if os.path.exists(target_dir):
                status_bar.showMessage(_('Soundpack installation cancelled - '
                    'There is already a {basename} directory in '
                    '{soundpacks_dir}').format(basename=soundpack_dir_name,
                        soundpacks_dir=self.soundpacks_dir))
            else:
                shutil.move(soundpack_dir, self.soundpacks_dir)
                status_bar.showMessage(_('Soundpack installation completed'))

            delete_path(self.extract_dir)
            self.moving_new_soundpack = False

            self.game_dir_changed(self.game_dir)
            self.finish_install_new_soundpack()

    def disable_existing(self):
        selection_model = self.installed_lv.selectionModel()
        if selection_model is None or not selection_model.hasSelection():
            return

        selected = selection_model.currentIndex()
        selected_info = self.soundpacks[selected.row()]

        if selected_info['enabled']:
            config_file = os.path.join(selected_info['path'], 'soundpack.txt')
            new_config_file = os.path.join(selected_info['path'],
                'soundpack.txt.disabled')
            try:
                shutil.move(config_file, new_config_file)
                selected_info['enabled'] = False
                self.soundpacks_model.setData(selected, selected_info['VIEW'] +
                    _(' (Disabled)'))
                self.disable_existing_button.setText(_('Enable'))
            except OSError as e:
                main_window = self.get_main_window()
                status_bar = main_window.statusBar()

                status_bar.showMessage(str(e))
        else:
            config_file = os.path.join(selected_info['path'],
                'soundpack.txt.disabled')
            new_config_file = os.path.join(selected_info['path'],
                'soundpack.txt')
            try:
                shutil.move(config_file, new_config_file)
                selected_info['enabled'] = True
                self.soundpacks_model.setData(selected, selected_info['VIEW'])
                self.disable_existing_button.setText(_('Disable'))
            except OSError as e:
                main_window = self.get_main_window()
                status_bar = main_window.statusBar()

                status_bar.showMessage(str(e))

    def delete_existing(self):
        selection_model = self.installed_lv.selectionModel()
        if selection_model is None or not selection_model.hasSelection():
            return

        selected = selection_model.currentIndex()
        selected_info = self.soundpacks[selected.row()]

        confirm_msgbox = QMessageBox()
        confirm_msgbox.setWindowTitle(_('Delete soundpack'))
        confirm_msgbox.setText(_('This will delete the soundpack directory. It '
            'cannot be undone.'))
        confirm_msgbox.setInformativeText(_('Are you sure you want to '
            'delete the {view} soundpack?').format(view=selected_info['VIEW']))
        confirm_msgbox.addButton(_('Delete the soundpack'),
            QMessageBox.YesRole)
        confirm_msgbox.addButton(_('I want to keep the soundpack'),
            QMessageBox.NoRole)
        confirm_msgbox.setIcon(QMessageBox.Warning)

        if confirm_msgbox.exec() == 0:
            main_window = self.get_main_window()
            status_bar = main_window.statusBar()

            if not delete_path(selected_info['path']):
                status_bar.showMessage(_('Soundpack deletion cancelled'))
            else:
                self.soundpacks_model.removeRows(selected.row(), 1)
                self.soundpacks.remove(selected_info)

                status_bar.showMessage(_('Soundpack deleted'))

    def installed_selection(self, selected, previous):
        self.installed_clicked()

    def installed_clicked(self):
        selection_model = self.installed_lv.selectionModel()
        if selection_model is not None and selection_model.hasSelection():
            selected = selection_model.currentIndex()
            selected_info = self.soundpacks[selected.row()]

            self.viewname_le.setText(selected_info['VIEW'])
            self.name_le.setText(selected_info['NAME'])
            self.path_label.setText(_('Path:'))
            self.path_le.setText(selected_info['path'])
            self.size_le.setText(sizeof_fmt(selected_info['size']))
            self.homepage_tb.setText('')

            if selected_info['enabled']:
                self.disable_existing_button.setText(_('Disable'))
            else:
                self.disable_existing_button.setText(_('Enable'))

        if not self.tab_disabled:
            self.disable_existing_button.setEnabled(True)
            self.delete_existing_button.setEnabled(True)

        self.install_new_button.setEnabled(False)

        repository_selection = self.repository_lv.selectionModel()
        if repository_selection is not None:
            repository_selection.clearSelection()

    def repository_selection(self, selected, previous):
        self.repository_clicked()

    def repository_clicked(self):
        selection_model = self.repository_lv.selectionModel()
        if selection_model is not None and selection_model.hasSelection():
            selected = selection_model.currentIndex()
            selected_info = self.repo_soundpacks[selected.row()]

            self.viewname_le.setText(selected_info['viewname'])
            self.name_le.setText(selected_info['name'])

            if selected_info['type'] == 'direct_download':
                self.path_label.setText(_('Url:'))
                self.path_le.setText(selected_info['url'])
                self.homepage_tb.setText('<a href="{url}">{url}</a>'.format(
                    url=html.escape(selected_info['homepage'])))
                if 'size' not in selected_info:
                    if not (self.current_repo_info is not None
                        and self.http_reply is not None
                        and self.http_reply.isRunning()
                        and self.current_repo_info is selected_info):
                        if (self.http_reply is not None
                            and self.http_reply.isRunning()):
                            self.http_reply_aborted = True
                            self.http_reply.abort()

                        self.http_reply_aborted = False
                        self.size_le.setText(_('Getting remote size'))
                        self.current_repo_info = selected_info

                        request = QNetworkRequest(QUrl(selected_info['url']))
                        request.setRawHeader(b'User-Agent', cons.FAKE_USER_AGENT)

                        self.http_reply = self.qnam.head(request)
                        self.http_reply.finished.connect(
                            self.size_query_finished)
                else:
                    self.size_le.setText(sizeof_fmt(selected_info['size']))
            elif selected_info['type'] == 'browser_download':
                self.path_label.setText(_('Url:'))
                self.path_le.setText(selected_info['url'])
                self.homepage_tb.setText('<a href="{url}">{url}</a>'.format(
                    url=html.escape(selected_info['homepage'])))
                if 'size' in selected_info:
                    self.size_le.setText(sizeof_fmt(selected_info['size']))
                else:
                    self.size_le.setText(_('Unknown'))

        if (self.soundpacks_dir is not None
            and os.path.isdir(self.soundpacks_dir)
            and not self.tab_disabled):
            self.install_new_button.setEnabled(True)
        self.disable_existing_button.setEnabled(False)
        self.delete_existing_button.setEnabled(False)

        installed_selection = self.installed_lv.selectionModel()
        if installed_selection is not None:
            installed_selection.clearSelection()

    def size_query_finished(self):
        if (not self.http_reply_aborted
            and self.http_reply.attribute(
                QNetworkRequest.HttpStatusCodeAttribute) == 200
            and self.http_reply.hasRawHeader(b'Content-Length')):

            content_length = int(self.http_reply.rawHeader(b'Content-Length'))
            self.current_repo_info['size'] = content_length

            selection_model = self.repository_lv.selectionModel()
            if selection_model is not None and selection_model.hasSelection():
                selected = selection_model.currentIndex()
                selected_info = self.repo_soundpacks[selected.row()]

                if selected_info is self.current_repo_info:
                    self.size_le.setText(sizeof_fmt(content_length))
        else:
            selection_model = self.repository_lv.selectionModel()
            if selection_model is not None and selection_model.hasSelection():
                selected = selection_model.currentIndex()
                selected_info = self.repo_soundpacks[selected.row()]

                if selected_info is self.current_repo_info:
                    self.size_le.setText(_('Unknown'))

    def config_info(self, config_file):
        val = {}
        try:
            with open(config_file, 'r', encoding='latin1') as f:
                for line in f:
                    if line.startswith('NAME'):
                        space_index = line.find(' ')
                        name = line[space_index:].strip().replace(
                            ',', '')
                        val['NAME'] = name
                    elif line.startswith('VIEW'):
                        space_index = line.find(' ')
                        view = line[space_index:].strip()
                        val['VIEW'] = view

                    if 'NAME' in val and 'VIEW' in val:
                        break
        except FileNotFoundError:
            return val
        return val

    def scan_size(self, soundpack_info):
        next_scans = deque()
        current_scan = scandir(soundpack_info['path'])

        total_size = 0

        while True:
            try:
                entry = next(current_scan)
                if entry.is_dir():
                    next_scans.append(entry.path)
                elif entry.is_file():
                    total_size += entry.stat().st_size
            except StopIteration:
                if len(next_scans) > 0:
                    current_scan = scandir(next_scans.popleft())
                else:
                    break

        return total_size

    def add_soundpack(self, soundpack_info):
        index = self.soundpacks_model.rowCount()
        self.soundpacks_model.insertRows(self.soundpacks_model.rowCount(), 1)
        disabled_text = ''
        if not soundpack_info['enabled']:
            disabled_text = _(' (Disabled)')
        self.soundpacks_model.setData(self.soundpacks_model.index(index),
            soundpack_info['VIEW'] + disabled_text)

    def clear_soundpacks(self):
        self.game_dir = None
        self.soundpacks = []

        self.disable_existing_button.setEnabled(False)
        self.delete_existing_button.setEnabled(False)
        self.install_new_button.setEnabled(False)

        if self.soundpacks_model is not None:
            self.soundpacks_model.setStringList([])
        self.soundpacks_model = None

        repository_selection = self.repository_lv.selectionModel()
        if repository_selection is not None:
            repository_selection.clearSelection()

        self.viewname_le.setText('')
        self.name_le.setText('')
        self.path_le.setText('')
        self.size_le.setText('')
        self.homepage_tb.setText('')

    def game_dir_changed(self, new_dir):
        self.game_dir = new_dir
        self.soundpacks = []

        self.disable_existing_button.setEnabled(False)
        self.delete_existing_button.setEnabled(False)

        self.soundpacks_model = QStringListModel()
        self.installed_lv.setModel(self.soundpacks_model)
        self.installed_lv.selectionModel().currentChanged.connect(
            self.installed_selection)

        repository_selection = self.repository_lv.selectionModel()
        if repository_selection is not None:
            repository_selection.clearSelection()
        self.install_new_button.setEnabled(False)

        self.viewname_le.setText('')
        self.name_le.setText('')
        self.path_le.setText('')
        self.size_le.setText('')
        self.homepage_tb.setText('')

        soundpacks_dirs = [ 
            os.path.join(new_dir, 'data', 'sound'),
            os.path.join(new_dir,         'sound')  # User sound
        ]
        for soundpacks_dir in soundpacks_dirs:
            if os.path.isdir(soundpacks_dir):
                self.soundpacks_dir = soundpacks_dir

                dir_scan = scandir(soundpacks_dir)

                while True:
                    try:
                        entry = next(dir_scan)
                        if entry.is_dir():
                            soundpack_path = entry.path
                            config_file = os.path.join(soundpack_path,
                                'soundpack.txt')
                            if os.path.isfile(config_file):
                                info = self.config_info(config_file)
                                if 'NAME' in info and 'VIEW' in info:
                                    soundpack_info = {
                                        'path': soundpack_path,
                                        'enabled': True
                                    }
                                    soundpack_info.update(info)

                                    self.soundpacks.append(soundpack_info)
                                    soundpack_info['size'] = (
                                        self.scan_size(soundpack_info))
                                    self.add_soundpack(soundpack_info)
                                    continue
                            disabled_config_file = os.path.join(soundpack_path,
                                'soundpack.txt.disabled')
                            if os.path.isfile(disabled_config_file):
                                info = self.config_info(disabled_config_file)
                                if 'NAME' in info and 'VIEW' in info:
                                    soundpack_info = {
                                        'path': soundpack_path,
                                        'enabled': False
                                    }
                                    soundpack_info.update(info)

                                    self.soundpacks.append(soundpack_info)
                                    soundpack_info['size'] = (
                                        self.scan_size(soundpack_info))
                                    self.add_soundpack(soundpack_info)

                    except StopIteration:
                        break
            else:
                self.soundpacks_dir = None
예제 #2
0
class VAbstractNetworkClient(QObject):
    """Позволяет приложению отправлять сетевые запросы и получать на них ответы.

    Вся работа с сетью должна быть инкапсулирована в его наследниках.
    """
    @staticmethod
    def contentTypeFrom(reply: QNetworkReply, default=None):
        """Определяет и возвращает MIME-тип содержимого (со всеми вспомогательными данными, напр., кодировкой)
        из http-заголовка `Content-type` в ответе `reply`.
        Если тип содержимого определить невозможно, возвращает `default`.
        """
        assert reply
        contentType = reply.header(QNetworkRequest.ContentTypeHeader)
        if contentType:
            assert isinstance(contentType, str)  # TODO: Delete me!
            return contentType
        return default

    @staticmethod
    def encodingFrom(reply: QNetworkReply, default: str = "utf-8") -> str:
        """Определяет и возвращает кодировку содержимого из http-заголовка `Content-type` в ответе `reply`.
        Если кодировку определить невозможно, возвращает `default`.
        """
        missing = object()
        contentType = VAbstractNetworkClient.contentTypeFrom(reply, missing)

        if contentType is missing:
            return default

        try:
            charset = contentType.split(";")[1]
            assert "charset" in charset
            encoding = charset.split("=")[1]
            return encoding.strip()
        except:
            return default

    @staticmethod
    def waitForFinished(reply: QNetworkReply, timeout: int = -1):
        """Блокирует вызывающий метод на время, пока не будет завершен сетевой ответ `reply` (то есть пока
        не испустится сигнал `reply.finished`), или пока не истечет `timeout` миллисекунд.
        Если `timeout` меньше 0 (по умолчанию), то по данному таймеру блокировка отменяться не будет.
        """
        if reply.isFinished():
            return

        event_loop = QEventLoop()
        reply.finished.connect(event_loop.quit)
        if timeout >= 0:
            timer = QTimer()
            timer.setInterval(timeout)
            timer.setSingleShot(True)
            timer.timeout.connect(event_loop.quit)
            # Если блокировка отменится до истечения таймера, то при выходе из метода таймер остановится и уничтожится.
            timer.start()
        event_loop.exec()
        reply.finished.disconnect(event_loop.quit)

    networkAccessManagerChanged = pyqtSignal(QNetworkAccessManager,
                                             arguments=['manager'])
    """Сигнал об изменении менеджера доступа к сети.

    :param QNetworkAccessManager manager: Новый менеджер доступа к сети.
    """

    baseUrlChanged = pyqtSignal(QUrl, arguments=['url'])
    """Сигнал об изменении базового url-а.

    :param QUrl url: Новый базовый url.
    """

    replyFinished = pyqtSignal(QNetworkReply, arguments=['reply'])
    """Сигнал о завершении ответа на сетевой запрос.

    :param QNetworkReply reply: Завершенный сетевой запрос.
    """

    def __init__(self, parent: QObject = None):
        super().__init__(parent)

        self.__networkAccessManager = QNetworkAccessManager(parent=self)
        self.__baseUrl = QUrl()

    def getNetworkAccessManager(self) -> QNetworkAccessManager:
        """Возвращает менеджер доступа к сети."""
        return self.__networkAccessManager

    def setNetworkAccessManager(self, manager: QNetworkAccessManager):
        """Устанавливает менеджер доступа к сети."""
        assert manager
        if manager is self.__networkAccessManager:
            return
        if self.__networkAccessManager.parent() is self:
            self.__networkAccessManager.deleteLater()
        self.__networkAccessManager = manager
        self.networkAccessManagerChanged.emit(manager)

    networkAccessManager = pyqtProperty(type=QNetworkAccessManager,
                                        fget=getNetworkAccessManager,
                                        fset=setNetworkAccessManager,
                                        notify=networkAccessManagerChanged,
                                        doc="Менеджер доступа к сети.")

    def getBaseUrl(self) -> QUrl:
        """Возвращает базовый url."""
        return QUrl(self.__baseUrl)

    def setBaseUrl(self, url: QUrl):
        """Устанавливает базовый url."""
        if url == self.__baseUrl:
            return
        self.__baseUrl = QUrl(url)
        self.baseUrlChanged.emit(url)

    baseUrl = pyqtProperty(type=QUrl,
                           fget=getBaseUrl,
                           fset=setBaseUrl,
                           notify=baseUrlChanged,
                           doc="Базовый url.")

    def _connectReplySignals(self, reply: QNetworkReply):
        """Соединяет сигналы ответа с сигналами клиента."""
        reply.finished.connect(lambda: self.replyFinished.emit(reply))
        # TODO: Добавить сюда подключение остальных сигналов.

    def _get(self, request: QNetworkRequest) -> QNetworkReply:
        """Запускает отправку GET-запроса и возвращает ответ :class:`QNetworkReply` на него."""
        reply = self.__networkAccessManager.get(request)
        self._connectReplySignals(reply)
        return reply

    def _head(self, request: QNetworkRequest) -> QNetworkReply:
        """Запускает отправку HEAD-запроса и возвращает ответ :class:`QNetworkReply` на него."""
        reply = self.__networkAccessManager.head(request)
        self._connectReplySignals(reply)
        return reply

    def _post(self, request: QNetworkRequest, data=None) -> QNetworkReply:
        """Запускает отправку POST-запроса и возвращает ответ :class:`QNetworkReply` на него.

        _post(self, request: QNetworkRequest) -> QNetworkReply.
        _post(self, request: QNetworkRequest, data: bytes) -> QNetworkReply.
        _post(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply.
        _post(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply.
        _post(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply.
        _post(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply.
        """
        if data is not None:
            reply = self.__networkAccessManager.post(request, data)
        else:
            reply = self.__networkAccessManager.sendCustomRequest(
                request, b"POST")
        self._connectReplySignals(reply)
        return reply

    def _put(self, request: QNetworkRequest, data=None) -> QNetworkReply:
        """Запускает отправку PUT-запроса и возвращает ответ :class:`QNetworkReply` на него.

        _put(self, request: QNetworkRequest) -> QNetworkReply.
        _put(self, request: QNetworkRequest, data: bytes) -> QNetworkReply.
        _put(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply.
        _put(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply.
        _put(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply.
        _put(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply.
        """
        if data is not None:
            reply = self.__networkAccessManager.put(request, data)
        else:
            reply = self.__networkAccessManager.sendCustomRequest(
                request, b"PUT")
        self._connectReplySignals(reply)
        return reply

    def _delete(self, request: QNetworkRequest, data=None) -> QNetworkReply:
        """Запускает отправку DELETE-запроса и возвращает ответ :class:`QNetworkReply` на него.

        _delete(self, request: QNetworkRequest) -> QNetworkReply.
        _delete(self, request: QNetworkRequest, data: bytes) -> QNetworkReply.
        _delete(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply.
        _delete(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply.
        _delete(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply.
        _delete(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply.
        """
        if data is not None:
            reply = self.__networkAccessManager.deleteResource(request)
        else:
            reply = self.__networkAccessManager.sendCustomRequest(
                request, b"DELETE")
        self._connectReplySignals(reply)
        return reply

    def _sendCustomRequest(self,
                           request: QNetworkRequest,
                           verb: bytes,
                           data=None) -> QNetworkReply:
        """Запускает отправку пользовательского запроса и возвращает ответ :class:`QNetworkReply` на него.

        _sendCustomRequest(self, request: QNetworkRequest, verb: bytes) -> QNetworkReply.
        _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: bytes) -> QNetworkReply.
        _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: bytearray) -> QNetworkReply.
        _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QByteArray) -> QNetworkReply.
        _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QIODevice) -> QNetworkReply.
        _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QHttpMultiPart) -> QNetworkReply.
        """
        reply = self.__networkAccessManager.sendCustomRequest(
            request, verb, data)
        self._connectReplySignals(reply)
        return reply