forked from openlp/openlp
working webview implementation of the ccli importer
Still a WIP: - I'll add the old preview thing - Need to make default size of the window larger - Need to make UI look better
This commit is contained in:
parent
a036362237
commit
483f14ec15
@ -27,7 +27,7 @@ from PyQt5 import QtCore, QtWidgets
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.ui import SingleColumnTableWidget
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.widgets.edits import HistoryComboBox
|
||||
from openlp.plugins.songs.forms.webengine import WebEngineView
|
||||
|
||||
|
||||
class Ui_SongSelectDialog(object):
|
||||
@ -36,122 +36,23 @@ class Ui_SongSelectDialog(object):
|
||||
"""
|
||||
def setup_ui(self, songselect_dialog):
|
||||
songselect_dialog.setObjectName('songselect_dialog')
|
||||
songselect_dialog.resize(616, 378)
|
||||
songselect_dialog.resize(800, 600)
|
||||
self.songselect_layout = QtWidgets.QVBoxLayout(songselect_dialog)
|
||||
self.songselect_layout.setSpacing(0)
|
||||
self.songselect_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.songselect_layout.setSpacing(8)
|
||||
self.songselect_layout.setContentsMargins(8, 8, 8, 8)
|
||||
self.songselect_layout.setObjectName('songselect_layout')
|
||||
self.stacked_widget = QtWidgets.QStackedWidget(songselect_dialog)
|
||||
self.stacked_widget.setObjectName('stacked_widget')
|
||||
self.login_page = QtWidgets.QWidget()
|
||||
self.login_page.setObjectName('login_page')
|
||||
self.login_layout = QtWidgets.QFormLayout(self.login_page)
|
||||
self.login_layout.setContentsMargins(120, 100, 120, 100)
|
||||
self.login_layout.setSpacing(8)
|
||||
self.login_layout.setObjectName('login_layout')
|
||||
self.notice_layout = QtWidgets.QHBoxLayout()
|
||||
self.notice_layout.setObjectName('notice_layout')
|
||||
self.notice_label = QtWidgets.QLabel(self.login_page)
|
||||
self.notice_label.setWordWrap(True)
|
||||
self.notice_label.setObjectName('notice_label')
|
||||
self.notice_layout.addWidget(self.notice_label)
|
||||
self.login_layout.setLayout(0, QtWidgets.QFormLayout.SpanningRole, self.notice_layout)
|
||||
self.username_label = QtWidgets.QLabel(self.login_page)
|
||||
self.username_label.setObjectName('usernameLabel')
|
||||
self.login_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.username_label)
|
||||
self.username_edit = QtWidgets.QLineEdit(self.login_page)
|
||||
self.username_edit.setObjectName('usernameEdit')
|
||||
self.login_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.username_edit)
|
||||
self.password_label = QtWidgets.QLabel(self.login_page)
|
||||
self.password_label.setObjectName('passwordLabel')
|
||||
self.login_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.password_label)
|
||||
self.password_edit = QtWidgets.QLineEdit(self.login_page)
|
||||
self.password_edit.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.password_edit.setObjectName('passwordEdit')
|
||||
self.login_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.password_edit)
|
||||
self.save_password_checkbox = QtWidgets.QCheckBox(self.login_page)
|
||||
self.save_password_checkbox.setTristate(False)
|
||||
self.save_password_checkbox.setObjectName('save_password_checkbox')
|
||||
self.login_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.save_password_checkbox)
|
||||
self.login_button_layout = QtWidgets.QHBoxLayout()
|
||||
self.login_button_layout.setSpacing(8)
|
||||
self.login_button_layout.setContentsMargins(0, -1, -1, -1)
|
||||
self.login_button_layout.setObjectName('login_button_layout')
|
||||
self.login_spacer = QtWidgets.QWidget(self.login_page)
|
||||
self.login_spacer.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.login_spacer.setObjectName('login_spacer')
|
||||
self.login_button_layout.addWidget(self.login_spacer)
|
||||
self.login_progress_bar = QtWidgets.QProgressBar(self.login_page)
|
||||
self.login_progress_bar.setMinimum(0)
|
||||
self.login_progress_bar.setMaximum(3)
|
||||
self.login_progress_bar.setValue(0)
|
||||
self.login_progress_bar.setMinimumWidth(200)
|
||||
self.login_progress_bar.setVisible(False)
|
||||
self.login_button_layout.addWidget(self.login_progress_bar)
|
||||
self.login_button = QtWidgets.QPushButton(self.login_page)
|
||||
self.login_button.setIcon(UiIcons().edit)
|
||||
self.login_button.setObjectName('login_button')
|
||||
self.login_button_layout.addWidget(self.login_button)
|
||||
self.login_layout.setLayout(4, QtWidgets.QFormLayout.SpanningRole, self.login_button_layout)
|
||||
self.stacked_widget.addWidget(self.login_page)
|
||||
self.search_page = QtWidgets.QWidget()
|
||||
self.search_page.setObjectName('search_page')
|
||||
self.search_layout = QtWidgets.QVBoxLayout(self.search_page)
|
||||
self.search_layout.setSpacing(8)
|
||||
self.search_layout.setContentsMargins(8, 8, 8, 8)
|
||||
self.search_layout.setObjectName('search_layout')
|
||||
self.search_input_layout = QtWidgets.QHBoxLayout()
|
||||
self.search_input_layout.setSpacing(8)
|
||||
self.search_input_layout.setObjectName('search_input_layout')
|
||||
self.search_label = QtWidgets.QLabel(self.search_page)
|
||||
self.search_label.setObjectName('search_label')
|
||||
self.search_input_layout.addWidget(self.search_label)
|
||||
self.search_combobox = HistoryComboBox(self.search_page)
|
||||
self.search_combobox.setObjectName('search_combobox')
|
||||
self.search_input_layout.addWidget(self.search_combobox)
|
||||
self.search_button = QtWidgets.QPushButton(self.search_page)
|
||||
self.search_button.setIcon(UiIcons().search)
|
||||
self.search_button.setObjectName('search_button')
|
||||
self.search_input_layout.addWidget(self.search_button)
|
||||
self.search_layout.addLayout(self.search_input_layout)
|
||||
self.search_progress_layout = QtWidgets.QHBoxLayout()
|
||||
self.search_progress_layout.setSpacing(8)
|
||||
self.search_progress_layout.setObjectName('search_progress_layout')
|
||||
self.search_progress_bar = QtWidgets.QProgressBar(self.search_page)
|
||||
self.search_progress_bar.setMinimum(0)
|
||||
self.search_progress_bar.setMaximum(3)
|
||||
self.search_progress_bar.setValue(0)
|
||||
self.search_progress_layout.addWidget(self.search_progress_bar)
|
||||
self.stop_button = QtWidgets.QPushButton(self.search_page)
|
||||
self.stop_button.setIcon(UiIcons().stop)
|
||||
self.stop_button.setObjectName('stop_button')
|
||||
self.search_progress_layout.addWidget(self.stop_button)
|
||||
self.search_layout.addLayout(self.search_progress_layout)
|
||||
self.search_results_widget = QtWidgets.QListWidget(self.search_page)
|
||||
self.search_results_widget.setProperty("showDropIndicator", False)
|
||||
self.search_results_widget.setAlternatingRowColors(True)
|
||||
self.search_results_widget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.search_results_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.search_results_widget.setObjectName('search_results_widget')
|
||||
self.search_layout.addWidget(self.search_results_widget)
|
||||
self.result_count_label = QtWidgets.QLabel(self.search_page)
|
||||
self.result_count_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
|
||||
self.result_count_label.setObjectName('result_count_label')
|
||||
self.search_layout.addWidget(self.result_count_label)
|
||||
self.view_layout = QtWidgets.QHBoxLayout()
|
||||
self.view_layout.setSpacing(8)
|
||||
self.view_layout.setObjectName('view_layout')
|
||||
self.logout_button = QtWidgets.QPushButton(self.search_page)
|
||||
self.logout_button.setIcon(UiIcons().edit)
|
||||
self.view_layout.addWidget(self.logout_button)
|
||||
self.view_spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.view_layout.addItem(self.view_spacer)
|
||||
self.view_button = QtWidgets.QPushButton(self.search_page)
|
||||
self.view_button.setIcon(UiIcons().search)
|
||||
self.view_button.setObjectName('view_button')
|
||||
self.view_layout.addWidget(self.view_button)
|
||||
self.search_layout.addLayout(self.view_layout)
|
||||
self.stacked_widget.addWidget(self.search_page)
|
||||
# Webview page
|
||||
self.webview_page = QtWidgets.QWidget()
|
||||
self.webview_page.setObjectName('webview_page')
|
||||
self.webview_layout = QtWidgets.QGridLayout(self.webview_page)
|
||||
self.webview_layout.setObjectName('webview_layout')
|
||||
self.webview_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.webview = WebEngineView(self)
|
||||
self.webview_layout.addWidget(self.webview, 1, 0, 3, 1)
|
||||
self.stacked_widget.addWidget(self.webview_page)
|
||||
# Song page
|
||||
self.song_page = QtWidgets.QWidget()
|
||||
self.song_page.setObjectName('song_page')
|
||||
self.song_layout = QtWidgets.QGridLayout(self.song_page)
|
||||
@ -193,36 +94,54 @@ class Ui_SongSelectDialog(object):
|
||||
self.lyrics_table_widget.setObjectName('lyrics_table_widget')
|
||||
self.lyrics_table_widget.setRowCount(0)
|
||||
self.song_layout.addWidget(self.lyrics_table_widget, 3, 1, 1, 3)
|
||||
self.song_progress_bar = QtWidgets.QProgressBar(self.song_page)
|
||||
self.song_progress_bar.setMinimum(0)
|
||||
self.song_progress_bar.setMaximum(3)
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.song_progress_bar.setVisible(False)
|
||||
self.song_layout.addWidget(self.song_progress_bar, 4, 0, 1, 4)
|
||||
self.import_layout = QtWidgets.QHBoxLayout()
|
||||
self.import_layout.setObjectName('import_layout')
|
||||
self.back_button = QtWidgets.QPushButton(self.song_page)
|
||||
self.back_button.setIcon(UiIcons().back)
|
||||
self.back_button.setObjectName('back_button')
|
||||
self.import_layout.addWidget(self.back_button)
|
||||
self.import_spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Minimum)
|
||||
self.import_layout.addItem(self.import_spacer)
|
||||
self.import_button = QtWidgets.QPushButton(self.song_page)
|
||||
self.import_button.setIcon(UiIcons().download)
|
||||
self.import_button.setObjectName('import_button')
|
||||
self.import_layout.addWidget(self.import_button)
|
||||
self.song_layout.addLayout(self.import_layout, 5, 0, 1, 5)
|
||||
self.stacked_widget.addWidget(self.song_page)
|
||||
self.songselect_layout.addWidget(self.stacked_widget)
|
||||
self.username_label.setBuddy(self.username_edit)
|
||||
self.password_label.setBuddy(self.password_edit)
|
||||
self.title_label.setBuddy(self.title_edit)
|
||||
self.authors_label.setBuddy(self.author_list_widget)
|
||||
self.copyright_label.setBuddy(self.copyright_edit)
|
||||
self.ccli_label.setBuddy(self.ccli_edit)
|
||||
self.lyrics_label.setBuddy(self.lyrics_table_widget)
|
||||
|
||||
self.stacked_widget.addWidget(self.song_page)
|
||||
# The top panel
|
||||
self.top_button_layout = QtWidgets.QGridLayout()
|
||||
self.top_button_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.top_button_layout.setSpacing(8)
|
||||
self.top_button_layout.setObjectName('top_button_layout')
|
||||
self.back_button = QtWidgets.QPushButton(songselect_dialog)
|
||||
self.back_button.setIcon(UiIcons().back)
|
||||
self.back_button.setObjectName('back_button')
|
||||
self.top_button_layout.addWidget(self.back_button, 0, 0, 1, 1)
|
||||
self.url_bar = QtWidgets.QLineEdit(songselect_dialog)
|
||||
self.url_bar.setObjectName('ccli_edit')
|
||||
self.top_button_layout.addWidget(self.url_bar, 0, 1, 1, 8)
|
||||
self.song_progress_bar = QtWidgets.QProgressBar(songselect_dialog)
|
||||
self.song_progress_bar.setMinimum(0)
|
||||
self.song_progress_bar.setMaximum(3)
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.song_progress_bar.setVisible(False)
|
||||
self.top_button_layout.addWidget(self.song_progress_bar, 0, 1, 1, 8)
|
||||
# The bottom panel
|
||||
self.bottom_button_layout = QtWidgets.QGridLayout()
|
||||
self.bottom_button_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.bottom_button_layout.setSpacing(8)
|
||||
self.bottom_button_layout.setObjectName('bottom_button_layout')
|
||||
self.close_button = QtWidgets.QPushButton(songselect_dialog)
|
||||
self.close_button.setIcon(UiIcons().close)
|
||||
self.close_button.setObjectName('close_button')
|
||||
self.bottom_button_layout.addWidget(self.close_button, 0, 0, 1, 1)
|
||||
self.spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Minimum)
|
||||
self.bottom_button_layout.addItem(self.spacer, 0, 1, 1, 7)
|
||||
self.view_button = QtWidgets.QPushButton(songselect_dialog)
|
||||
self.view_button.setIcon(UiIcons().search)
|
||||
self.view_button.setObjectName('view_button')
|
||||
self.bottom_button_layout.addWidget(self.view_button, 0, 8, 1, 1)
|
||||
self.import_button = QtWidgets.QPushButton(songselect_dialog)
|
||||
self.import_button.setIcon(UiIcons().download)
|
||||
self.import_button.setObjectName('import_button')
|
||||
self.bottom_button_layout.addWidget(self.import_button, 0, 9, 1, 1)
|
||||
# Add everything to the interface layout
|
||||
self.songselect_layout.addLayout(self.top_button_layout)
|
||||
self.songselect_layout.addWidget(self.stacked_widget)
|
||||
self.songselect_layout.addLayout(self.bottom_button_layout)
|
||||
self.retranslate_ui(songselect_dialog)
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
|
||||
@ -231,25 +150,13 @@ class Ui_SongSelectDialog(object):
|
||||
Translate the GUI.
|
||||
"""
|
||||
songselect_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'CCLI SongSelect Importer'))
|
||||
self.notice_label.setText(
|
||||
translate('SongsPlugin.SongSelectForm', '<strong>Note:</strong> '
|
||||
'An Internet connection is required in order to import songs from CCLI SongSelect.')
|
||||
)
|
||||
self.username_label.setText(translate('SongsPlugin.SongSelectForm', 'Username:'))
|
||||
self.password_label.setText(translate('SongsPlugin.SongSelectForm', 'Password:'))
|
||||
self.save_password_checkbox.setText(translate('SongsPlugin.SongSelectForm', 'Save username and password'))
|
||||
self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login'))
|
||||
self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:'))
|
||||
self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search'))
|
||||
self.stop_button.setText(translate('SongsPlugin.SongSelectForm', 'Stop'))
|
||||
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
|
||||
'Found {count:d} song(s)').format(count=0))
|
||||
self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout'))
|
||||
self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View'))
|
||||
self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'Preview'))
|
||||
self.title_label.setText(translate('SongsPlugin.SongSelectForm', 'Title:'))
|
||||
self.authors_label.setText(translate('SongsPlugin.SongSelectForm', 'Author(s):'))
|
||||
self.copyright_label.setText(translate('SongsPlugin.SongSelectForm', 'Copyright:'))
|
||||
self.ccli_label.setText(translate('SongsPlugin.SongSelectForm', 'CCLI Number:'))
|
||||
self.lyrics_label.setText(translate('SongsPlugin.SongSelectForm', 'Lyrics:'))
|
||||
self.back_button.setText(translate('SongsPlugin.SongSelectForm', 'Back'))
|
||||
self.back_button.setText(translate('SongsPlugin.SongSelectForm', 'Back',
|
||||
'When pressed takes user to the CCLI home page'))
|
||||
self.import_button.setText(translate('SongsPlugin.SongSelectForm', 'Import'))
|
||||
self.close_button.setText(translate('SongsPlugin.SongSelectForm', 'Close'))
|
||||
|
@ -22,56 +22,20 @@
|
||||
The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI for the SongSelect importer
|
||||
"""
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from sqlalchemy.sql import and_
|
||||
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.threading import ThreadWorker, run_thread
|
||||
from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
|
||||
from openlp.plugins.songs.lib.songselect import SongSelectImport
|
||||
from openlp.plugins.songs.lib.db import Song
|
||||
from openlp.plugins.songs.lib.songselect import SongSelectImport, Pages
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SearchWorker(ThreadWorker):
|
||||
"""
|
||||
Run the actual SongSelect search, and notify the GUI when we find each song.
|
||||
"""
|
||||
show_info = QtCore.pyqtSignal(str, str)
|
||||
found_song = QtCore.pyqtSignal(dict)
|
||||
finished = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, importer, search_text):
|
||||
super().__init__()
|
||||
self.importer = importer
|
||||
self.search_text = search_text
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Run a search and then parse the results page of the search.
|
||||
"""
|
||||
songs = self.importer.search(self.search_text, 1000, self._found_song_callback)
|
||||
if len(songs) >= 1000:
|
||||
self.show_info.emit(
|
||||
translate('SongsPlugin.SongSelectForm', 'More than 1000 results'),
|
||||
translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has '
|
||||
'been stopped. Please refine your search to fetch better '
|
||||
'results.'))
|
||||
self.finished.emit()
|
||||
self.quit.emit()
|
||||
|
||||
def _found_song_callback(self, song):
|
||||
"""
|
||||
A callback used by the paginate function to notify watching processes when it finds a song.
|
||||
|
||||
:param song: The song that was found
|
||||
"""
|
||||
self.found_song.emit(song)
|
||||
|
||||
|
||||
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties):
|
||||
"""
|
||||
The :class:`SongSelectForm` class is the SongSelect dialog.
|
||||
@ -88,43 +52,26 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
|
||||
"""
|
||||
Initialise the SongSelectForm
|
||||
"""
|
||||
self.song_count = 0
|
||||
self.song = None
|
||||
self.set_progress_visible(False)
|
||||
self.song_select_importer = SongSelectImport(self.db_manager)
|
||||
self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
|
||||
self.login_button.clicked.connect(self.on_login_button_clicked)
|
||||
self.search_button.clicked.connect(self.on_search_button_clicked)
|
||||
self.search_combobox.returnPressed.connect(self.on_search_button_clicked)
|
||||
self.stop_button.clicked.connect(self.on_stop_button_clicked)
|
||||
self.logout_button.clicked.connect(self.done)
|
||||
self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked)
|
||||
self.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed)
|
||||
self.song_select_importer = SongSelectImport(self.db_manager, self.webview)
|
||||
self.url_bar.returnPressed.connect(self.on_url_bar_return_pressed)
|
||||
self.view_button.clicked.connect(self.on_view_button_clicked)
|
||||
self.back_button.clicked.connect(self.on_back_button_clicked)
|
||||
self.close_button.clicked.connect(self.done)
|
||||
self.import_button.clicked.connect(self.on_import_button_clicked)
|
||||
self.webview.page().loadStarted.connect(self.page_load_started)
|
||||
self.webview.page().loadFinished.connect(self.page_loaded)
|
||||
|
||||
def exec(self):
|
||||
"""
|
||||
Execute the dialog. This method sets everything back to its initial
|
||||
values.
|
||||
"""
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
self.username_edit.setEnabled(True)
|
||||
self.password_edit.setEnabled(True)
|
||||
self.save_password_checkbox.setEnabled(True)
|
||||
self.search_combobox.clearEditText()
|
||||
self.search_combobox.clear()
|
||||
self.search_results_widget.clear()
|
||||
self.song_select_importer.reset_webview()
|
||||
self.view_button.setEnabled(False)
|
||||
if self.settings.contains('songs/songselect password'):
|
||||
self.username_edit.setText(self.settings.value('songs/songselect username'))
|
||||
self.password_edit.setText(self.settings.value('songs/songselect password'))
|
||||
self.save_password_checkbox.setChecked(True)
|
||||
if self.settings.contains('songs/songselect searches'):
|
||||
self.search_combobox.addItems(
|
||||
self.settings.value('songs/songselect searches').split('|'))
|
||||
self.username_edit.setFocus()
|
||||
self.back_button.setEnabled(False)
|
||||
self.import_button.setEnabled(False)
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
return QtWidgets.QDialog.exec(self)
|
||||
|
||||
def done(self, result_code):
|
||||
@ -133,279 +80,127 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
|
||||
|
||||
:param result_code: The result of the dialog.
|
||||
"""
|
||||
log.debug('Closing SongSelectForm')
|
||||
if self.stacked_widget.currentIndex() > 0:
|
||||
progress_dialog = QtWidgets.QProgressDialog(
|
||||
translate('SongsPlugin.SongSelectForm', 'Logging out...'), '', 0, 2, self)
|
||||
progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress_dialog.setCancelButton(None)
|
||||
progress_dialog.setValue(1)
|
||||
progress_dialog.show()
|
||||
progress_dialog.setFocus()
|
||||
self.application.process_events()
|
||||
sleep(0.5)
|
||||
self.application.process_events()
|
||||
self.song_select_importer.logout()
|
||||
self.application.process_events()
|
||||
progress_dialog.setValue(2)
|
||||
return QtWidgets.QDialog.done(self, result_code)
|
||||
|
||||
def _update_login_progress(self):
|
||||
"""
|
||||
Update the progress bar as the user logs in.
|
||||
"""
|
||||
self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
|
||||
self.application.process_events()
|
||||
def page_load_started(self):
|
||||
self.song_progress_bar.setMaximum(0)
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.song_progress_bar.setVisible(True)
|
||||
self.url_bar.setVisible(False)
|
||||
self.import_button.setEnabled(False)
|
||||
self.view_button.setEnabled(False)
|
||||
self.back_button.setEnabled(False)
|
||||
|
||||
def page_loaded(self, successful):
|
||||
self.song = None
|
||||
page_type = self.song_select_importer.get_page_type()
|
||||
if page_type == Pages.Login:
|
||||
self.signin_page_loaded()
|
||||
elif page_type == Pages.Song:
|
||||
self.song_progress_bar.setMaximum(3)
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.song = self.song_select_importer.get_song(self._update_song_progress)
|
||||
self.import_button.setEnabled(True)
|
||||
self.view_button.setEnabled(True)
|
||||
self.back_button.setEnabled(True)
|
||||
if page_type == Pages.Other:
|
||||
self.back_button.setEnabled(True)
|
||||
self.song_progress_bar.setVisible(False)
|
||||
self.url_bar.setText(self.webview.url().toString())
|
||||
self.url_bar.setCursorPosition(0)
|
||||
self.url_bar.setVisible(True)
|
||||
|
||||
def signin_page_loaded(self):
|
||||
username = self.settings.value('songs/songselect username')
|
||||
password = self.settings.value('songs/songselect password')
|
||||
self.song_select_importer.set_login_fields(username, password)
|
||||
|
||||
def _update_song_progress(self):
|
||||
"""
|
||||
Update the progress bar as the song is being downloaded.
|
||||
Update the progress bar.
|
||||
"""
|
||||
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
|
||||
self.application.process_events()
|
||||
|
||||
def _view_song(self, current_item):
|
||||
def _view_song(self):
|
||||
"""
|
||||
Load a song into the song view.
|
||||
"""
|
||||
if not current_item:
|
||||
return
|
||||
else:
|
||||
current_item = current_item.data(QtCore.Qt.UserRole)
|
||||
# Stop the current search, if it's running
|
||||
self.song_select_importer.stop()
|
||||
# Clear up the UI
|
||||
self.song_progress_bar.setVisible(True)
|
||||
self.import_button.setEnabled(False)
|
||||
self.back_button.setEnabled(False)
|
||||
self.title_edit.setText('')
|
||||
self.title_edit.setEnabled(False)
|
||||
self.copyright_edit.setText('')
|
||||
self.copyright_edit.setEnabled(False)
|
||||
self.ccli_edit.setText('')
|
||||
self.ccli_edit.setEnabled(False)
|
||||
self.author_list_widget.clear()
|
||||
self.author_list_widget.setEnabled(False)
|
||||
self.lyrics_table_widget.clear()
|
||||
self.lyrics_table_widget.setRowCount(0)
|
||||
self.lyrics_table_widget.setEnabled(False)
|
||||
self.stacked_widget.setCurrentIndex(2)
|
||||
song = {}
|
||||
for key, value in current_item.items():
|
||||
song[key] = value
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.application.process_events()
|
||||
# Get the full song
|
||||
song = self.song_select_importer.get_song(song, self._update_song_progress)
|
||||
if not song:
|
||||
if not self.song:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self, translate('SongsPlugin.SongSelectForm', 'Incomplete song'),
|
||||
translate('SongsPlugin.SongSelectForm', 'This song is missing some information, like the lyrics, '
|
||||
'and cannot be imported.'),
|
||||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok), QtWidgets.QMessageBox.Ok)
|
||||
self.stacked_widget.setCurrentIndex(1)
|
||||
return
|
||||
# Clear up the UI
|
||||
self.author_list_widget.clear()
|
||||
self.lyrics_table_widget.clear()
|
||||
self.lyrics_table_widget.setRowCount(0)
|
||||
# Update the UI
|
||||
self.title_edit.setText(song['title'])
|
||||
self.copyright_edit.setText(song['copyright'])
|
||||
self.ccli_edit.setText(song['ccli_number'])
|
||||
for author in song['authors']:
|
||||
QtWidgets.QListWidgetItem(author, self.author_list_widget)
|
||||
for counter, verse in enumerate(song['verses']):
|
||||
self.title_edit.setText(self.song['title'])
|
||||
self.copyright_edit.setText(self.song['copyright'])
|
||||
self.ccli_edit.setText(self.song['ccli_number'])
|
||||
for author in self.song['authors']:
|
||||
self.author_list_widget.addItem(QtWidgets.QListWidgetItem(author, self.author_list_widget))
|
||||
for counter, verse in enumerate(self.song['verses']):
|
||||
self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1)
|
||||
item = QtWidgets.QTableWidgetItem(verse['lyrics'])
|
||||
item.setData(QtCore.Qt.UserRole, verse['label'])
|
||||
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
|
||||
self.lyrics_table_widget.setItem(counter, 0, item)
|
||||
self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in song['verses']])
|
||||
self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in self.song['verses']])
|
||||
self.lyrics_table_widget.resizeRowsToContents()
|
||||
self.title_edit.setEnabled(True)
|
||||
self.copyright_edit.setEnabled(True)
|
||||
self.ccli_edit.setEnabled(True)
|
||||
self.author_list_widget.setEnabled(True)
|
||||
self.lyrics_table_widget.setEnabled(True)
|
||||
self.lyrics_table_widget.repaint()
|
||||
self.import_button.setEnabled(True)
|
||||
self.back_button.setEnabled(True)
|
||||
self.song_progress_bar.setVisible(False)
|
||||
self.song_progress_bar.setValue(0)
|
||||
self.song = song
|
||||
self.application.process_events()
|
||||
|
||||
def on_save_password_checkbox_toggled(self, checked):
|
||||
"""
|
||||
Show a warning dialog when the user toggles the save checkbox on or off.
|
||||
|
||||
:param checked: If the combobox is checked or not
|
||||
"""
|
||||
if checked and self.login_page.isVisible():
|
||||
answer = QtWidgets.QMessageBox.question(
|
||||
self, translate('SongsPlugin.SongSelectForm', 'Save Username and Password'),
|
||||
translate('SongsPlugin.SongSelectForm', 'WARNING: Saving your username and password is INSECURE, your '
|
||||
'password is stored in PLAIN TEXT. Click Yes to save your '
|
||||
'password or No to cancel this.'),
|
||||
defaultButton=QtWidgets.QMessageBox.No)
|
||||
if answer == QtWidgets.QMessageBox.No:
|
||||
self.save_password_checkbox.setChecked(False)
|
||||
|
||||
def on_login_button_clicked(self):
|
||||
"""
|
||||
Log the user in to SongSelect.
|
||||
"""
|
||||
self.username_edit.setEnabled(False)
|
||||
self.password_edit.setEnabled(False)
|
||||
self.save_password_checkbox.setEnabled(False)
|
||||
self.login_button.setEnabled(False)
|
||||
self.login_spacer.setVisible(False)
|
||||
self.login_progress_bar.setValue(0)
|
||||
self.login_progress_bar.setVisible(True)
|
||||
self.application.process_events()
|
||||
# Log the user in
|
||||
subscription_level = self.song_select_importer.login(
|
||||
self.username_edit.text(), self.password_edit.text(), self._update_login_progress)
|
||||
if not subscription_level:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
|
||||
translate('SongsPlugin.SongSelectForm',
|
||||
'There was a problem logging in, perhaps your username or password is incorrect?')
|
||||
)
|
||||
else:
|
||||
if subscription_level == 'Free':
|
||||
QtWidgets.QMessageBox.information(
|
||||
self,
|
||||
translate('SongsPlugin.SongSelectForm', 'Free user'),
|
||||
translate('SongsPlugin.SongSelectForm', 'You logged in with a free account, '
|
||||
'the search will be limited to songs '
|
||||
'in the public domain.')
|
||||
)
|
||||
if self.save_password_checkbox.isChecked():
|
||||
self.settings.setValue('songs/songselect username', self.username_edit.text())
|
||||
self.settings.setValue('songs/songselect password', self.password_edit.text())
|
||||
else:
|
||||
self.settings.remove('songs/songselect username')
|
||||
self.settings.remove('songs/songselect password')
|
||||
self.lyrics_table_widget.scrollToTop()
|
||||
self.stacked_widget.setCurrentIndex(1)
|
||||
self.login_progress_bar.setVisible(False)
|
||||
self.login_progress_bar.setValue(0)
|
||||
self.login_spacer.setVisible(True)
|
||||
self.login_button.setEnabled(True)
|
||||
self.username_edit.setEnabled(True)
|
||||
self.password_edit.setEnabled(True)
|
||||
self.save_password_checkbox.setEnabled(True)
|
||||
self.search_combobox.setFocus()
|
||||
self.application.process_events()
|
||||
|
||||
def on_search_button_clicked(self):
|
||||
def on_url_bar_return_pressed(self):
|
||||
"""
|
||||
Run a search on SongSelect.
|
||||
Go to the url in the url bar
|
||||
"""
|
||||
# Set up UI components
|
||||
self.view_button.setEnabled(False)
|
||||
self.search_button.setEnabled(False)
|
||||
self.search_combobox.setEnabled(False)
|
||||
self.search_progress_bar.setMinimum(0)
|
||||
self.search_progress_bar.setMaximum(0)
|
||||
self.search_progress_bar.setValue(0)
|
||||
self.set_progress_visible(True)
|
||||
self.search_results_widget.clear()
|
||||
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
|
||||
'Found {count:d} song(s)').format(count=self.song_count))
|
||||
self.application.process_events()
|
||||
self.song_count = 0
|
||||
search_history = self.search_combobox.getItems()
|
||||
self.settings.setValue('songs/songselect searches', '|'.join(search_history))
|
||||
# Create thread and run search
|
||||
worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
|
||||
worker.show_info.connect(self.on_search_show_info)
|
||||
worker.found_song.connect(self.on_search_found_song)
|
||||
worker.finished.connect(self.on_search_finished)
|
||||
run_thread(worker, 'songselect')
|
||||
|
||||
def on_stop_button_clicked(self):
|
||||
"""
|
||||
Stop the search when the stop button is clicked.
|
||||
"""
|
||||
self.song_select_importer.stop()
|
||||
|
||||
def on_search_show_info(self, title, message):
|
||||
"""
|
||||
Show an informational message from the search thread
|
||||
:param title:
|
||||
:param message:
|
||||
"""
|
||||
QtWidgets.QMessageBox.information(self, title, message)
|
||||
|
||||
def on_search_found_song(self, song):
|
||||
"""
|
||||
Add a song to the list when one is found.
|
||||
:param song:
|
||||
"""
|
||||
self.song_count += 1
|
||||
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
|
||||
'Found {count:d} song(s)').format(count=self.song_count))
|
||||
item_title = song['title']
|
||||
if song['authors']:
|
||||
item_title += ' (' + ', '.join(song['authors']) + ')'
|
||||
song_item = QtWidgets.QListWidgetItem(item_title, self.search_results_widget)
|
||||
song_item.setData(QtCore.Qt.UserRole, song)
|
||||
|
||||
def on_search_finished(self):
|
||||
"""
|
||||
Slot which is called when the search is completed.
|
||||
"""
|
||||
self.application.process_events()
|
||||
self.set_progress_visible(False)
|
||||
self.search_button.setEnabled(True)
|
||||
self.search_combobox.setEnabled(True)
|
||||
self.application.process_events()
|
||||
|
||||
def on_search_results_widget_selection_changed(self):
|
||||
"""
|
||||
Enable or disable the view button when the selection changes.
|
||||
"""
|
||||
self.view_button.setEnabled(len(self.search_results_widget.selectedItems()) > 0)
|
||||
url = self.url_bar.text()
|
||||
self.song_select_importer.set_page(url)
|
||||
|
||||
def on_view_button_clicked(self):
|
||||
"""
|
||||
View a song from SongSelect.
|
||||
Import a song from SongSelect.
|
||||
"""
|
||||
self._view_song(self.search_results_widget.currentItem())
|
||||
self.view_button.setEnabled(False)
|
||||
self.url_bar.setEnabled(False)
|
||||
self._view_song()
|
||||
|
||||
def on_search_results_widget_double_clicked(self, current_item):
|
||||
def on_back_button_clicked(self, force_return_to_home=False):
|
||||
"""
|
||||
View a song from SongSelect
|
||||
|
||||
:param current_item:
|
||||
Go back to the search page or just to the webview if on the preview screen
|
||||
"""
|
||||
self._view_song(current_item)
|
||||
|
||||
def on_back_button_clicked(self):
|
||||
"""
|
||||
Go back to the search page.
|
||||
"""
|
||||
self.stacked_widget.setCurrentIndex(1)
|
||||
self.search_combobox.setFocus()
|
||||
if (self.stacked_widget.currentIndex() == 0 or force_return_to_home):
|
||||
self.song_select_importer.set_home_page()
|
||||
else:
|
||||
self.view_button.setEnabled(True)
|
||||
self.url_bar.setEnabled(True)
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
|
||||
def on_import_button_clicked(self):
|
||||
"""
|
||||
Import a song from SongSelect.
|
||||
"""
|
||||
# Warn user if a song exists in the database with the same ccli_number
|
||||
songs_with_same_ccli_number = self.plugin.manager.get_all_objects(
|
||||
Song, and_(Song.ccli_number.like(self.song['ccli_number']), Song.ccli_number != ''))
|
||||
if len(songs_with_same_ccli_number) > 0:
|
||||
continue_import = QtWidgets.QMessageBox.question(self,
|
||||
translate('SongsPlugin.SongSelectForm',
|
||||
'Song Duplicate Warning'),
|
||||
translate('SongsPlugin.SongSelectForm',
|
||||
'A song with the same CCLI number is already in '
|
||||
'your database.\n\n'
|
||||
'Are you sure you want to import this song?'),
|
||||
defaultButton=QtWidgets.QMessageBox.No)
|
||||
if continue_import == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
self.song_select_importer.save_song(self.song)
|
||||
self.song = None
|
||||
if QtWidgets.QMessageBox.question(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
|
||||
QtWidgets.QMessageBox.information(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
|
||||
translate('SongsPlugin.SongSelectForm',
|
||||
'Your song has been imported, would you '
|
||||
'like to import more songs?'),
|
||||
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
|
||||
self.on_back_button_clicked()
|
||||
else:
|
||||
self.application.process_events()
|
||||
self.done(QtWidgets.QDialog.Accepted)
|
||||
|
||||
def set_progress_visible(self, is_visible):
|
||||
"""
|
||||
Show or hide the search progress, including the stop button.
|
||||
"""
|
||||
self.search_progress_bar.setVisible(is_visible)
|
||||
self.stop_button.setVisible(is_visible)
|
||||
'Your song has been imported'))
|
||||
self.on_back_button_clicked(True)
|
||||
|
50
openlp/plugins/songs/forms/webengine.py
Normal file
50
openlp/plugins/songs/forms/webengine.py
Normal file
@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2020 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
"""
|
||||
Subclass of QWebEngineView used to block js messages; Useful for public sites
|
||||
where we have no control on the code run in the webpage, and do not want to
|
||||
see the error messages.
|
||||
"""
|
||||
from PyQt5 import QtWebEngineWidgets
|
||||
|
||||
|
||||
class WebEnginePage(QtWebEngineWidgets.QWebEnginePage):
|
||||
"""
|
||||
A custom WebEngine page to capture Javascript console logging
|
||||
"""
|
||||
def javaScriptConsoleMessage(self, level, message, line_number, source_id):
|
||||
"""
|
||||
Override the parent method in order to hide the ccli site messages
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class WebEngineView(QtWebEngineWidgets.QWebEngineView):
|
||||
"""
|
||||
A sub-classed QWebEngineView
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Constructor
|
||||
"""
|
||||
super(WebEngineView, self).__init__(parent)
|
||||
self.setPage(WebEnginePage(self))
|
@ -22,29 +22,18 @@
|
||||
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
|
||||
"""
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from html import unescape
|
||||
from html.parser import HTMLParser
|
||||
from http.cookiejar import CookieJar
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import HTTPCookieProcessor, URLError, build_opener
|
||||
|
||||
from urllib.request import URLError
|
||||
from PyQt5 import QtCore
|
||||
from bs4 import BeautifulSoup, NavigableString
|
||||
|
||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||
from openlp.plugins.songs.lib.db import Song, Author, Topic
|
||||
from openlp.plugins.songs.lib.openlyricsxml import SongXML
|
||||
from openlp.core.common.utils import wait_for
|
||||
|
||||
|
||||
USER_AGENTS = [
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/52.0.2743.116 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0'
|
||||
]
|
||||
BASE_URL = 'https://songselect.ccli.com'
|
||||
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
|
||||
'https%3a%2f%2fsongselect.ccli.com%2f'
|
||||
@ -56,177 +45,140 @@ SONG_PAGE = BASE_URL + '/Songs/'
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pages(object):
|
||||
"""
|
||||
Songselect web page types.
|
||||
"""
|
||||
Login = 0
|
||||
Home = 1
|
||||
Search = 2
|
||||
Song = 3
|
||||
Other = 4
|
||||
|
||||
|
||||
class SongSelectImport(object):
|
||||
"""
|
||||
The :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class contains all the code which interfaces
|
||||
with CCLI's SongSelect service and downloads the songs.
|
||||
"""
|
||||
def __init__(self, db_manager):
|
||||
def __init__(self, db_manager, webview):
|
||||
"""
|
||||
Set up the song select importer
|
||||
|
||||
:param db_manager: The song database manager
|
||||
"""
|
||||
self.db_manager = db_manager
|
||||
self.html_parser = HTMLParser()
|
||||
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
|
||||
self.opener.addheaders = [('User-Agent', random.choice(USER_AGENTS))]
|
||||
self.run_search = True
|
||||
self.webview = webview
|
||||
|
||||
def login(self, username, password, callback=None):
|
||||
def get_page_type(self):
|
||||
"""
|
||||
Log the user into SongSelect. This method takes a username and password, and runs ``callback()`` at various
|
||||
points which can be used to give the user some form of feedback.
|
||||
Get the type of page the user is currently ono
|
||||
|
||||
:param username: SongSelect username
|
||||
:param password: SongSelect password
|
||||
:param callback: Method to notify of progress.
|
||||
:return: subscription level on success, None on failure.
|
||||
:return: The page the user is on
|
||||
"""
|
||||
if callback:
|
||||
callback()
|
||||
try:
|
||||
login_page = BeautifulSoup(self.opener.open(LOGIN_PAGE).read(), 'lxml')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not login to SongSelect, {error}'.format(error=error))
|
||||
return False
|
||||
if callback:
|
||||
callback()
|
||||
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
|
||||
data = urlencode({
|
||||
'__RequestVerificationToken': token_input['value'],
|
||||
'emailAddress': username,
|
||||
'password': password,
|
||||
'RememberMe': 'false'
|
||||
})
|
||||
login_form = login_page.find('form')
|
||||
if login_form:
|
||||
login_url = login_form.attrs['action']
|
||||
else:
|
||||
login_url = '/Account/SignIn'
|
||||
if not login_url.startswith('http'):
|
||||
if login_url[0] != '/':
|
||||
login_url = '/' + login_url
|
||||
login_url = LOGIN_URL + login_url
|
||||
try:
|
||||
posted_page = BeautifulSoup(self.opener.open(login_url, data.encode('utf-8')).read(), 'lxml')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not login to SongSelect, {error}'.format(error=error))
|
||||
return False
|
||||
if callback:
|
||||
callback()
|
||||
# Page if user is in an organization
|
||||
if posted_page.find('input', id='SearchText') is not None:
|
||||
self.subscription_level = self.find_subscription_level(posted_page)
|
||||
return self.subscription_level
|
||||
# Page if user is not in an organization
|
||||
elif posted_page.find('div', id="select-organization") is not None:
|
||||
try:
|
||||
home_page = BeautifulSoup(self.opener.open(BASE_URL).read(), 'lxml')
|
||||
self.subscription_level = self.find_subscription_level(home_page)
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not reach SongSelect, {error}'.format(error=error))
|
||||
self.subscription_level = None
|
||||
return self.subscription_level
|
||||
else:
|
||||
log.debug(posted_page)
|
||||
current_url_host = self.webview.page().url().host()
|
||||
current_url_path = self.webview.page().url().path()
|
||||
if (current_url_host == QtCore.QUrl(LOGIN_URL).host() and current_url_path == QtCore.QUrl(LOGIN_PAGE).path()):
|
||||
return Pages.Login
|
||||
elif (current_url_host == QtCore.QUrl(BASE_URL).host()):
|
||||
if (current_url_path == '/' or current_url_path == ''):
|
||||
return Pages.Home
|
||||
elif (current_url_path == QtCore.QUrl(SEARCH_URL).path()):
|
||||
return Pages.Search
|
||||
elif (self.get_song_number_from_url(current_url_path) is not None):
|
||||
return Pages.Song
|
||||
return Pages.Other
|
||||
|
||||
def _run_javascript(self, script):
|
||||
"""
|
||||
Run a script and returns the result
|
||||
|
||||
:param script: The javascript to be run
|
||||
:return: The evaluated result
|
||||
"""
|
||||
self.web_stuff = ""
|
||||
self.got_web_stuff = False
|
||||
|
||||
def handle_result(result):
|
||||
"""
|
||||
Handle the result from the asynchronous call
|
||||
"""
|
||||
self.got_web_stuff = True
|
||||
self.web_stuff = result
|
||||
self.webview.page().runJavaScript(script, handle_result)
|
||||
wait_for(lambda: self.got_web_stuff)
|
||||
return self.web_stuff
|
||||
|
||||
def reset_webview(self):
|
||||
"""
|
||||
Sets the webview back to the login page using the Qt setUrl method
|
||||
"""
|
||||
url = QtCore.QUrl(LOGIN_PAGE)
|
||||
self.webview.setUrl(url)
|
||||
|
||||
def set_home_page(self):
|
||||
"""
|
||||
Sets the webview to the search page
|
||||
"""
|
||||
self.set_page(BASE_URL)
|
||||
|
||||
def set_page(self, url):
|
||||
"""
|
||||
Sets the active page in the webview
|
||||
|
||||
:param url: The new page location
|
||||
"""
|
||||
script = 'document.location = "{}"'.format(url)
|
||||
self._run_javascript(script)
|
||||
|
||||
def set_login_fields(self, username, password):
|
||||
script_set_login_fields = ('document.getElementById("EmailAddress").value = "{email}";'
|
||||
'document.getElementById("Password").value = "{password}";'
|
||||
).format(email=username, password=password)
|
||||
self._run_javascript(script_set_login_fields)
|
||||
|
||||
def get_page(self, url):
|
||||
"""
|
||||
Gets the html for the url through the active webview
|
||||
|
||||
:return: String containing a html document
|
||||
"""
|
||||
script_get_page = ('var openlp_page_data = null;'
|
||||
'fetch("{}").then(data => {{return data.text()}})'
|
||||
' .then(data => {{openlp_page_data = data}})').format(url)
|
||||
self._run_javascript(script_get_page)
|
||||
wait_for(lambda: self._run_javascript('openlp_page_data != null'))
|
||||
return self._run_javascript('openlp_page_data')
|
||||
|
||||
def get_song_number_from_url(self, url):
|
||||
"""
|
||||
Gets the ccli song number for a song from the url
|
||||
|
||||
:return: String containg ccli song number, None is returned if not found
|
||||
"""
|
||||
ccli_number_regex = re.compile(r'.*?Songs\/([0-9]+).*', re.IGNORECASE)
|
||||
regex_matches = ccli_number_regex.match(url)
|
||||
if regex_matches:
|
||||
return regex_matches.group(1)
|
||||
return None
|
||||
|
||||
def find_subscription_level(self, page):
|
||||
scripts = page.find_all('script')
|
||||
for tag in scripts:
|
||||
if tag.string:
|
||||
match = re.search("'Subscription': '(?P<subscription_level>[^']+)", tag.string)
|
||||
if match:
|
||||
return match.group('subscription_level')
|
||||
log.error('Could not determine SongSelect subscription level')
|
||||
return None
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
Log the user out of SongSelect
|
||||
"""
|
||||
try:
|
||||
self.opener.open(LOGOUT_URL)
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not log out of SongSelect, {error}'.format(error=error))
|
||||
|
||||
def search(self, search_text, max_results, callback=None):
|
||||
"""
|
||||
Set up a search.
|
||||
|
||||
:param search_text: The text to search for.
|
||||
:param max_results: Maximum number of results to fetch.
|
||||
:param callback: A method which is called when each song is found, with the song as a parameter.
|
||||
:return: List of songs
|
||||
"""
|
||||
self.run_search = True
|
||||
search_text = search_text.strip()
|
||||
params = {
|
||||
'SongContent': '',
|
||||
'PrimaryLanguage': '',
|
||||
'Keys': '',
|
||||
'Themes': '',
|
||||
'List': 'publicdomain' if self.subscription_level == 'Free' else '',
|
||||
'Sort': '',
|
||||
'SearchText': search_text
|
||||
}
|
||||
current_page = 1
|
||||
songs = []
|
||||
while self.run_search:
|
||||
if current_page > 1:
|
||||
params['CurrentPage'] = current_page
|
||||
try:
|
||||
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
|
||||
search_results = results_page.find_all('div', 'song-result')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not search SongSelect, {error}'.format(error=error))
|
||||
results_page = None
|
||||
search_results = None
|
||||
if not search_results:
|
||||
if results_page and re.compile('^[0-9]+$').match(search_text):
|
||||
author_elements = results_page.find('ul', class_='authors').find_all('li')
|
||||
song = {
|
||||
'link': SONG_PAGE + search_text,
|
||||
'authors': [unescape(li.find('a').string).strip() for li in author_elements],
|
||||
'title': unescape(results_page.find('div', 'content-title').find('h1').string).strip()
|
||||
}
|
||||
if callback:
|
||||
callback(song)
|
||||
songs.append(song)
|
||||
break
|
||||
for result in search_results:
|
||||
authors = result.find('p', 'song-result-subtitle').string
|
||||
if authors:
|
||||
authors = unescape(authors).strip().split(', ')
|
||||
else:
|
||||
authors = ""
|
||||
song = {
|
||||
'title': unescape(result.find('p', 'song-result-title').find('a').string).strip(),
|
||||
'authors': authors,
|
||||
'link': BASE_URL + result.find('p', 'song-result-title').find('a')['href']
|
||||
}
|
||||
if callback:
|
||||
callback(song)
|
||||
songs.append(song)
|
||||
if len(songs) >= max_results:
|
||||
self.run_search = False
|
||||
break
|
||||
current_page += 1
|
||||
return songs
|
||||
|
||||
def get_song(self, song, callback=None):
|
||||
def get_song(self, callback=None):
|
||||
"""
|
||||
Get the full song from SongSelect
|
||||
|
||||
:param song: The song dictionary to update
|
||||
:param song: The song page url
|
||||
:param callback: A callback which can be used to indicate progress
|
||||
:return: The updated song dictionary
|
||||
:return: Dictionary containing the song info
|
||||
"""
|
||||
song = {}
|
||||
# Get current song
|
||||
current_url = self.webview.url().path()
|
||||
ccli_number = self.get_song_number_from_url(current_url)
|
||||
|
||||
if callback:
|
||||
callback()
|
||||
try:
|
||||
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
|
||||
song_page = BeautifulSoup(self.get_page(SONG_PAGE + ccli_number), 'lxml')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not get song from SongSelect, {error}'.format(error=error))
|
||||
return None
|
||||
@ -234,7 +186,7 @@ class SongSelectImport(object):
|
||||
if callback:
|
||||
callback()
|
||||
try:
|
||||
lyrics_page = BeautifulSoup(self.opener.open(BASE_URL + lyrics_link).read(), 'lxml')
|
||||
lyrics_page = BeautifulSoup(self.get_page(BASE_URL + lyrics_link), 'lxml')
|
||||
except (TypeError, URLError):
|
||||
log.exception('Could not get lyrics from SongSelect')
|
||||
return None
|
||||
@ -249,6 +201,9 @@ class SongSelectImport(object):
|
||||
copyright_elements.extend(ul.find_all('li')[1:])
|
||||
if ul.find('li', string=themes_regex):
|
||||
theme_elements.extend(ul.find_all('li')[1:])
|
||||
author_elements = song_page.find('div', 'content-title').find('ul', 'authors').find_all('li')
|
||||
song['title'] = unescape(song_page.find('div', 'content-title').find('h1').string.strip())
|
||||
song['authors'] = [unescape(li.find('a').string).strip() for li in author_elements]
|
||||
song['copyright'] = '/'.join([unescape(li.string).strip() for li in copyright_elements])
|
||||
song['topics'] = [unescape(li.string).strip() for li in theme_elements]
|
||||
song['ccli_number'] = song_page.find('div', 'song-content-data').find('ul').find('li')\
|
||||
@ -273,7 +228,7 @@ class SongSelectImport(object):
|
||||
"""
|
||||
Save a song to the database, using the db_manager
|
||||
|
||||
:param song:
|
||||
:param song: Dictionary of the song to save
|
||||
:return:
|
||||
"""
|
||||
db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
|
||||
@ -312,9 +267,3 @@ class SongSelectImport(object):
|
||||
db_song.topics.append(topic)
|
||||
self.db_manager.save_object(db_song)
|
||||
return db_song
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the search.
|
||||
"""
|
||||
self.run_search = False
|
||||
|
@ -79,6 +79,25 @@ class SongsTab(SettingsTab):
|
||||
self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
|
||||
self.chords_layout.addWidget(self.neolatin_notation_radio_button)
|
||||
self.left_layout.addWidget(self.chords_group_box)
|
||||
# CCLI SongSelect login group box
|
||||
|
||||
self.ccli_login_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.ccli_login_group_box.setObjectName('ccli_login_group_box')
|
||||
self.ccli_login_layout = QtWidgets.QFormLayout(self.ccli_login_group_box)
|
||||
self.ccli_login_layout.setObjectName('ccli_login_layout')
|
||||
self.ccli_username_label = QtWidgets.QLabel(self.ccli_login_group_box)
|
||||
self.ccli_username_label.setObjectName('ccli_username_label')
|
||||
self.ccli_username = QtWidgets.QLineEdit(self.ccli_login_group_box)
|
||||
self.ccli_username.setObjectName('ccli_username')
|
||||
self.ccli_login_layout.addRow(self.ccli_username_label, self.ccli_username)
|
||||
self.ccli_password_label = QtWidgets.QLabel(self.ccli_login_group_box)
|
||||
self.ccli_password_label.setObjectName('ccli_password_label')
|
||||
self.ccli_password = QtWidgets.QLineEdit(self.ccli_login_group_box)
|
||||
self.ccli_password.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.ccli_password.setObjectName('ccli_password')
|
||||
self.ccli_login_layout.addRow(self.ccli_password_label, self.ccli_password)
|
||||
self.left_layout.addWidget(self.ccli_login_group_box)
|
||||
|
||||
# Footer group box
|
||||
self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.footer_group_box.setObjectName('footer_group_box')
|
||||
@ -121,6 +140,9 @@ class SongsTab(SettingsTab):
|
||||
self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
|
||||
self.disable_chords_import_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||
'Ignore chords when importing songs'))
|
||||
self.ccli_login_group_box.setTitle(translate('SongsPlugin.SongsTab', 'SongSelect Login'))
|
||||
self.ccli_username_label.setText(translate('SongsPlugin.SongsTab', 'Username:'))
|
||||
self.ccli_password_label.setText(translate('SongsPlugin.SongsTab', 'Password:'))
|
||||
self.chord_notation_label.setText(translate('SongsPlugin.SongsTab', 'Chord notation to use:'))
|
||||
self.english_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'English') + ' (C-D-E-F-G-A-B)')
|
||||
self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
|
||||
@ -222,6 +244,8 @@ class SongsTab(SettingsTab):
|
||||
self.neolatin_notation_radio_button.setChecked(True)
|
||||
else:
|
||||
self.english_notation_radio_button.setChecked(True)
|
||||
self.ccli_username.setText(self.settings.value('songs/songselect username'))
|
||||
self.ccli_password.setText(self.settings.value('songs/songselect password'))
|
||||
self.footer_edit_box.setPlainText(self.settings.value('songs/footer template'))
|
||||
|
||||
def save(self):
|
||||
@ -231,6 +255,19 @@ class SongsTab(SettingsTab):
|
||||
self.settings.setValue('songs/enable chords', self.chords_group_box.isChecked())
|
||||
self.settings.setValue('songs/disable chords import', self.disable_chords_import)
|
||||
self.settings.setValue('songs/chord notation', self.chord_notation)
|
||||
self.settings.setValue('songs/songselect username', self.ccli_username.text())
|
||||
# Only save password if it's blank or the user acknowleges the warning
|
||||
if (self.ccli_password.text() == ''):
|
||||
self.settings.setValue('songs/songselect password', '')
|
||||
elif (self.ccli_password.text() != self.settings.value('songs/songselect password')):
|
||||
answer = QtWidgets.QMessageBox.question(
|
||||
self, translate('SongsPlugin.SongsTab', 'Save Username and Password'),
|
||||
translate('SongsPlugin.SongsTab', 'WARNING: Saving your SongSelect password is INSECURE, '
|
||||
'your password is stored in PLAIN TEXT. Click Yes to save '
|
||||
'your password or No to cancel this.'),
|
||||
defaultButton=QtWidgets.QMessageBox.No)
|
||||
if answer == QtWidgets.QMessageBox.Yes:
|
||||
self.settings.setValue('songs/songselect password', self.ccli_password.text())
|
||||
# Only save footer template if it has been changed. This allows future updates
|
||||
if self.footer_edit_box.toPlainText() != self.settings.get_default_value('songs/footer template'):
|
||||
self.settings.setValue('songs/footer template', self.footer_edit_box.toPlainText())
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -24,7 +24,7 @@ This module contains tests for the lib submodule of the Images plugin.
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.plugins.songs.lib.songstab import SongsTab
|
||||
@ -135,6 +135,38 @@ def test_neolatin_notation_button(form):
|
||||
assert form.chord_notation == 'neo-latin'
|
||||
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songstab.QtWidgets.QMessageBox.question')
|
||||
@patch('openlp.core.common.settings.Settings.setValue')
|
||||
def test_password_change(mocked_settings_set_val, mocked_question, form):
|
||||
"""
|
||||
Test the ccli password sends a warning when changed, and saves when accepted
|
||||
"""
|
||||
# GIVEN: Warning is accepted and new password set
|
||||
form.ccli_password.setText('new_password')
|
||||
mocked_question.return_value = QtWidgets.QMessageBox.Yes
|
||||
# WHEN: save is invoked
|
||||
form.save()
|
||||
# THEN: footer should not have been saved (one less call than the change test below)
|
||||
mocked_question.assert_called_once()
|
||||
assert mocked_settings_set_val.call_count == 9
|
||||
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songstab.QtWidgets.QMessageBox.question')
|
||||
@patch('openlp.core.common.settings.Settings.setValue')
|
||||
def test_password_change_cancelled(mocked_settings_set_val, mocked_question, form):
|
||||
"""
|
||||
Test the ccli password sends a warning when changed and does not save when cancelled
|
||||
"""
|
||||
# GIVEN: Warning is not accepted and new password set
|
||||
form.ccli_password.setText('new_password')
|
||||
mocked_question.return_value = QtWidgets.QMessageBox.No
|
||||
# WHEN: save is invoked
|
||||
form.save()
|
||||
# THEN: footer should not have been saved (one less call than the change test below)
|
||||
mocked_question.assert_called_once()
|
||||
assert mocked_settings_set_val.call_count == 8
|
||||
|
||||
|
||||
@patch('openlp.core.common.settings.Settings.setValue')
|
||||
def test_footer_nochange(mocked_settings_set_val, form):
|
||||
"""
|
||||
@ -144,7 +176,7 @@ def test_footer_nochange(mocked_settings_set_val, form):
|
||||
# WHEN: save is invoked
|
||||
form.save()
|
||||
# THEN: footer should not have been saved (one less call than the change test below)
|
||||
assert mocked_settings_set_val.call_count == 7
|
||||
assert mocked_settings_set_val.call_count == 9
|
||||
|
||||
|
||||
@patch('openlp.core.common.settings.Settings.setValue')
|
||||
@ -157,7 +189,7 @@ def test_footer_change(mocked_settings_set_val, form):
|
||||
# WHEN: save is invoked
|
||||
form.save()
|
||||
# THEN: footer should have been saved (one more call to setValue than the nochange test)
|
||||
assert mocked_settings_set_val.call_count == 8
|
||||
assert mocked_settings_set_val.call_count == 10
|
||||
assert form.footer_edit_box.toPlainText() == 'A new footer'
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user