Change SongSelect import procedure to import when clicking download on webpage

This commit is contained in:
Tomas Groth 2023-08-18 18:03:32 +00:00 committed by Raoul Snyman
parent eaa97f433d
commit ad8dcb0f8a
17 changed files with 427 additions and 531 deletions

View File

@ -22,7 +22,6 @@
from PyQt5 import QtWidgets
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry
from openlp.core.lib.ui import create_button_box
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.edits import SpellTextEdit
@ -68,22 +67,21 @@ class Ui_EditVerseDialog(object):
self.verse_type_layout.addWidget(self.insert_button)
self.verse_type_layout.addStretch()
self.dialog_layout.addLayout(self.verse_type_layout)
if Registry().get('settings').value('songs/enable chords'):
self.transpose_widget = QtWidgets.QWidget()
self.transpose_layout = QtWidgets.QHBoxLayout(self.transpose_widget)
self.transpose_layout.setObjectName('transpose_layout')
self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
self.transpose_label.setObjectName('transpose_label')
self.transpose_layout.addWidget(self.transpose_label)
self.transpose_up_button = QtWidgets.QPushButton(edit_verse_dialog)
self.transpose_up_button.setIcon(UiIcons().arrow_up)
self.transpose_up_button.setObjectName('transpose_up')
self.transpose_layout.addWidget(self.transpose_up_button)
self.transpose_down_button = QtWidgets.QPushButton(edit_verse_dialog)
self.transpose_down_button.setIcon(UiIcons().arrow_down)
self.transpose_down_button.setObjectName('transpose_down')
self.transpose_layout.addWidget(self.transpose_down_button)
self.dialog_layout.addWidget(self.transpose_widget)
self.transpose_widget = QtWidgets.QWidget()
self.transpose_layout = QtWidgets.QHBoxLayout(self.transpose_widget)
self.transpose_layout.setObjectName('transpose_layout')
self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
self.transpose_label.setObjectName('transpose_label')
self.transpose_layout.addWidget(self.transpose_label)
self.transpose_up_button = QtWidgets.QPushButton(edit_verse_dialog)
self.transpose_up_button.setIcon(UiIcons().arrow_up)
self.transpose_up_button.setObjectName('transpose_up')
self.transpose_layout.addWidget(self.transpose_up_button)
self.transpose_down_button = QtWidgets.QPushButton(edit_verse_dialog)
self.transpose_down_button.setIcon(UiIcons().arrow_down)
self.transpose_down_button.setObjectName('transpose_down')
self.transpose_layout.addWidget(self.transpose_down_button)
self.dialog_layout.addWidget(self.transpose_widget)
self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
self.dialog_layout.addWidget(self.button_box)
self.retranslate_ui(edit_verse_dialog)
@ -106,7 +104,6 @@ class Ui_EditVerseDialog(object):
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
'Split a slide into two by inserting a verse splitter.'))
if Registry().get('settings').value('songs/enable chords'):
self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))
self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))

View File

@ -54,9 +54,12 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
self.forced_split_button.clicked.connect(self.on_forced_split_button_clicked)
if Registry().get('settings').value('songs/enable chords'):
self.transpose_down_button.clicked.connect(self.on_transpose_down_button_clicked)
self.transpose_up_button.clicked.connect(self.on_transpose_up_button_clicked)
self.transpose_down_button.clicked.connect(self.on_transpose_down_button_clicked)
self.transpose_up_button.clicked.connect(self.on_transpose_up_button_clicked)
def exec(self):
self.transpose_widget.setVisible(Registry().get('settings').value('songs/enable chords'))
super(EditVerseForm, self).exec()
def insert_verse(self, verse_tag, verse_num=1):
"""

View File

@ -131,14 +131,6 @@ class Ui_SongSelectDialog(object):
self.message_area.setWordWrap(True)
self.message_area.setObjectName('message_area')
self.bottom_button_layout.addWidget(self.message_area, 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)
@ -151,7 +143,6 @@ class Ui_SongSelectDialog(object):
Translate the GUI.
"""
songselect_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'CCLI SongSelect Importer'))
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:'))
@ -159,5 +150,4 @@ class Ui_SongSelectDialog(object):
self.lyrics_label.setText(translate('SongsPlugin.SongSelectForm', 'Lyrics:'))
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'))

View File

@ -22,15 +22,20 @@
The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI for the SongSelect importer
"""
import logging
import re
from PyQt5 import QtCore, QtWidgets
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.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
log = logging.getLogger(__name__)
@ -46,6 +51,8 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
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):
"""
@ -54,12 +61,68 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
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.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)
self.webview.page().profile().downloadRequested.connect(self.on_download_requested)
self.webview.urlChanged.connect(self.update_url)
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')
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):
"""
@ -67,9 +130,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
values.
"""
self.song_select_importer.reset_webview()
self.view_button.setEnabled(False)
self.back_button.setEnabled(False)
self.import_button.setEnabled(False)
self.stacked_widget.setCurrentIndex(0)
return QtWidgets.QDialog.exec(self)
@ -82,51 +143,26 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
return QtWidgets.QDialog.done(self, result_code)
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)
self.message_area.setText('')
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()
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)
if self.song:
self.import_button.setEnabled(True)
self.view_button.setEnabled(True)
else:
message = translate('SongsPlugin.SongSelectForm', 'This song cannot be read. Perhaps your CCLI account '
'does not give you access to this song.')
self.message_area.setText(message)
else:
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.
"""
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
self.application.process_events()
def _view_song(self):
"""
Load a song into the song view.
@ -166,14 +202,6 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
url = self.url_bar.text()
self.song_select_importer.set_page(url)
def on_view_button_clicked(self):
"""
Import a song from SongSelect.
"""
self.view_button.setEnabled(False)
self.url_bar.setEnabled(False)
self._view_song()
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
@ -181,17 +209,21 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
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):
def check_for_duplicate(self, song_content):
"""
Import a song from SongSelect.
Warn user if a song exists in the database with the same ccli_number
"""
# 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.song['ccli_number']), Song.ccli_number != ''))
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',
@ -202,10 +234,5 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
'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
QtWidgets.QMessageBox.information(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
translate('SongsPlugin.SongSelectForm',
'Your song has been imported'))
self.on_back_button_clicked(True)
return False
return True

View File

@ -21,6 +21,7 @@
import chardet
import codecs
import logging
import re
from openlp.core.common.i18n import translate
from openlp.plugins.songs.lib import VerseType
@ -241,7 +242,7 @@ class CCLIFileImport(SongImport):
<Empty line>
<Empty line>
Song CCLI number
# e.g. CCLI Number (e.g.CCLI-Liednummer: 2672885)
# e.g. CCLI Number (e.g.CCLI-Liednummer: 2672885).
Song copyright (if it begins ©, otherwise after authors)
# e.g. © 1999 Integrity's Hosanna! Music | LenSongs Publishing
Song authors # e.g. Lenny LeBlanc | Paul Baloche
@ -251,6 +252,18 @@ class CCLIFileImport(SongImport):
CCLI Licence number of user
# e.g. CCLI-Liedlizenznummer: 14 / CCLI License No. 14
In the 2023 format the footer is as below:
Song authors # e.g. Lenny LeBlanc | Paul Baloche
Song CCLI number
# e.g. CCLI Number (e.g.CCLI-Liednummer: 2672885).
Song copyright (if it begins ©, otherwise after authors)
# e.g. © 1999 Integrity's Hosanna! Music | LenSongs Publishing
Licencing info
# e.g. For use solely with the SongSelect Terms of Use.
CCLI Licence number of user
# e.g. CCLI-Liedlizenznummer: 14 / CCLI License No. 14
"""
log.debug('TXT file text: {text}'.format(text=text_list))
line_number = 0
@ -259,8 +272,15 @@ class CCLIFileImport(SongImport):
verse_type = VerseType.tags[VerseType.Verse]
song_author = ''
verse_start = False
for line in text_list:
for idx in range(len(text_list)):
line = text_list[idx]
clean_line = line.strip()
if idx + 1 < len(text_list):
next_line = text_list[idx + 1]
next_clean_line = next_line.strip()
else:
next_line = None
next_clean_line = None
if not clean_line:
if line_number == 0:
continue
@ -280,18 +300,26 @@ class CCLIFileImport(SongImport):
# line_number=1, ccli number, first line after verses
if clean_line.startswith('CCLI'):
line_number += 1
ccli_parts = clean_line.split(' ')
self.ccli_number = ccli_parts[len(ccli_parts) - 1]
ccli_parts = re.findall(r'\d+', clean_line)
if ccli_parts:
self.ccli_number = ccli_parts[0]
# if CCLI is on the next line, this is 2023 format and the current line is authors
elif next_clean_line and next_clean_line.startswith('CCLI'):
line_number += 1
song_author = clean_line
elif not verse_start:
# We have the verse descriptor
verse_desc_parts = clean_line.split(' ')
if len(verse_desc_parts):
if verse_desc_parts[0].startswith('Ver'):
first_verse_desc = verse_desc_parts[0].upper()
if first_verse_desc.startswith(('VER', VerseType.translated_tags[VerseType.Verse])):
verse_type = VerseType.tags[VerseType.Verse]
elif verse_desc_parts[0].startswith('Ch'):
elif first_verse_desc.startswith(('CH', VerseType.translated_tags[VerseType.Chorus])):
verse_type = VerseType.tags[VerseType.Chorus]
elif verse_desc_parts[0].startswith('Br'):
elif first_verse_desc.startswith(('BR', VerseType.translated_tags[VerseType.Bridge])):
verse_type = VerseType.tags[VerseType.Bridge]
elif first_verse_desc.startswith(('END', VerseType.translated_tags[VerseType.Ending])):
verse_type = VerseType.tags[VerseType.Ending]
else:
# we need to analyse the next line for
# verse type, so set flag
@ -318,14 +346,18 @@ class CCLIFileImport(SongImport):
# last part. Add l so as to keep the CRLF
verse_text = verse_text + line
else:
# line_number=2, copyright
# line_number=2, copyright or CCLIE
if line_number == 2:
line_number += 1
if clean_line.startswith('©'):
self.add_copyright(clean_line)
elif clean_line.startswith('CCLI'):
ccli_parts = re.findall(r'\d+', clean_line)
if ccli_parts:
self.ccli_number = ccli_parts[0]
else:
song_author = clean_line
# n=3, authors
# n=3, authors or copyright
elif line_number == 3:
line_number += 1
if song_author:
@ -335,11 +367,6 @@ class CCLIFileImport(SongImport):
# line_number=4, comments lines before last line
elif line_number == 4 and not clean_line.startswith('CCL'):
self.comments += clean_line
# split on known separators
author_list = song_author.split('/')
if len(author_list) < 2:
author_list = song_author.split('|')
# Clean spaces before and after author names.
for author_name in author_list:
self.add_author(author_name.strip())
# add author(s)
self.parse_author(song_author)
return self.finish()

View File

@ -63,12 +63,13 @@ class ChordProImport(SongImport):
try:
file_content = song_file.read()
except UnicodeDecodeError:
self.log_error(song_file.name, translate('SongsPlugin.CCLIFileImport',
self.log_error(song_file.name, translate('SongsPlugin.ChordProFileImport',
'The file contains unreadable characters.'))
return
current_verse = ''
current_verse_type = 'v'
skip_block = False
chorus_added = []
for line in file_content.splitlines():
line = line.rstrip()
# Detect tags
@ -81,8 +82,10 @@ class ChordProImport(SongImport):
self.alternate_title = tag_value
elif tag_name == 'composer':
self.parse_author(tag_value, AuthorType.Music)
elif tag_name in ['lyricist', 'artist', 'author']: # author is not an official directive
elif tag_name in ['lyricist', 'author']: # author is not an official directive
self.parse_author(tag_value, AuthorType.Words)
elif tag_name == 'artist':
self.parse_author(tag_value)
elif tag_name == 'meta':
meta_tag_name, meta_tag_value = tag_value.split(' ', 1)
# Skip, if no value
@ -95,47 +98,52 @@ class ChordProImport(SongImport):
self.alternate_title = meta_tag_value
elif meta_tag_name == 'composer':
self.parse_author(meta_tag_value, AuthorType.Music)
elif meta_tag_name in ['lyricist', 'artist', 'author']:
elif meta_tag_name in ['lyricist', 'author']:
self.parse_author(meta_tag_value, AuthorType.Words)
elif meta_tag_name == 'artist':
self.parse_author(meta_tag_value)
elif meta_tag_name in ['topic', 'topics']:
for topic in meta_tag_value.split(','):
self.topics.append(topic.strip())
elif 'ccli' in meta_tag_name:
self.ccli_number = meta_tag_value
elif tag_name == 'ccli':
self.ccli_number = tag_value
elif tag_name == 'copyright':
self.add_copyright(tag_value)
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
# Detect if the comment is used as a chorus repeat marker
if tag_value.lower().startswith('chorus'):
# Detect if the comment is used as a verse type marker
lower_tag_value = tag_value.lower()
if lower_tag_value.startswith(('chorus', 'verse', 'bridge', 'ending')):
if current_verse.strip():
# Add collected verse to the lyrics
# Strip out chords if set up to
if not self.settings.value('songs/enable chords') or \
self.settings.value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
self.add_chordpro_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
self.repeat_verse('c1')
# Detect if the comment is used as a chorus repeat marker
if lower_tag_value.startswith('chorus'):
if lower_tag_value in chorus_added:
self.repeat_verse('c%d' % (chorus_added.index(lower_tag_value) + 1))
else:
chorus_added.append(lower_tag_value)
current_verse_type = 'c'
elif lower_tag_value.startswith('verse'):
current_verse_type = 'v'
elif lower_tag_value.startswith('bridge'):
current_verse_type = 'b'
elif lower_tag_value.startswith('ending'):
current_verse_type = 'e'
else:
self.add_comment(tag_value)
elif tag_name in ['start_of_chorus', 'soc']:
current_verse_type = 'c'
elif tag_name in ['end_of_chorus', 'eoc']:
# Add collected chorus to the lyrics
# Strip out chords if set up to
if not self.settings.value('songs/enable chords') or \
self.settings.value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
self.add_chordpro_verse(current_verse.rstrip(), current_verse_type)
chorus_added.append('chorus')
current_verse_type = 'v'
current_verse = ''
elif tag_name in ['start_of_tab', 'sot']:
if current_verse.strip():
# Add collected verse to the lyrics
# Strip out chords if set up to
if not self.settings.value('songs/enable chords') or \
self.settings.value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
self.add_chordpro_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
skip_block = True
@ -145,11 +153,7 @@ class ChordProImport(SongImport):
# A new song starts below this tag
if self.verses and self.title:
if current_verse.strip():
# Strip out chords if set up to
if not self.settings.value('songs/enable chords') or \
self.settings.value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
self.add_chordpro_verse(current_verse.rstrip(), current_verse_type)
if not self.finish():
self.log_error(song_file.name)
self.set_defaults()
@ -164,16 +168,14 @@ class ChordProImport(SongImport):
elif line == "['|]":
# Found a vertical bar
continue
elif line.startswith('CCLI') or line.startswith('©'):
# Found the CCLI footer or a copyright footer, stop here.
break
else:
if skip_block:
continue
elif line == '' and current_verse.strip() and current_verse_type != 'c':
# Add collected verse to the lyrics
# Strip out chords if set up to
if not self.settings.value('songs/enable chords') or \
self.settings.value('songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
self.add_chordpro_verse(current_verse.rstrip(), current_verse_type)
current_verse_type = 'v'
current_verse = ''
else:
@ -182,11 +184,7 @@ class ChordProImport(SongImport):
else:
current_verse += line + '\n'
if current_verse.strip():
# Strip out chords if set up to
if not self.settings.value('songs/enable chords') or self.settings.value(
'songs/disable chords import'):
current_verse = re.sub(r'\[.*?\]', '', current_verse)
self.add_verse(current_verse.rstrip(), current_verse_type)
self.add_chordpro_verse(current_verse.rstrip(), current_verse_type)
# if no title was in directives, get it from the first line
if not self.title and self.verses:
(verse_def, verse_text, lang) = self.verses[0]
@ -212,3 +210,18 @@ class ChordProImport(SongImport):
tag_name = line[:colon_idx]
tag_value = line[colon_idx + 1:-1].strip()
return tag_name, tag_value
def add_chordpro_verse(self, verse_text, verse_type):
"""
Strip out chords if set up to.
In some cases (noteably CCLI) spaces and dashes are inserted for readability,
but they do not look good on display so remove them
:param verse_text:
:param verse_type:
"""
if not self.settings.value('songs/enable chords') or self.settings.value('songs/disable chords import'):
verse_text = re.sub(r'( +- +)?\[.*?\]( +- +)?', '', verse_text)
else:
verse_text = re.sub(r' +- +\[', '[', verse_text)
verse_text = re.sub(r'\] +- +', ']', verse_text)
self.add_verse(verse_text.rstrip(), verse_type)

View File

@ -32,7 +32,7 @@ from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry
from openlp.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, SongBook, MediaFile, Song, Topic
from openlp.plugins.songs.lib.db import Author, AuthorType, SongBook, MediaFile, Song, Topic
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.plugins.songs.lib.ui import SongStrings
@ -226,13 +226,65 @@ class SongImport(QtCore.QObject):
self.copyright += ' '
self.copyright += copyright
def _check_for_author_prefix(self, author, default_type):
"""
Detect if the author type is prefixed the author name. Looks for the prefix in entire string, in case multiple
authors are listed, each prefixed with a type.
:param author: String with author(s) to search for predefined prefixes
:param default_type: The default or expected author type to use
:return: True if a prefix was found, meaning the author will be added from this method. Else False is returned,
and the caller must add the author.
"""
author_low = author.lower()
music_prefixes = ['music by', translate('SongsPlugin.SongImport', 'music by').lower(),
'music:', translate('SongsPlugin.SongImport', 'music:').lower(),
'arranged by', translate('SongsPlugin.SongImport', 'arranged by').lower(),
'arranged:', translate('SongsPlugin.SongImport', 'arranged:').lower(),
'composed by', translate('SongsPlugin.SongImport', 'composed by').lower(),
'composer:', translate('SongsPlugin.SongImport', 'composer:').lower()]
words_prefixes = ['words by', translate('SongsPlugin.SongImport', 'words by').lower(),
'words:', translate('SongsPlugin.SongImport', 'words:').lower(),
'lyrics by', translate('SongsPlugin.SongImport', 'lyrics by').lower(),
'lyrics:', translate('SongsPlugin.SongImport', 'lyrics:').lower(),
'written by', translate('SongsPlugin.SongImport', 'written by').lower(),
'writer:', translate('SongsPlugin.SongImport', 'writer:').lower(),
'authored by', translate('SongsPlugin.SongImport', 'authored by').lower(),
'author:', translate('SongsPlugin.SongImport', 'author:').lower()]
translation_prefixes = ['tranlated by', translate('SongsPlugin.SongImport', 'tranlated by').lower(),
'translation:', translate('SongsPlugin.SongImport', 'translation:').lower()]
prefix_map = [(music_prefixes, AuthorType.Music), (words_prefixes, AuthorType.Words),
(translation_prefixes, AuthorType.Translation)]
for prefixes, prefix_type in prefix_map:
for prefix in prefixes:
idx = author_low.find(prefix)
if idx == -1:
pass # no hit
elif idx == 0:
# found the prefix right at the start
author_without_prefix = author[len(prefix):].strip()
self.parse_author(author_without_prefix, prefix_type)
return True
else:
# found the prefix "inside" the string
author_before_prefix = author[0:idx].strip()
author_after_prefix = author[idx + len(prefix):].strip()
# the type of the authors before the "prefix" must be of the type provided in the method parameter
self.parse_author(author_before_prefix, default_type)
self.parse_author(author_after_prefix, prefix_type)
return True
return False
def parse_author(self, text, type=None):
"""
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
Add the author. OpenLP stores them individually so split by 'and', '&' comma and semicolon.
TODO: check for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
"""
# check if the given text is prefixed with the author type, in which case the adding of the author will
# be done in _check_for_author_prefix
if self._check_for_author_prefix(text, type):
return
for author in text.split(','):
authors = author.split('&')
authors = re.split(r';|&| and |/|\|', author)
for i in range(len(authors)):
author2 = authors[i].strip()
if author2.find(' ') == -1 and i < len(authors) - 1:

View File

@ -23,10 +23,7 @@ The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect i
"""
import logging
import re
from html import unescape
from urllib.error 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
@ -162,70 +159,6 @@ class SongSelectImport(object):
return regex_matches.group(1)
return None
def get_song(self, callback=None):
"""
Get the full song from SongSelect
:param song: The song page url
:param callback: A callback which can be used to indicate progress
: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.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
try:
lyrics_link = song_page.find('section', 'page-section').find('a')['href']
except KeyError:
# can't find a link to the song - most likely the user account has no access to it
return None
if callback:
callback()
try:
lyrics_page = BeautifulSoup(self.get_page(BASE_URL + lyrics_link), 'lxml')
except (TypeError, URLError):
log.exception('Could not get lyrics from SongSelect')
return None
if callback:
callback()
theme_elements = []
# Themes regex only works if the ccli site is in english.
themes_regex = re.compile(r'\bThemes\b')
for ul in song_page.find_all('ul', 'song-meta-list'):
if ul.find('li', string=themes_regex):
theme_elements.extend(ul.find_all('li')[1:])
copyright_elements = lyrics_page.find('ul', 'copyright').find_all('li')
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')\
.find('strong').string.strip()
song['verses'] = []
verses = lyrics_page.find('div', 'song-viewer lyrics').find_all('p')
verse_labels = lyrics_page.find('div', 'song-viewer lyrics').find_all('h3')
for verse, label in zip(verses, verse_labels):
song_verse = {'label': unescape(label.string).strip(), 'lyrics': ''}
for v in verse.contents:
if isinstance(v, NavigableString):
song_verse['lyrics'] += unescape(v.string).strip()
else:
song_verse['lyrics'] += '\n'
song_verse['lyrics'] = song_verse['lyrics'].strip(' \n\r\t')
song['verses'].append(song_verse)
for counter, author in enumerate(song['authors']):
song['authors'][counter] = unescape(author)
return song
def save_song(self, song):
"""
Save a song to the database, using the db_manager

View File

@ -22,7 +22,6 @@
All the tests
"""
import os
import sys
import shutil
from tempfile import mkdtemp
from tempfile import mkstemp
@ -31,8 +30,7 @@ from unittest.mock import MagicMock
import pytest
from pytestqt.qt_compat import qt_api
from PyQt5 import QtCore, QtWidgets # noqa
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
from PyQt5 import QtCore # noqa
from openlp.core.app import OpenLP
from openlp.core.state import State

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 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/>. #
##########################################################################
"""
This module contains tests for the ccli song importer.
"""
from tests.helpers.songfileimport import SongImportTestHelper
from tests.utils.constants import RESOURCE_PATH
TEST_PATH = RESOURCE_PATH / 'songs' / 'songselect'
def test_ccli(mock_settings):
with SongImportTestHelper('CCLIFileImport', 'cclifile') as helper:
helper.file_import([TEST_PATH / 'TestSong.bin'],
helper.load_external_result_data(TEST_PATH / 'TestSong-bin.json'))
helper.file_import([TEST_PATH / 'TestSong.txt'],
helper.load_external_result_data(TEST_PATH / 'TestSong-txt.json'))
helper.file_import([TEST_PATH / 'TestSong2023.txt'],
helper.load_external_result_data(TEST_PATH / 'TestSong2023-txt.json'))

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 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/>. #
##########################################################################
"""
This module contains tests for the SongImport submodule of the Songs plugin.
"""
from unittest.mock import MagicMock
from openlp.plugins.songs.lib.db import AuthorType
from openlp.plugins.songs.lib.importers.songimport import SongImport
def test_parse_author_simple_with_type(settings):
# GIVEN: A SongImport object with a mocked DB manager and a dummy file path
db_manager_mock = MagicMock()
song_import = SongImport(db_manager_mock, file_path='/dummy/')
# WHEN: Parsing simple author name with type
song_import.parse_author('Random Author', AuthorType.Words)
# THEN: The result should be an registered author of that type
assert song_import.authors == [('Random Author', AuthorType.Words)]
def test_parse_author_list_with_type(settings):
# GIVEN: A SongImport object with a mocked DB manager and a dummy file path
db_manager_mock = MagicMock()
song_import = SongImport(db_manager_mock, file_path='/dummy/')
# WHEN: Parsing simple list of author names with type
song_import.parse_author('Random Author, Important Name; Very Important Person', AuthorType.Words)
# THEN: The result should be registered authors of that type
assert song_import.authors == [('Random Author', AuthorType.Words),
('Important Name', AuthorType.Words),
('Very Important Person', AuthorType.Words)]
def test_parse_author_with_prefix(settings):
# GIVEN: A SongImport object with a mocked DB manager and a dummy file path
db_manager_mock = MagicMock()
song_import = SongImport(db_manager_mock, file_path='/dummy/')
# WHEN: Parsing author name with type prefixes
song_import.parse_author('Lyrics: Random Author')
# THEN: The result should be an registered author of that type
assert song_import.authors == [('Random Author', AuthorType.Words)]
def test_parse_author_with_multiple_prefixes(settings):
# GIVEN: A SongImport object with a mocked DB manager and a dummy file path
db_manager_mock = MagicMock()
song_import = SongImport(db_manager_mock, file_path='/dummy/')
# WHEN: Parsing author names with multiple inline prefixes
song_import.parse_author('Music by Random Author, Important Name; Lyrics: Very Important Person. '
'Translation: Very Clever Person')
# THEN: The result should be registered authors of that type
assert song_import.authors == [('Random Author', AuthorType.Music),
('Important Name', AuthorType.Music),
('Very Important Person', AuthorType.Words),
('Very Clever Person', AuthorType.Translation)]

View File

@ -29,7 +29,7 @@ re-downloading the HTML pages and changing the code to use the new layout.
from unittest import TestCase
from unittest.mock import MagicMock, patch, sentinel
from PyQt5 import QtWidgets, QtCore
from PyQt5 import QtCore
from openlp.core.common.registry import Registry
from openlp.plugins.songs.forms.songselectform import SongSelectForm
@ -47,6 +47,7 @@ class TestSongSelectImport(TestCase, TestMixin):
"""
Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class
"""
def test_constructor(self):
"""
Test that constructing a basic SongSelectImport object works correctly
@ -261,146 +262,6 @@ class TestSongSelectImport(TestCase, TestMixin):
# THEN: The returned value should be None
assert result is None
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
def test_get_song_page_raises_exception(self, mocked_get_page, mock_get_num):
"""
Test that when BeautifulSoup gets a bad song page the get_song() method returns None
"""
# GIVEN: A mocked callback and an importer object
mocked_get_page.side_effect = None
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: The callback should have been called once and None should be returned
mocked_callback.assert_called_with()
assert result is None, 'The get_song() method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_get_page, mock_get_num):
"""
Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
"""
# GIVEN: A bunch of mocked out stuff and an importer object
song_page = MagicMock(return_value={'href': '/lyricpage'})
MockedBeautifulSoup.side_effect = [song_page, TypeError('Test Error')]
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: The callback should have been called twice and None should be returned
assert 2 == mocked_callback.call_count, 'The callback should have been called twice'
assert result is None, 'The get_song() method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.log.exception')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
def test_get_song_no_access(self, mocked_get_page, mock_get_num, mock_log_exception):
"""
Test that the get_song() handles the case when the user's CCLI account has no access to the song
"""
fake_song_page = '''<!DOCTYPE html><html><body>
<div class="content-title">
<h1>Song Title</h1>
<ul class="authors">
<li><a>Author 1</a></li>
<li><a>Author 2</a></li>
</ul>
</div>
<div class="song-content-data"><ul><li><strong>1234_cclinumber_5678</strong></li></ul></div>
<section class="page-section">
<a title="View song lyrics" data-open="ssUpgradeModal"></a>
</section>
<ul class="song-meta-list">
<li>Themes</li><li><a>theme1</a></li><li><a>theme2</a></li>
</ul>
</body></html>
'''
fake_lyrics_page = '''<!DOCTYPE html><html><body>
<div class="song-viewer lyrics">
<h3>Verse 1</h3>
<p>verse thing 1<br>line 2</p>
<h3>Verse 2</h3>
<p>verse thing 2</p>
</div>
<ul class="copyright">
<li>Copy thing</li><li>Copy thing 2</li>
</ul>
</body></html>
'''
mocked_get_page.side_effect = [fake_song_page, fake_lyrics_page]
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: None should be returned
assert result is None, 'The get_song() method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_song_number_from_url')
@patch('openlp.plugins.songs.lib.songselect.SongSelectImport.get_page')
def test_get_song(self, mocked_get_page, mock_get_num):
"""
Test that the get_song() method returns the correct song details
"""
fake_song_page = '''<!DOCTYPE html><html><body>
<div class="content-title">
<h1>Song Title</h1>
<ul class="authors">
<li><a>Author 1</a></li>
<li><a>Author 2</a></li>
</ul>
</div>
<div class="song-content-data"><ul><li><strong>1234_cclinumber_5678</strong></li></ul></div>
<section class="page-section">
<a title="View song lyrics" href="pretend link"></a>
</section>
<ul class="song-meta-list">
<li>Themes</li><li><a>theme1</a></li><li><a>theme2</a></li>
</ul>
</body></html>
'''
fake_lyrics_page = '''<!DOCTYPE html><html><body>
<div class="song-viewer lyrics">
<h3>Verse 1</h3>
<p>verse thing 1<br>line 2</p>
<h3>Verse 2</h3>
<p>verse thing 2</p>
<h3>Spoken Words</h3>
<p>completely custom verse type</p>
</div>
<ul class="copyright">
<li>Copy thing</li><li>Copy thing 2</li>
</ul>
</body></html>
'''
mocked_get_page.side_effect = [fake_song_page, fake_lyrics_page]
mocked_callback = MagicMock()
importer = SongSelectImport(None, MagicMock())
# WHEN: get_song is called
result = importer.get_song(callback=mocked_callback)
# THEN: The callback should have been called three times and the song should be returned
assert 3 == mocked_callback.call_count, 'The callback should have been called twice'
assert result is not None, 'The get_song() method should have returned a song dictionary'
assert result['title'] == 'Song Title'
assert result['authors'] == ['Author 1', 'Author 2']
assert result['copyright'] == 'Copy thing/Copy thing 2'
assert result['topics'] == ['theme1', 'theme2']
assert result['ccli_number'] == '1234_cclinumber_5678'
assert result['verses'] == [{'label': 'Verse 1', 'lyrics': 'verse thing 1\nline 2'},
{'label': 'Verse 2', 'lyrics': 'verse thing 2'},
{'label': 'Spoken Words', 'lyrics': 'completely custom verse type'}]
@patch('openlp.plugins.songs.lib.songselect.clean_song')
@patch('openlp.plugins.songs.lib.songselect.Topic')
@patch('openlp.plugins.songs.lib.songselect.Author')
@ -605,7 +466,7 @@ class TestSongSelectForm(TestCase, TestMixin):
# THEN: The import object should exist, song var should be None, and the page hooked up
assert ssform.song is None
assert isinstance(ssform.song_select_importer, SongSelectImport), 'SongSelectImport object should be created'
assert ssform.webview.page.call_count == 2, 'Page should be called twice, once for each load handler'
assert ssform.webview.page.call_count == 3, 'Page should be called 3 times, once for each load handler'
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QDialog.exec')
def test_exec(self, mocked_exec):
@ -647,9 +508,6 @@ class TestSongSelectForm(TestCase, TestMixin):
"""
# GIVEN: The SongSelectForm
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_progress_bar = MagicMock()
ssform.import_button = MagicMock()
ssform.view_button = MagicMock()
ssform.back_button = MagicMock()
ssform.url_bar = MagicMock()
ssform.message_area = MagicMock()
@ -657,13 +515,10 @@ class TestSongSelectForm(TestCase, TestMixin):
# WHEN: The method is run
ssform.page_load_started()
# THEN: The UI should be set up accordingly (working bar and disabled buttons)
ssform.song_progress_bar.setMaximum.assert_called_with(0)
ssform.song_progress_bar.setVisible.assert_called_with(True)
ssform.import_button.setEnabled.assert_called_with(False)
ssform.view_button.setEnabled.assert_called_with(False)
# THEN: The UI should be set up accordingly
ssform.back_button.setEnabled.assert_called_with(False)
ssform.message_area.setText.assert_called_with('')
ssform.message_area.setText.assert_called_with('Import songs by clicking the "Download" in the Lyrics tab '
'or "Download ChordPro" in the Chords tabs.')
def test_page_loaded_login(self):
"""
@ -682,46 +537,6 @@ class TestSongSelectForm(TestCase, TestMixin):
# THEN: The signin page method should be called
ssform.signin_page_loaded.assert_called_once()
def test_page_loaded_song(self):
"""
Test the page_loaded method for a "Song" page
"""
# GIVEN: The SongSelectForm and mocked song page
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.get_page_type.return_value = Pages.Song
ssform.song_progress_bar = MagicMock()
ssform.url_bar = MagicMock()
# WHEN: The method is run
ssform.page_loaded(True)
# THEN: Progress bar should have been set max 3 (for loading song)
ssform.song_progress_bar.setMaximum.assert_called_with(3)
ssform.song_progress_bar.setVisible.call_count == 2
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_page_loaded_song_no_access(self, mocked_translate):
"""
Test the page_loaded method for a "Song" page to which the CCLI account has no access
"""
# GIVEN: The SongSelectForm and mocked song page and translate function
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.song_select_importer = MagicMock()
ssform.song_select_importer.get_page_type.return_value = Pages.Song
ssform.song_select_importer.get_song.return_value = None
ssform.song_progress_bar = MagicMock()
ssform.url_bar = MagicMock()
ssform.message_area = MagicMock()
mocked_translate.return_value = 'some message'
# WHEN: The method is run
ssform.page_loaded(True)
# THEN: The no access message should be shown and the progress bar should be less than 3
ssform.message_area.setText.assert_called_with('some message')
ssform.song_progress_bar.setValue.call_count < 4
def test_page_loaded_other(self):
"""
Test the page_loaded method for an "Other" page
@ -820,94 +635,6 @@ class TestSongSelectForm(TestCase, TestMixin):
# THEN: Page should not have changed and a warning should show
ssform.song_select_importer.set_page.assert_called_with("test")
@patch('openlp.plugins.songs.forms.songselectform.and_')
@patch('openlp.plugins.songs.forms.songselectform.Song')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_on_import(self, mocked_trans, mocked_quest, mocked_info, mocked_song, mocked_and):
"""
Test that when a song is imported and the user clicks the "yes" button, the UI goes back to the previous page
"""
# GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
mocked_trans.side_effect = lambda *args: args[1]
mocked_quest.return_value = QtWidgets.QMessageBox.Yes
ssform = SongSelectForm(None, MagicMock(), MagicMock())
mocked_song_select_importer = MagicMock()
ssform.song_select_importer = mocked_song_select_importer
ssform.song = {'ccli_number': '1234'}
# WHEN: The import button is clicked, and the user clicks Yes
with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
ssform.on_import_button_clicked()
# THEN: The on_back_button_clicked() method should have been called
mocked_song_select_importer.save_song.assert_called_with({'ccli_number': '1234'})
mocked_quest.assert_not_called()
mocked_info.assert_called_once()
mocked_on_back_button_clicked.assert_called_with(True)
assert ssform.song is None
@patch('openlp.plugins.songs.forms.songselectform.len')
@patch('openlp.plugins.songs.forms.songselectform.and_')
@patch('openlp.plugins.songs.forms.songselectform.Song')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_on_import_duplicate_yes_clicked(self, mock_trans, mock_q, mocked_info, mock_song, mock_and, mock_len):
"""
Test that when a duplicate song is imported and the user clicks the "yes" button, the song is imported
"""
# GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
mock_len.return_value = 1
mock_trans.side_effect = lambda *args: args[1]
mock_q.return_value = QtWidgets.QMessageBox.Yes
ssform = SongSelectForm(None, MagicMock(), MagicMock())
mocked_song_select_importer = MagicMock()
ssform.song_select_importer = mocked_song_select_importer
ssform.song = {'ccli_number': '1234'}
# WHEN: The import button is clicked, and the user clicks Yes
with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
ssform.on_import_button_clicked()
# THEN: Should have been saved and the on_back_button_clicked() method should have been called
mocked_song_select_importer.save_song.assert_called_with({'ccli_number': '1234'})
mock_q.assert_called_once()
mocked_info.assert_called_once()
mocked_on_back_button_clicked.assert_called_once()
assert ssform.song is None
@patch('openlp.plugins.songs.forms.songselectform.len')
@patch('openlp.plugins.songs.forms.songselectform.and_')
@patch('openlp.plugins.songs.forms.songselectform.Song')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
@patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
@patch('openlp.plugins.songs.forms.songselectform.translate')
def test_on_import_duplicate_no_clicked(self, mock_trans, mock_q, mocked_info, mock_song, mock_and, mock_len):
"""
Test that when a duplicate song is imported and the user clicks the "no" button, the UI exits
"""
# GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
mock_len.return_value = 1
mock_trans.side_effect = lambda *args: args[1]
mock_q.return_value = QtWidgets.QMessageBox.No
ssform = SongSelectForm(None, MagicMock(), MagicMock())
mocked_song_select_importer = MagicMock()
ssform.song_select_importer = mocked_song_select_importer
ssform.song = {'ccli_number': '1234'}
# WHEN: The import button is clicked, and the user clicks No
with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
ssform.on_import_button_clicked()
# THEN: Should have not been saved
assert mocked_song_select_importer.save_song.call_count == 0
mock_q.assert_called_once()
mocked_info.assert_not_called()
mocked_on_back_button_clicked.assert_not_called()
assert ssform.song is not None
def test_on_back_button_clicked_preview(self):
"""
Test that when the back button is clicked on preview screen, the stacked widget is set back one page
@ -967,35 +694,6 @@ class TestSongSelectForm(TestCase, TestMixin):
mocked_stacked_widget.setCurrentIndex.assert_called_with(0)
ssimporter.assert_called_with()
def test_update_song_progress(self):
"""
Test the _update_song_progress() method
"""
# GIVEN: A SongSelect form
ssform = SongSelectForm(None, MagicMock(), MagicMock())
# WHEN: _update_song_progress() is called
with patch.object(ssform, 'song_progress_bar') as mocked_song_progress_bar:
mocked_song_progress_bar.value.return_value = 2
ssform._update_song_progress()
# THEN: The song progress bar should be updated
mocked_song_progress_bar.setValue.assert_called_with(3)
def test_on_view_button_clicked(self):
"""
Test that view song function is run when the view button is clicked
"""
# GIVEN: A SongSelect form
ssform = SongSelectForm(None, MagicMock(), MagicMock())
# WHEN: A song result is double-clicked
with patch.object(ssform, '_view_song') as mocked_view_song:
ssform.on_view_button_clicked()
# THEN: The song is fetched and shown to the user
mocked_view_song.assert_called_with()
def test_songselect_file_import():
"""
@ -1006,3 +704,5 @@ def test_songselect_file_import():
helper.load_external_result_data(TEST_PATH / 'TestSong-bin.json'))
helper.file_import([TEST_PATH / 'TestSong.txt'],
helper.load_external_result_data(TEST_PATH / 'TestSong-txt.json'))
helper.file_import([TEST_PATH / 'TestSong2023.txt'],
helper.load_external_result_data(TEST_PATH / 'TestSong2023-txt.json'))

View File

@ -1,7 +1,7 @@
{
"title": "Amazing Grace",
"authors": [
"Words: John Newton (1725-1807)"
["John Newton (1725-1807)", "words"]
],
"copyright" : "Public Domain",
"verse_order_list": ["V1", "V2", "V3", "V4", "V5", "V6"],

View File

@ -0,0 +1,21 @@
{
"authors": [
"Author One",
"Author Two"
],
"ccli_number": "123456",
"song_number": 0,
"title": "Test Song",
"copyright": "© 2023 OpenLP Music",
"verse_order_list": [],
"verses": [
[
"Line One Verse One\nLine Two Verse One\nLine Three Verse One\nLine Four Verse One\n",
"v"
],
[
"Line One Verse Two\nLine Two Verse Two\nLine Three Verse Two\nLine Four Verse Two\n",
"v"
]
]
}

View File

@ -0,0 +1,21 @@
Test Song
Verse 1
Line One Verse One
Line Two Verse One
Line Three Verse One
Line Four Verse One
Verse 2
Line One Verse Two
Line Two Verse Two
Line Three Verse Two
Line Four Verse Two
Author One, Author Two
CCLI Song #123456
© 2023 OpenLP Music
For use solely with the OpenLP Song Test Suite. All rights reserved. http://openlp.org
CCLI License #00000

View File

@ -1,6 +1,7 @@
{
"authors": [
"Words: Reginald Heber (1783-1826). Music: John B. Dykes (1823-1876)"
["Reginald Heber (1783-1826)", "words"],
["John B. Dykes (1823-1876)", "music"]
],
"title": "Holy Holy Holy Lord God Almighty_v2_1_2",
"verse_order_list": [],

View File

@ -1,6 +1,7 @@
{
"authors": [
"Carl Brockhaus / Johann Georg Bäßler 1806"
"Carl Brockhaus",
"Johann Georg Bäßler 1806"
],
"title": "Du, Herr, verläßt mich nicht",
"verse_order_list": [],