mirror of https://gitlab.com/openlp/openlp.git
284 lines
14 KiB
Python
284 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2024 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/>. #
|
|
##########################################################################
|
|
"""
|
|
The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI for the SongSelect importer
|
|
"""
|
|
import logging
|
|
import re
|
|
|
|
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets
|
|
from sqlalchemy.sql import and_
|
|
from tempfile import TemporaryDirectory
|
|
|
|
from openlp.core.common.i18n import translate
|
|
from openlp.core.common.mixins import RegistryProperties
|
|
from openlp.core.common.platform import is_linux, is_macosx
|
|
from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
|
|
from openlp.plugins.songs.lib.db import Song
|
|
from openlp.plugins.songs.lib.songselect import SongSelectImport, Pages
|
|
from openlp.plugins.songs.lib.importers.chordpro import ChordProImport
|
|
from openlp.plugins.songs.lib.importers.cclifile import CCLIFileImport
|
|
|
|
|
|
CHROME_USER_AGENT = 'Mozilla/5.0 ({os_info}) AppleWebKit/537.36 ' \
|
|
'(KHTML, like Gecko) Chrome/{version} Safari/537.36'
|
|
WIN_OS_USER_AGENT = 'Windows NT 10.0; Win64; x64'
|
|
MAC_OS_USER_AGENT = 'Macintosh; Intel Mac OS X 13_5_2'
|
|
LINUX_OS_USER_AGENT = 'X11; Linux x86_64'
|
|
|
|
REPLACE_ALL_JS = """
|
|
// Based on: https://vanillajstoolkit.com/polyfills/stringreplaceall/
|
|
if (!String.prototype.replaceAll) {
|
|
String.prototype.replaceAll = function(str, newStr){
|
|
// If a regex pattern
|
|
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
|
return this.replace(str, newStr);
|
|
}
|
|
// If a string
|
|
return this.split(str).join(newStr);
|
|
};
|
|
}
|
|
"""
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties):
|
|
"""
|
|
The :class:`SongSelectForm` class is the SongSelect dialog.
|
|
"""
|
|
|
|
def __init__(self, parent=None, plugin=None, db_manager=None):
|
|
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
|
|
QtCore.Qt.WindowCloseButtonHint)
|
|
self.plugin = plugin
|
|
self.db_manager = db_manager
|
|
self.setup_ui(self)
|
|
self.current_download_item = None
|
|
self.tmp_folder = TemporaryDirectory(prefix='openlp_songselect_', ignore_cleanup_errors=True)
|
|
|
|
def initialise(self):
|
|
"""
|
|
Initialise the SongSelectForm
|
|
"""
|
|
self.song = None
|
|
self.song_select_importer = SongSelectImport(self.db_manager, self.webview)
|
|
self.url_bar.returnPressed.connect(self.on_url_bar_return_pressed)
|
|
self.back_button.clicked.connect(self.on_back_button_clicked)
|
|
self.close_button.clicked.connect(self.done)
|
|
# We need to set the user agent to an up to date browser version do to the pyqtwebengine package
|
|
# from pypi has not been updated in ages...
|
|
# TODO: get the latest version number from somewhere
|
|
chrome_version = '117.0.5938.48'
|
|
if is_linux():
|
|
user_agent = CHROME_USER_AGENT.format(os_info=LINUX_OS_USER_AGENT, version=chrome_version)
|
|
elif is_macosx():
|
|
user_agent = CHROME_USER_AGENT.format(os_info=MAC_OS_USER_AGENT, version=chrome_version)
|
|
else:
|
|
user_agent = CHROME_USER_AGENT.format(os_info=WIN_OS_USER_AGENT, version=chrome_version)
|
|
self.webview.page().profile().setHttpUserAgent(user_agent)
|
|
self.webview.page().loadStarted.connect(self.page_load_started)
|
|
self.webview.page().loadFinished.connect(self.page_loaded)
|
|
self.webview.page().profile().downloadRequested.connect(self.on_download_requested)
|
|
self.webview.urlChanged.connect(self.update_url)
|
|
self.inject_js_str_replaceall()
|
|
|
|
def inject_js_str_replaceall(self):
|
|
"""
|
|
Inject an implementation of string replaceAll which are missing in pre 5.15.3 QWebEngine
|
|
"""
|
|
script = QtWebEngineWidgets.QWebEngineScript()
|
|
script.setInjectionPoint(QtWebEngineWidgets.QWebEngineScript.InjectionPoint.DocumentCreation)
|
|
script.setSourceCode(REPLACE_ALL_JS)
|
|
script.setWorldId(QtWebEngineWidgets.QWebEngineScript.ScriptWorldId.MainWorld)
|
|
script.setRunsOnSubFrames(True)
|
|
script.setName('string_replaceall')
|
|
self.webview.page().scripts().insert(script)
|
|
|
|
def update_url(self, new_url):
|
|
self.url_bar.setText(new_url.toString())
|
|
|
|
def download_finished(self):
|
|
"""
|
|
Callback for when download has finished
|
|
"""
|
|
if self.current_download_item:
|
|
if self.current_download_item.state() == QtWebEngineWidgets.QWebEngineDownloadItem.DownloadCompleted:
|
|
self.song_progress_bar.setValue(2)
|
|
song_filename = self.current_download_item.downloadDirectory() + '/' \
|
|
+ self.current_download_item.downloadFileName()
|
|
song_file = open(song_filename, 'rt', encoding='utf-8')
|
|
song_content = song_file.read()
|
|
song_file.seek(0)
|
|
if self.check_for_duplicate(song_content):
|
|
# if a chordpro title tag is in the file, assume it is chordpro format
|
|
if '{title:' in song_content:
|
|
# assume it is a ChordPro file
|
|
chordpro_importer = ChordProImport(self.plugin.manager, file_path=song_filename)
|
|
chordpro_importer.do_import_file(song_file)
|
|
else:
|
|
# assume it is a simple lyrics
|
|
cccli_lyrics_importer = CCLIFileImport(self.plugin.manager, file_path=song_filename)
|
|
lines = song_file.readlines()
|
|
cccli_lyrics_importer.do_import_txt_file(lines)
|
|
self.song_progress_bar.setValue(3)
|
|
QtWidgets.QMessageBox.information(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
|
|
translate('SongsPlugin.SongSelectForm',
|
|
'Your song has been imported'))
|
|
song_file.close()
|
|
self.song_progress_bar.setVisible(False)
|
|
self.url_bar.setVisible(True)
|
|
self.webview.setEnabled(True)
|
|
|
|
@QtCore.pyqtSlot(QtWebEngineWidgets.QWebEngineDownloadItem)
|
|
def on_download_requested(self, download_item):
|
|
"""
|
|
Called when download is started
|
|
"""
|
|
# only import from txt is supported
|
|
if download_item.suggestedFileName().endswith('.txt'):
|
|
self.song_progress_bar.setMaximum(3)
|
|
self.song_progress_bar.setValue(1)
|
|
self.song_progress_bar.setVisible(True)
|
|
self.webview.setEnabled(False)
|
|
download_item.setDownloadDirectory(self.tmp_folder.name)
|
|
download_item.accept()
|
|
self.current_download_item = download_item
|
|
self.current_download_item.finished.connect(self.download_finished)
|
|
else:
|
|
download_item.cancel()
|
|
QtWidgets.QMessageBox.information(self, translate('SongsPlugin.SongSelectForm', 'Unsupported format'),
|
|
translate('SongsPlugin.SongSelectForm',
|
|
'OpenLP can only import simple lyrics or ChordPro'))
|
|
|
|
def exec(self):
|
|
"""
|
|
Execute the dialog. This method sets everything back to its initial
|
|
values.
|
|
"""
|
|
self.song_select_importer.reset_webview()
|
|
self.back_button.setEnabled(False)
|
|
self.stacked_widget.setCurrentIndex(0)
|
|
return QtWidgets.QDialog.exec(self)
|
|
|
|
def done(self, result_code):
|
|
"""
|
|
Log out of SongSelect.
|
|
|
|
:param result_code: The result of the dialog.
|
|
"""
|
|
return QtWidgets.QDialog.done(self, result_code)
|
|
|
|
def page_load_started(self):
|
|
self.back_button.setEnabled(False)
|
|
self.url_bar.setCursorPosition(0)
|
|
self.url_bar.setVisible(True)
|
|
self.message_area.setText(translate('SongsPlugin.SongSelectForm',
|
|
'Import songs by clicking the "Download" in the Lyrics tab '
|
|
'or "Download ChordPro" in the Chords tabs.'))
|
|
|
|
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()
|
|
else:
|
|
self.back_button.setEnabled(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 _view_song(self):
|
|
"""
|
|
Load a song into the song view.
|
|
"""
|
|
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)
|
|
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(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 self.song['verses']])
|
|
self.lyrics_table_widget.resizeRowsToContents()
|
|
self.lyrics_table_widget.scrollToTop()
|
|
self.stacked_widget.setCurrentIndex(1)
|
|
|
|
def on_url_bar_return_pressed(self):
|
|
"""
|
|
Go to the url in the url bar
|
|
"""
|
|
url = self.url_bar.text()
|
|
self.song_select_importer.set_page(url)
|
|
|
|
def on_back_button_clicked(self, force_return_to_home=False):
|
|
"""
|
|
Go back to the search page or just to the webview if on the preview screen
|
|
"""
|
|
if (self.stacked_widget.currentIndex() == 0 or force_return_to_home):
|
|
self.song_select_importer.set_home_page()
|
|
else:
|
|
self.url_bar.setEnabled(True)
|
|
self.stacked_widget.setCurrentIndex(0)
|
|
|
|
def check_for_duplicate(self, song_content):
|
|
"""
|
|
Warn user if a song exists in the database with the same ccli_number
|
|
"""
|
|
# First extract the CCLI number of the song
|
|
match = re.search(r'\nCCLI.+?#\s*(\d+)', song_content)
|
|
if match:
|
|
self.ccli_number = match.group(1)
|
|
else:
|
|
return True
|
|
songs_with_same_ccli_number = self.plugin.manager.get_all_objects(
|
|
Song, and_(Song.ccli_number.like(self.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 False
|
|
return True
|