openlp/openlp/plugins/songs/forms/songselectform.py

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