# -*- 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 . # ########################################################################## """ 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