def song_to_line_dict(self, song): song_xml = SongXML() if song.chords: verse_chords_xml = song_xml.get_verses(song.chords) else: verse_chords_xml = song_xml.get_verses(song.lyrics) section_line_dict = {} for count, verse in enumerate(verse_chords_xml): # This silently migrates from localized verse type markup. # If we trusted the database, this would be unnecessary. verse_tag = verse[0]['type'] index = None if len(verse_tag) > 1: index = VerseType.from_translated_string(verse_tag) if index is None: index = VerseType.from_string(verse_tag, None) else: verse_tags_translated = True if index is None: index = VerseType.from_tag(verse_tag) verse[0]['type'] = VerseType.tags[index] if verse[0]['label'] == '': verse[0]['label'] = '1' section_header = '%s%s' % (verse[0]['type'], verse[0]['label']) line_list = [] for line in verse[1].split('\n'): line_list.append(line) section_line_dict[section_header] = line_list return section_line_dict
def on_verse_edit_all_button_clicked(self): verse_list = '' if self.verse_list_widget.rowCount() > 0: for row in range(self.verse_list_widget.rowCount()): item = self.verse_list_widget.item(row, 0) field = item.data(QtCore.Qt.UserRole) verse_tag = VerseType.translated_name(field[0]) verse_num = field[1:] verse_list += '---[%s:%s]---\n' % (verse_tag, verse_num) verse_list += item.text() verse_list += '\n' self.verse_form.set_verse(verse_list) else: self.verse_form.set_verse('') if not self.verse_form.exec_(): return verse_list = self.verse_form.get_all_verses() verse_list = str(verse_list.replace('\r\n', '\n')) self.verse_list_widget.clear() self.verse_list_widget.setRowCount(0) for row in self.find_verse_split.split(verse_list): for match in row.split('---['): for count, parts in enumerate(match.split(']---\n')): if count == 0: if len(parts) == 0: continue # handling carefully user inputted versetags separator = parts.find(':') if separator >= 0: verse_name = parts[0:separator].strip() verse_num = parts[separator+1:].strip() else: verse_name = parts verse_num = '1' verse_index = VerseType.from_loose_input(verse_name) verse_tag = VerseType.tags[verse_index] # Later we need to handle v1a as well. #regex = re.compile(r'(\d+\w.)') regex = re.compile(r'\D*(\d+)\D*') match = regex.match(verse_num) if match: verse_num = match.group(1) else: verse_num = '1' verse_def = '%s%s' % (verse_tag, verse_num) else: if parts.endswith('\n'): parts = parts.rstrip('\n') item = QtGui.QTableWidgetItem(parts) item.setData(QtCore.Qt.UserRole, verse_def) self.verse_list_widget.setRowCount(self.verse_list_widget.rowCount() + 1) self.verse_list_widget.setItem(self.verse_list_widget.rowCount() - 1, 0, item) self.tag_rows() self.verse_edit_button.setEnabled(False) self.verse_delete_button.setEnabled(False) # Check if all verse tags are used. self.on_verse_order_text_changed(self.verse_order_edit.text())
def save_song(self, song): """ Save a song to the database, using the db_manager :param song: :return: """ db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number']) song_xml = SongXML() verse_order = [] for verse in song['verses']: verse_type, verse_number = verse['label'].split(' ')[:2] verse_type = VerseType.from_loose_input(verse_type) verse_number = int(verse_number) song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics']) verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number)) db_song.verse_order = ' '.join(verse_order) db_song.lyrics = song_xml.extract_xml() clean_song(self.db_manager, db_song) self.db_manager.save_object(db_song) db_song.authors_songs = [] for author_name in song['authors']: author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name) if not author: author = Author.populate(first_name=author_name.rsplit(' ', 1)[0], last_name=author_name.rsplit(' ', 1)[1], display_name=author_name) db_song.add_author(author) self.db_manager.save_object(db_song) return db_song
def set_verse(self, text, single=False, tag='%s1' % VerseType.tags[VerseType.Verse]): """ Save the verse :param text: The text :param single: is this a single verse :param tag: The tag """ self.has_single_verse = single if single: verse_type_index = VerseType.from_tag(tag[0], None) verse_number = tag[1:] if verse_type_index is not None: self.verse_type_combo_box.setCurrentIndex(verse_type_index) self.verse_number_box.setValue(int(verse_number)) self.insert_button.setVisible(False) else: if not text: text = '---[%s:1]---\n' % VerseType.translated_names[VerseType.Verse] self.verse_type_combo_box.setCurrentIndex(0) self.verse_number_box.setValue(1) self.insert_button.setVisible(True) self.verse_text_edit.setPlainText(text) self.verse_text_edit.setFocus() self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
def _validate_verse_list(self, verse_order, verse_count): verses = [] invalid_verses = [] verse_names = [] order_names = str(verse_order).split() order = self._extract_verse_order(verse_order) for index in range(verse_count): verse = self.verse_list_widget.item(index, 0) verse = verse.data(QtCore.Qt.UserRole) if verse not in verse_names: verses.append(verse) verse_names.append('%s%s' % (VerseType.translated_tag(verse[0]), verse[1:])) for count, item in enumerate(order): if item not in verses: invalid_verses.append(order_names[count]) if invalid_verses: valid = create_separated_list(verse_names) if len(invalid_verses) > 1: msg = translate('SongsPlugin.EditSongForm', 'There are no verses corresponding to "%(invalid)s".' 'Valid entries are %(valid)s.\nPlease enter the verses seperated by spaces.') \ % {'invalid' : ', '.join(invalid_verses), 'valid' : valid} else: msg = translate('SongsPlugin.EditSongForm', 'There is no verse corresponding to "%(invalid)s".' 'Valid entries are %(valid)s.\nPlease enter the verses seperated by spaces.') \ % {'invalid' : invalid_verses[0], 'valid' : valid} critical_error_message_box(title=translate('SongsPlugin.EditSongForm', 'Invalid Verse Order'), message=msg) return len(invalid_verses) == 0
def test_from_loose_input_with_valid_input(self, mocked_translated_tags): """ Test that the from_loose_input() method returns valid output on valid input. """ # GIVEN: A mocked VerseType.translated_tags # WHEN: We run the from_loose_input() method with a valid verse type, we get the expected VerseType back result = VerseType.from_loose_input('v') # THEN: The result should be a Verse self.assertEqual(result, VerseType.Verse, 'The result should be a verse, but was "%s"' % result)
def test_from_loose_input_with_invalid_input(self, mocked_translated_tags): """ Test that the from_loose_input() method returns a sane default when passed an invalid tag and None as default. """ # GIVEN: A mocked VerseType.translated_tags # WHEN: We run the from_loose_input() method with an invalid verse type, we get the specified default back result = VerseType.from_loose_input('m', None) # THEN: The result should be None self.assertIsNone(result, 'The result should be None, but was "%s"' % result)
def translated_name_test(self): """ Test that the translated_name() method returns the correct name """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the translated_name() method with a "verse" result = VerseType.translated_name('v') # THEN: The result should be "Verse" self.assertEqual(result, 'Verse', 'The result should be "Verse"') # WHEN: We run the translated_name() method with a "chorus" result = VerseType.translated_name('c') # THEN: The result should be "Chorus" self.assertEqual(result, 'Chorus', 'The result should be "Chorus"')
def test_from_loose_input_with_valid_input(self, mocked_translated_tags): """ Test that the from_loose_input() method returns valid output on valid input. """ # GIVEN: A mocked VerseType.translated_tags # WHEN: We run the from_loose_input() method with a valid verse type, we get the expected VerseType back result = VerseType.from_loose_input('v') # THEN: The result should be a Verse assert result == VerseType.Verse, 'The result should be a verse, but was "%s"' % result
def test_from_loose_input_with_invalid_input(self, mocked_translated_tags): """ Test that the from_loose_input() method returns a sane default when passed an invalid tag and None as default. """ # GIVEN: A mocked VerseType.translated_tags # WHEN: We run the from_loose_input() method with an invalid verse type, we get the specified default back result = VerseType.from_loose_input('m', None) # THEN: The result should be None assert result is None, 'The result should be None, but was "%s"' % result
def test_translated_tag(self): """ Test that the translated_tag() method returns the correct tags """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the translated_tag() method with a "verse" result = VerseType.translated_tag('v') # THEN: The result should be "V" assert result == 'V', 'The result should be "V"' # WHEN: We run the translated_tag() method with a "chorus" result = VerseType.translated_tag('c') # THEN: The result should be "C" assert result == 'C', 'The result should be "C"'
def insert_verse(self, verse_tag, verse_num=1): """ Insert a verse :param verse_tag: The verse tag :param verse_num: The verse number """ if self.verse_text_edit.textCursor().columnNumber() != 0: self.verse_text_edit.insertPlainText('\n') verse_tag = VerseType.translated_name(verse_tag) self.verse_text_edit.insertPlainText('---[%s:%s]---\n' % (verse_tag, verse_num)) self.verse_text_edit.setFocus()
def _extract_verse_order(self, verse_order): order = [] order_names = str(verse_order).split() for item in order_names: if len(item) == 1: verse_index = VerseType.from_translated_tag(item, None) if verse_index is not None: order.append(VerseType.tags[verse_index] + '1') else: # it matches no verses anyway order.append('') else: verse_index = VerseType.from_translated_tag(item[0], None) if verse_index is None: # it matches no verses anyway order.append('') else: verse_tag = VerseType.tags[verse_index] verse_num = item[1:].lower() order.append(verse_tag + verse_num) return order
def save_song(self, song): """ Save a song to the database, using the db_manager :param song: :return: """ db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number']) song_xml = SongXML() verse_order = [] for verse in song['verses']: if ' ' in verse['label']: verse_type, verse_number = verse['label'].split(' ', 1) else: verse_type = verse['label'] verse_number = 1 verse_type = VerseType.from_loose_input(verse_type) verse_number = int(verse_number) song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics']) verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number)) db_song.verse_order = ' '.join(verse_order) db_song.lyrics = song_xml.extract_xml() clean_song(self.db_manager, db_song) self.db_manager.save_object(db_song) db_song.authors_songs = [] for author_name in song['authors']: author = self.db_manager.get_object_filtered( Author, Author.display_name == author_name) if not author: name_parts = author_name.rsplit(' ', 1) first_name = name_parts[0] if len(name_parts) == 1: last_name = '' else: last_name = name_parts[1] author = Author.populate(first_name=first_name, last_name=last_name, display_name=author_name) db_song.add_author(author) db_song.topics = [] for topic_name in song.get('topics', []): topic = self.db_manager.get_object_filtered( Topic, Topic.name == topic_name) if not topic: topic = Topic.populate(name=topic_name) db_song.topics.append(topic) self.db_manager.save_object(db_song) return db_song
def test_translated_invalid_tag_with_invalid_default(self): """ Test that the translated_tag() method returns a sane default tag when passed an invalid default """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the translated_tag() method with an invalid verse type and an invalid default result = VerseType.translated_tag('q', 29) # THEN: The result should be "O" assert result == 'O', 'The result should be "O", but was "%s"' % result
def test_translated_invalid_name(self): """ Test that the translated_name() method returns the default name when passed an invalid tag """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the translated_name() method with an invalid verse type result = VerseType.translated_name('z') # THEN: The result should be "Other" assert result == 'Other', 'The result should be "Other", but was "%s"' % result
def test_from_tag_with_invalid_default(self): """ Test that the from_tag() method returns a sane default when passed an invalid tag and an invalid default. """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the from_tag() method with an invalid verse type, we get the specified default back result = VerseType.from_tag('@', 'asdf') # THEN: The result should be VerseType.Other assert result == VerseType.Other, 'The result should be VerseType.Other, but was "%s"' % result
def test_translated_invalid_tag(self): """ Test that the translated_tag() method returns the default tag when passed an invalid tag """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the translated_tag() method with an invalid verse type result = VerseType.translated_tag('z') # THEN: The result should be "O" self.assertEqual(result, 'O', 'The result should be "O", but was "%s"' % result)
def from_tag_with_none_default_test(self): """ Test that the from_tag() method returns a sane default when passed an invalid tag and None as default. """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the from_tag() method with an invalid verse type, we get the specified default back result = VerseType.from_tag('m', None) # THEN: The result should be None self.assertIsNone(result, 'The result should be None, but was "%s"' % result)
def from_tag_with_specified_default_test(self): """ Test that the from_tag() method returns the specified default when passed an invalid tag. """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the from_tag() method with an invalid verse type, we get the specified default back result = VerseType.from_tag('x', VerseType.Chorus) # THEN: The result should be VerseType.Chorus self.assertEqual(result, VerseType.Chorus, 'The result should be VerseType.Chorus, but was "%s"' % result)
def from_tag_test(self): """ Test that the from_tag() method returns the correct VerseType. """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the from_tag() method with a valid verse type, we get the name back result = VerseType.from_tag('v') # THEN: The result should be VerseType.Verse self.assertEqual(result, VerseType.Verse, 'The result should be VerseType.Verse, but was "%s"' % result)
def translated_invalid_name_with_invalid_default_test(self): """ Test that the translated_name() method returns the specified default tag when passed an invalid tag """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the translated_name() method with an invalid verse type and specify an invalid default result = VerseType.translated_name('q', 29) # THEN: The result should be "Other" self.assertEqual(result, 'Other', 'The result should be "Other", but was "%s"' % result)
def from_tag_with_invalid_default_test(self): """ Test that the from_tag() method returns a sane default when passed an invalid tag and an invalid default. """ # GIVEN: A mocked out translate() function that just returns what it was given with patch('openlp.plugins.songs.lib.translate') as mocked_translate: mocked_translate.side_effect = lambda x, y: y # WHEN: We run the from_tag() method with an invalid verse type, we get the specified default back result = VerseType.from_tag('m', 29) # THEN: The result should be VerseType.Other self.assertEqual(result, VerseType.Other, 'The result should be VerseType.Other, but was "%s"' % result)
def tag_rows(self): """ Tag the Song List rows based on the verse list """ row_label = [] for row in range(self.verse_list_widget.rowCount()): item = self.verse_list_widget.item(row, 0) verse_def = item.data(QtCore.Qt.UserRole) verse_tag = VerseType.translated_tag(verse_def[0]) row_def = '%s%s' % (verse_tag, verse_def[1:]) row_label.append(row_def) self.verse_list_widget.setVerticalHeaderLabels(row_label) self.verse_list_widget.resizeRowsToContents() self.verse_list_widget.repaint()
def save_song(self, song): """ Save a song to the database, using the db_manager :param song: :return: """ db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number']) song_xml = SongXML() verse_order = [] for verse in song['verses']: if ' ' in verse['label']: verse_type, verse_number = verse['label'].split(' ', 1) else: verse_type = verse['label'] verse_number = 1 verse_type = VerseType.from_loose_input(verse_type) verse_number = int(verse_number) song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics']) verse_order.append('{tag}{number}'.format(tag=VerseType.tags[verse_type], number=verse_number)) db_song.verse_order = ' '.join(verse_order) db_song.lyrics = song_xml.extract_xml() clean_song(self.db_manager, db_song) self.db_manager.save_object(db_song) db_song.authors_songs = [] for author_name in song['authors']: author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name) if not author: name_parts = author_name.rsplit(' ', 1) first_name = name_parts[0] if len(name_parts) == 1: last_name = '' else: last_name = name_parts[1] author = Author.populate(first_name=first_name, last_name=last_name, display_name=author_name) db_song.add_author(author) for topic_name in song.get('topics', []): topic = self.db_manager.get_object_filtered(Topic, Topic.name == topic_name) if not topic: topic = Topic.populate(name=topic_name) db_song.topics.append(topic) self.db_manager.save_object(db_song) return db_song
def save_song(self, song): """ Save a song to the database, using the db_manager :param song: :return: """ db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number']) song_xml = SongXML() verse_order = [] for verse in song['verses']: verse_type, verse_number = verse['label'].split(' ')[:2] verse_type = VerseType.from_loose_input(verse_type) verse_number = int(verse_number) song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics']) verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number)) db_song.verse_order = ' '.join(verse_order) db_song.lyrics = song_xml.extract_xml() clean_song(self.db_manager, db_song) self.db_manager.save_object(db_song) db_song.authors_songs = [] for author_name in song['authors']: author = self.db_manager.get_object_filtered( Author, Author.display_name == author_name) if not author: author = Author.populate(first_name=author_name.rsplit(' ', 1)[0], last_name=author_name.rsplit(' ', 1)[1], display_name=author_name) db_song.add_author(author) self.db_manager.save_object(db_song) return db_song
def setupUi(self): self.song_vertical_layout = QtWidgets.QVBoxLayout(self) self.song_vertical_layout.setObjectName('song_vertical_layout') self.song_group_box = QtWidgets.QGroupBox(self) self.song_group_box.setObjectName('song_group_box') self.song_group_box.setFixedWidth(300) self.song_group_box_layout = QtWidgets.QVBoxLayout(self.song_group_box) self.song_group_box_layout.setObjectName('song_group_box_layout') self.song_info_form_layout = QtWidgets.QFormLayout() self.song_info_form_layout.setObjectName('song_info_form_layout') # Add title widget. self.song_title_label = QtWidgets.QLabel(self) self.song_title_label.setObjectName('song_title_label') self.song_info_form_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.song_title_label) self.song_title_content = QtWidgets.QLabel(self) self.song_title_content.setObjectName('song_title_content') self.song_title_content.setText(self.song.title) self.song_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.song_title_content) # Add alternate title widget. self.song_alternate_title_label = QtWidgets.QLabel(self) self.song_alternate_title_label.setObjectName( 'song_alternate_title_label') self.song_info_form_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.song_alternate_title_label) self.song_alternate_title_content = QtWidgets.QLabel(self) self.song_alternate_title_content.setObjectName( 'song_alternate_title_content') self.song_alternate_title_content.setText(self.song.alternate_title) self.song_alternate_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.song_alternate_title_content) # Add CCLI number widget. self.song_ccli_number_label = QtWidgets.QLabel(self) self.song_ccli_number_label.setObjectName('song_ccli_number_label') self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.song_ccli_number_label) self.song_ccli_number_content = QtWidgets.QLabel(self) self.song_ccli_number_content.setObjectName('song_ccli_number_content') self.song_ccli_number_content.setText(self.song.ccli_number) self.song_ccli_number_content.setWordWrap(True) self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.song_ccli_number_content) # Add copyright widget. self.song_copyright_label = QtWidgets.QLabel(self) self.song_copyright_label.setObjectName('song_copyright_label') self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.song_copyright_label) self.song_copyright_content = QtWidgets.QLabel(self) self.song_copyright_content.setObjectName('song_copyright_content') self.song_copyright_content.setWordWrap(True) self.song_copyright_content.setText(self.song.copyright) self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.song_copyright_content) # Add comments widget. self.song_comments_label = QtWidgets.QLabel(self) self.song_comments_label.setObjectName('song_comments_label') self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.song_comments_label) self.song_comments_content = QtWidgets.QLabel(self) self.song_comments_content.setObjectName('song_comments_content') self.song_comments_content.setText(self.song.comments) self.song_comments_content.setWordWrap(True) self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.song_comments_content) # Add authors widget. self.song_authors_label = QtWidgets.QLabel(self) self.song_authors_label.setObjectName('song_authors_label') self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.song_authors_label) self.song_authors_content = QtWidgets.QLabel(self) self.song_authors_content.setObjectName('song_authors_content') self.song_authors_content.setWordWrap(True) authors_text = ', '.join( [author.display_name for author in self.song.authors]) self.song_authors_content.setText(authors_text) self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.song_authors_content) # Add verse order widget. self.song_verse_order_label = QtWidgets.QLabel(self) self.song_verse_order_label.setObjectName('song_verse_order_label') self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.song_verse_order_label) self.song_verse_order_content = QtWidgets.QLabel(self) self.song_verse_order_content.setObjectName('song_verse_order_content') self.song_verse_order_content.setText(self.song.verse_order) self.song_verse_order_content.setWordWrap(True) self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.song_verse_order_content) self.song_group_box_layout.addLayout(self.song_info_form_layout) # Add verses widget. self.song_info_verse_list_widget = QtWidgets.QTableWidget( self.song_group_box) self.song_info_verse_list_widget.setColumnCount(1) self.song_info_verse_list_widget.horizontalHeader().setVisible(False) self.song_info_verse_list_widget.setObjectName( 'song_info_verse_list_widget') self.song_info_verse_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.NoSelection) self.song_info_verse_list_widget.setEditTriggers( QtWidgets.QAbstractItemView.NoEditTriggers) self.song_info_verse_list_widget.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff) self.song_info_verse_list_widget.setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff) self.song_info_verse_list_widget.setAlternatingRowColors(True) song_xml = SongXML() verses = song_xml.get_verses(self.song.lyrics) self.song_info_verse_list_widget.setRowCount(len(verses)) song_tags = [] for verse_number, verse in enumerate(verses): item = QtWidgets.QTableWidgetItem() item.setText(verse[1]) self.song_info_verse_list_widget.setItem(verse_number, 0, item) # We cannot use from_loose_input() here, because database # is supposed to contain English lowercase singlechar tags. verse_tag = verse[0]['type'] verse_index = None if len(verse_tag) > 1: verse_index = VerseType.from_translated_string(verse_tag) if verse_index is None: verse_index = VerseType.from_string(verse_tag, None) if verse_index is None: verse_index = VerseType.from_tag(verse_tag) verse_tag = VerseType.translated_tags[verse_index].upper() song_tags.append(str(verse_tag + verse[0]['label'])) self.song_info_verse_list_widget.setVerticalHeaderLabels(song_tags) # Resize table fields to content and table to columns self.song_info_verse_list_widget.setColumnWidth( 0, self.song_group_box.width()) self.song_info_verse_list_widget.resizeRowsToContents() # The 6 is a trial and error value since verticalHeader().length() + offset() is a little bit to small. # It seems there is no clean way to determine the real height of the table contents. # The "correct" value slightly fluctuates depending on the theme used, in the worst case # Some pixels are missing at the bottom of the table, but all themes I tried still allowed # to read the last verse line, so I'll just leave it at that. self.song_info_verse_list_widget.setFixedHeight( self.song_info_verse_list_widget.verticalHeader().length() + self.song_info_verse_list_widget.verticalHeader().offset() + 6) self.song_group_box_layout.addWidget(self.song_info_verse_list_widget) self.song_group_box_layout.addStretch() self.song_vertical_layout.addWidget(self.song_group_box) self.song_remove_button = QtWidgets.QPushButton(self) self.song_remove_button.setObjectName('song_remove_button') self.song_remove_button.setIcon(build_icon(':/songs/song_delete.png')) self.song_remove_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) self.song_vertical_layout.addWidget(self.song_remove_button, alignment=QtCore.Qt.AlignHCenter)
def do_import(self): """ Receive a CSV file to import. """ # Get encoding encoding = get_file_encoding(self.import_source)['encoding'] with self.import_source.open('r', encoding=encoding) as songs_file: songs_reader = csv.DictReader(songs_file, escapechar='\\') try: records = list(songs_reader) except csv.Error as e: self.log_error( translate('SongsPlugin.WorshipAssistantImport', 'Error reading CSV file.'), translate('SongsPlugin.WorshipAssistantImport', 'Line {number:d}: {error}').format( number=songs_reader.line_num, error=e)) return num_records = len(records) log.info('{count} records found in CSV file'.format(count=num_records)) self.import_wizard.progress_bar.setMaximum(num_records) # Create regex to strip html tags re_html_strip = re.compile(r'<[^>]+>') for index, record in enumerate(records, 1): if self.stop_import_flag: return # Ensure that all keys are uppercase record = dict( (field.upper(), value) for field, value in record.items()) # The CSV file has a line in the middle of the file where the headers are repeated. # We need to skip this line. if record['TITLE'] == "TITLE" and record[ 'AUTHOR'] == 'AUTHOR' and record['LYRICS2'] == 'LYRICS2': continue self.set_defaults() verse_order_list = [] try: self.title = record['TITLE'] if record['AUTHOR'] != EMPTY_STR: self.parse_author(record['AUTHOR']) if record['COPYRIGHT'] != EMPTY_STR: self.add_copyright(record['COPYRIGHT']) if record['CCLINR'] != EMPTY_STR: self.ccli_number = record['CCLINR'] if record['ROADMAP'] != EMPTY_STR: verse_order_list = [ x.strip() for x in record['ROADMAP'].split(',') ] lyrics = record['LYRICS2'] except UnicodeDecodeError as e: self.log_error( translate('SongsPlugin.WorshipAssistantImport', 'Record {count:d}').format(count=index), translate('SongsPlugin.WorshipAssistantImport', 'Decoding error: {error}').format(error=e)) continue except TypeError as e: self.log_error( translate('SongsPlugin.WorshipAssistantImport', 'File not valid WorshipAssistant CSV format.'), 'TypeError: {error}'.format(error=e)) return verse = '' used_verses = [] verse_id = VerseType.tags[VerseType.Verse] + '1' for line in lyrics.splitlines(): if line.startswith('['): # verse marker # Add previous verse if verse: # remove trailing linebreak, part of the WA syntax self.add_verse(verse[:-1], verse_id) used_verses.append(verse_id) verse = '' # drop the square brackets right_bracket = line.find(']') content = line[1:right_bracket].lower() match = re.match(r'(\D*)(\d+)', content) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) else: # otherwise we assume number 1 and take the whole prefix as the verse tag verse_tag = content verse_num = '1' verse_index = VerseType.from_loose_input( verse_tag) if verse_tag else 0 verse_tag = VerseType.tags[verse_index] # Update verse order when the verse name has changed verse_id = verse_tag + verse_num # Make sure we've not choosen an id already used while verse_id in verse_order_list and content in verse_order_list: verse_num = str(int(verse_num) + 1) verse_id = verse_tag + verse_num if content != verse_id: for i in range(len(verse_order_list)): if verse_order_list[i].lower() == content.lower(): verse_order_list[i] = verse_id else: # add line text to verse. Strip out html verse += re_html_strip.sub('', line) + '\n' if verse: # remove trailing linebreak, part of the WA syntax if verse.endswith('\n\n'): verse = verse[:-1] self.add_verse(verse, verse_id) used_verses.append(verse_id) if verse_order_list: # Use the verse order in the import, but remove entries that doesn't have a text cleaned_verse_order_list = [] for verse in verse_order_list: if verse in used_verses: cleaned_verse_order_list.append(verse) self.verse_order_list = cleaned_verse_order_list if not self.finish(): self.log_error( translate('SongsPlugin.WorshipAssistantImport', 'Record {count:d}').format(count=index) + (': "' + self.title + '"' if self.title else ''))
def _process_lyrics(self, foilpresenterfolie, song): """ Processes the verses and search_lyrics for the song. :param foilpresenterfolie: The foilpresenterfolie object (lxml.objectify.ObjectifiedElement). :param song: The song object. """ sxml = SongXML() temp_verse_order = {} temp_verse_order_backup = [] temp_sortnr_backup = 1 temp_sortnr_liste = [] verse_count = { VerseType.tags[VerseType.Verse]: 1, VerseType.tags[VerseType.Chorus]: 1, VerseType.tags[VerseType.Bridge]: 1, VerseType.tags[VerseType.Ending]: 1, VerseType.tags[VerseType.Other]: 1, VerseType.tags[VerseType.Intro]: 1, VerseType.tags[VerseType.PreChorus]: 1 } if not hasattr(foilpresenterfolie.strophen, 'strophe'): self.importer.log_error( to_str(foilpresenterfolie.titel), str( translate( 'SongsPlugin.FoilPresenterSongImport', 'Invalid Foilpresenter song file. No verses found.'))) self.save_song = False return for strophe in foilpresenterfolie.strophen.strophe: text = to_str(strophe.text_) if hasattr(strophe, 'text_') else '' verse_name = to_str(strophe.key) children = strophe.getchildren() sortnr = False for child in children: if child.tag == 'sortnr': verse_sortnr = to_str(strophe.sortnr) sortnr = True # In older Version there is no sortnr, but we need one if not sortnr: verse_sortnr = str(temp_sortnr_backup) temp_sortnr_backup += 1 # Foilpresenter allows e. g. "Ref" or "1", but we need "C1" or "V1". temp_sortnr_liste.append(verse_sortnr) temp_verse_name = re.compile('[0-9].*').sub('', verse_name) temp_verse_name = temp_verse_name[:3].lower() if temp_verse_name == 'ref': verse_type = VerseType.tags[VerseType.Chorus] elif temp_verse_name == 'r': verse_type = VerseType.tags[VerseType.Chorus] elif temp_verse_name == '': verse_type = VerseType.tags[VerseType.Verse] elif temp_verse_name == 'v': verse_type = VerseType.tags[VerseType.Verse] elif temp_verse_name == 'bri': verse_type = VerseType.tags[VerseType.Bridge] elif temp_verse_name == 'cod': verse_type = VerseType.tags[VerseType.Ending] elif temp_verse_name == 'sch': verse_type = VerseType.tags[VerseType.Ending] elif temp_verse_name == 'pre': verse_type = VerseType.tags[VerseType.PreChorus] elif temp_verse_name == 'int': verse_type = VerseType.tags[VerseType.Intro] else: verse_type = VerseType.tags[VerseType.Other] verse_number = re.compile('[a-zA-Z.+-_ ]*').sub('', verse_name) # Foilpresenter allows e. g. "C", but we need "C1". if not verse_number: verse_number = str(verse_count[verse_type]) verse_count[verse_type] += 1 else: # test if foilpresenter have the same versenumber two times with # different parts raise the verse number for value in temp_verse_order_backup: if value == ''.join((verse_type, verse_number)): verse_number = str(int(verse_number) + 1) verse_type_index = VerseType.from_tag(verse_type[0]) verse_type = VerseType.tags[verse_type_index] temp_verse_order[verse_sortnr] = ''.join( (verse_type[0], verse_number)) temp_verse_order_backup.append(''.join( (verse_type[0], verse_number))) sxml.add_verse_to_lyrics(verse_type, verse_number, text) song.lyrics = str(sxml.extract_xml(), 'utf-8') # Process verse order verse_order = [] verse_strophenr = [] try: for strophennummer in foilpresenterfolie.reihenfolge.strophennummer: verse_strophenr.append(strophennummer) except AttributeError: pass # Currently we do not support different "parts"! if '0' in temp_verse_order: for vers in temp_verse_order_backup: verse_order.append(vers) else: for number in verse_strophenr: numberx = temp_sortnr_liste[int(number)] verse_order.append(temp_verse_order[str(numberx)]) song.verse_order = ' '.join(verse_order)
def doImportFile(self, file): """ Process the OpenSong file - pass in a file-like object, not a file path. """ self.setDefaults() try: tree = objectify.parse(file) except (Error, LxmlError): self.logError(file.name, SongStrings.XMLSyntaxError) log.exception(u'Error parsing XML') return root = tree.getroot() if root.tag != u'song': self.logError(file.name, unicode( translate('SongsPlugin.OpenSongImport', ('Invalid OpenSong song file. Missing song tag.')))) return fields = dir(root) decode = { u'copyright': self.addCopyright, u'ccli': u'ccli_number', u'author': self.parseAuthor, u'title': u'title', u'aka': u'alternate_title', u'hymn_number': u'song_number' } for attr, fn_or_string in decode.items(): if attr in fields: ustring = unicode(root.__getattr__(attr)) if isinstance(fn_or_string, basestring): setattr(self, fn_or_string, ustring) else: fn_or_string(ustring) if u'theme' in fields and unicode(root.theme) not in self.topics: self.topics.append(unicode(root.theme)) if u'alttheme' in fields and unicode(root.alttheme) not in self.topics: self.topics.append(unicode(root.alttheme)) # data storage while importing verses = {} # keep track of verses appearance order our_verse_order = [] # default verse verse_tag = VerseType.Tags[VerseType.Verse] verse_num = u'1' # for the case where song has several sections with same marker inst = 1 if u'lyrics' in fields: lyrics = unicode(root.lyrics) else: lyrics = u'' for this_line in lyrics.split(u'\n'): # remove comments semicolon = this_line.find(u';') if semicolon >= 0: this_line = this_line[:semicolon] this_line = this_line.strip() if not this_line: continue # skip guitar chords and page and column breaks if this_line.startswith(u'.') or this_line.startswith(u'---') or this_line.startswith(u'-!!'): continue # verse/chorus/etc. marker if this_line.startswith(u'['): # drop the square brackets right_bracket = this_line.find(u']') content = this_line[1:right_bracket].lower() # have we got any digits? # If so, verse number is everything from the digits # to the end (openlp does not have concept of part verses, so # just ignore any non integers on the end (including floats)) match = re.match(u'(\D*)(\d+)', content) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) else: # otherwise we assume number 1 and take the whole prefix as # the verse tag verse_tag = content verse_num = u'1' verse_index = VerseType.from_loose_input(verse_tag) if verse_tag else 0 verse_tag = VerseType.Tags[verse_index] inst = 1 if [verse_tag, verse_num, inst] in our_verse_order and verse_num in verses.get(verse_tag, {}): inst = len(verses[verse_tag][verse_num]) + 1 continue # number at start of line.. it's verse number if this_line[0].isdigit(): verse_num = this_line[0] this_line = this_line[1:].strip() our_verse_order.append([verse_tag, verse_num, inst]) verses.setdefault(verse_tag, {}) verses[verse_tag].setdefault(verse_num, {}) if inst not in verses[verse_tag][verse_num]: verses[verse_tag][verse_num][inst] = [] our_verse_order.append([verse_tag, verse_num, inst]) # Tidy text and remove the ____s from extended words this_line = self.tidyText(this_line) this_line = this_line.replace(u'_', u'') this_line = this_line.replace(u'|', u'\n') verses[verse_tag][verse_num][inst].append(this_line) # done parsing # add verses in original order verse_joints = {} for (verse_tag, verse_num, inst) in our_verse_order: lines = u'\n'.join(verses[verse_tag][verse_num][inst]) length = 0 while(length < len(verse_num) and verse_num[length].isnumeric()): length += 1 verse_def = u'%s%s' % (verse_tag, verse_num[:length]) verse_joints[verse_def] = u'%s\n[---]\n%s' % (verse_joints[verse_def], lines) \ if verse_def in verse_joints else lines for verse_def, lines in verse_joints.iteritems(): self.addVerse(lines, verse_def) if not self.verses: self.addVerse('') # figure out the presentation order, if present if u'presentation' in fields and root.presentation: order = unicode(root.presentation) # We make all the tags in the lyrics lower case, so match that here # and then split into a list on the whitespace order = order.lower().split() for verse_def in order: match = re.match(u'(\D*)(\d+.*)', verse_def) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) if not verse_tag: verse_tag = VerseType.Tags[VerseType.Verse] else: # Assume it's no.1 if there are no digits verse_tag = verse_def verse_num = u'1' verse_def = u'%s%s' % (verse_tag, verse_num) if verse_num in verses.get(verse_tag, {}): self.verseOrderList.append(verse_def) else: log.info(u'Got order %s but not in verse tags, dropping' u'this item from presentation order', verse_def) if not self.finish(): self.logError(file.name)
def generate_slide_data(self, service_item, *, item=None, context=ServiceItemContext.Service, **kwargs): """ Generate the slide data. Needs to be implemented by the plugin. :param service_item: The service item to be built on :param item: The Song item to be used :param context: Why is it being generated :param kwargs: Consume other unused args specified by the base implementation, but not use by this one. """ log.debug('generate_slide_data: {service}, {item}, {remote}'.format( service=service_item, item=item, remote=self.remote_song)) item_id = self._get_id_of_item_to_generate(item, self.remote_song) service_item.add_capability(ItemCapabilities.CanEdit) service_item.add_capability(ItemCapabilities.CanPreview) service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.OnLoadUpdate) service_item.add_capability(ItemCapabilities.AddIfNewItem) service_item.add_capability(ItemCapabilities.CanSoftBreak) service_item.add_capability(ItemCapabilities.HasMetaData) song = self.plugin.manager.get_object(Song, item_id) service_item.theme = song.theme_name service_item.edit_id = item_id verse_list = SongXML().get_verses(song.lyrics) if self.settings.value( 'songs/add songbook slide') and song.songbook_entries: first_slide = '\n' for songbook_entry in song.songbook_entries: first_slide += '{book} #{num}'.format( book=songbook_entry.songbook.name, num=songbook_entry.entry) if songbook_entry.songbook.publisher: first_slide += ' ({pub})'.format( pub=songbook_entry.songbook.publisher) first_slide += '\n\n' service_item.add_from_text(first_slide, 'O1') # no verse list or only 1 space (in error) verse_tags_translated = False if VerseType.from_translated_string(str( verse_list[0][0]['type'])) is not None: verse_tags_translated = True if not song.verse_order.strip(): for verse in verse_list: # We cannot use from_loose_input() here, because database is supposed to contain English lowercase # single char tags. verse_tag = verse[0]['type'] verse_index = None if len(verse_tag) > 1: verse_index = VerseType.from_translated_string(verse_tag) if verse_index is None: verse_index = VerseType.from_string(verse_tag, None) if verse_index is None: verse_index = VerseType.from_tag(verse_tag) verse_tag = VerseType.translated_tags[verse_index].upper() verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label']) force_verse = verse[1].split('[--}{--]\n') for split_verse in force_verse: service_item.add_from_text(split_verse, verse_def) else: # Loop through the verse list and expand the song accordingly. for order in song.verse_order.lower().split(): if not order: break for verse in verse_list: if verse[0]['type'][0].lower() == \ order[0] and (verse[0]['label'].lower() == order[1:] or not order[1:]): if verse_tags_translated: verse_index = VerseType.from_translated_tag( verse[0]['type']) else: verse_index = VerseType.from_tag(verse[0]['type']) verse_tag = VerseType.translated_tags[verse_index] verse_def = '{tag}{label}'.format( tag=verse_tag, label=verse[0]['label']) force_verse = verse[1].split('[--}{--]\n') for split_verse in force_verse: service_item.add_from_text(split_verse, verse_def) service_item.title = song.title author_list = self.generate_footer(service_item, song) service_item.data_string = { 'title': song.search_title, 'authors': ', '.join(author_list) } service_item.xml_version = self.open_lyrics.song_to_xml(song) # Add the audio file to the service item. if song.media_files: if State().check_preconditions('media'): service_item.add_capability( ItemCapabilities.HasBackgroundAudio) total_length = 0 for m in song.media_files: total_length += self.media_controller.media_length( m.file_path) service_item.background_audio = [ m.file_path for m in song.media_files ] service_item.set_media_length(total_length) service_item.metadata.append( '<em>{label}:</em> {media}'.format( label=translate('SongsPlugin.MediaItem', 'Media'), media=service_item.background_audio)) return True
def setupUi(self): self.song_vertical_layout = QtGui.QVBoxLayout(self) self.song_vertical_layout.setObjectName('song_vertical_layout') self.song_group_box = QtGui.QGroupBox(self) self.song_group_box.setObjectName('song_group_box') self.song_group_box.setFixedWidth(300) self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box) self.song_group_box_layout.setObjectName('song_group_box_layout') self.song_info_form_layout = QtGui.QFormLayout() self.song_info_form_layout.setObjectName('song_info_form_layout') # Add title widget. self.song_title_label = QtGui.QLabel(self) self.song_title_label.setObjectName('song_title_label') self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label) self.song_title_content = QtGui.QLabel(self) self.song_title_content.setObjectName('song_title_content') self.song_title_content.setText(self.song.title) self.song_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content) # Add alternate title widget. self.song_alternate_title_label = QtGui.QLabel(self) self.song_alternate_title_label.setObjectName('song_alternate_title_label') self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label) self.song_alternate_title_content = QtGui.QLabel(self) self.song_alternate_title_content.setObjectName('song_alternate_title_content') self.song_alternate_title_content.setText(self.song.alternate_title) self.song_alternate_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content) # Add CCLI number widget. self.song_ccli_number_label = QtGui.QLabel(self) self.song_ccli_number_label.setObjectName('song_ccli_number_label') self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label) self.song_ccli_number_content = QtGui.QLabel(self) self.song_ccli_number_content.setObjectName('song_ccli_number_content') self.song_ccli_number_content.setText(self.song.ccli_number) self.song_ccli_number_content.setWordWrap(True) self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content) # Add copyright widget. self.song_copyright_label = QtGui.QLabel(self) self.song_copyright_label.setObjectName('song_copyright_label') self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label) self.song_copyright_content = QtGui.QLabel(self) self.song_copyright_content.setObjectName('song_copyright_content') self.song_copyright_content.setWordWrap(True) self.song_copyright_content.setText(self.song.copyright) self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content) # Add comments widget. self.song_comments_label = QtGui.QLabel(self) self.song_comments_label.setObjectName('song_comments_label') self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label) self.song_comments_content = QtGui.QLabel(self) self.song_comments_content.setObjectName('song_comments_content') self.song_comments_content.setText(self.song.comments) self.song_comments_content.setWordWrap(True) self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content) # Add authors widget. self.song_authors_label = QtGui.QLabel(self) self.song_authors_label.setObjectName('song_authors_label') self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label) self.song_authors_content = QtGui.QLabel(self) self.song_authors_content.setObjectName('song_authors_content') self.song_authors_content.setWordWrap(True) authors_text = ', '.join([author.display_name for author in self.song.authors]) self.song_authors_content.setText(authors_text) self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content) # Add verse order widget. self.song_verse_order_label = QtGui.QLabel(self) self.song_verse_order_label.setObjectName('song_verse_order_label') self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label) self.song_verse_order_content = QtGui.QLabel(self) self.song_verse_order_content.setObjectName('song_verse_order_content') self.song_verse_order_content.setText(self.song.verse_order) self.song_verse_order_content.setWordWrap(True) self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content) self.song_group_box_layout.addLayout(self.song_info_form_layout) # Add verses widget. self.song_info_verse_list_widget = QtGui.QTableWidget(self.song_group_box) self.song_info_verse_list_widget.setColumnCount(1) self.song_info_verse_list_widget.horizontalHeader().setVisible(False) self.song_info_verse_list_widget.setObjectName('song_info_verse_list_widget') self.song_info_verse_list_widget.setSelectionMode(QtGui.QAbstractItemView.NoSelection) self.song_info_verse_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) self.song_info_verse_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.song_info_verse_list_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.song_info_verse_list_widget.setAlternatingRowColors(True) song_xml = SongXML() verses = song_xml.get_verses(self.song.lyrics) self.song_info_verse_list_widget.setRowCount(len(verses)) song_tags = [] for verse_number, verse in enumerate(verses): item = QtGui.QTableWidgetItem() item.setText(verse[1]) self.song_info_verse_list_widget.setItem(verse_number, 0, item) # We cannot use from_loose_input() here, because database # is supposed to contain English lowercase singlechar tags. verse_tag = verse[0]['type'] verse_index = None if len(verse_tag) > 1: verse_index = VerseType.from_translated_string(verse_tag) if verse_index is None: verse_index = VerseType.from_string(verse_tag, None) if verse_index is None: verse_index = VerseType.from_tag(verse_tag) verse_tag = VerseType.translated_tags[verse_index].upper() song_tags.append(str(verse_tag + verse[0]['label'])) self.song_info_verse_list_widget.setVerticalHeaderLabels(song_tags) # Resize table fields to content and table to columns self.song_info_verse_list_widget.setColumnWidth(0, self.song_group_box.width()) self.song_info_verse_list_widget.resizeRowsToContents() # The 6 is a trial and error value since verticalHeader().length() + offset() is a little bit to small. # It seems there is no clean way to determine the real height of the table contents. # The "correct" value slightly fluctuates depending on the theme used, in the worst case # Some pixels are missing at the bottom of the table, but all themes I tried still allowed # to read the last verse line, so I'll just leave it at that. self.song_info_verse_list_widget.setFixedHeight(self.song_info_verse_list_widget.verticalHeader().length() + self.song_info_verse_list_widget.verticalHeader().offset() + 6) self.song_group_box_layout.addWidget(self.song_info_verse_list_widget) self.song_group_box_layout.addStretch() self.song_vertical_layout.addWidget(self.song_group_box) self.song_remove_button = QtGui.QPushButton(self) self.song_remove_button.setObjectName('song_remove_button') self.song_remove_button.setIcon(build_icon(':/songs/song_delete.png')) self.song_remove_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) self.song_vertical_layout.addWidget(self.song_remove_button, alignment=QtCore.Qt.AlignHCenter)
def load_song(self, song_id, preview=False): """ Loads a song. ``song_id`` The song id (int). ``preview`` Should be ``True`` if the song is also previewed (boolean). """ log.debug('Load Song') self.initialise() self.song_tab_widget.setCurrentIndex(0) self.load_authors() self.load_topics() self.load_books() self.load_media_files() self.song = self.manager.get_object(Song, song_id) self.title_edit.setText(self.song.title) self.alternative_edit.setText( self.song.alternate_title if self.song.alternate_title else '') if self.song.song_book_id != 0: book_name = self.manager.get_object(Book, self.song.song_book_id) find_and_set_in_combo_box(self.song_book_combo_box, str(book_name.name)) else: self.song_book_combo_box.setEditText('') self.song_book_combo_box.setCurrentIndex(0) if self.song.theme_name: find_and_set_in_combo_box(self.theme_combo_box, str(self.song.theme_name)) else: # Clear the theme combo box in case it was previously set (bug #1212801) self.theme_combo_box.setEditText('') self.theme_combo_box.setCurrentIndex(0) self.copyright_edit.setText(self.song.copyright if self.song.copyright else '') self.comments_edit.setPlainText(self.song.comments if self.song.comments else '') self.ccli_number_edit.setText(self.song.ccli_number if self.song.ccli_number else '') self.song_book_number_edit.setText(self.song.song_number if self.song.song_number else '') # lazy xml migration for now self.verse_list_widget.clear() self.verse_list_widget.setRowCount(0) verse_tags_translated = False if self.song.lyrics.startswith('<?xml version='): songXML = SongXML() verse_list = songXML.get_verses(self.song.lyrics) for count, verse in enumerate(verse_list): self.verse_list_widget.setRowCount(self.verse_list_widget.rowCount() + 1) # This silently migrates from localized verse type markup. # If we trusted the database, this would be unnecessary. verse_tag = verse[0]['type'] index = None if len(verse_tag) > 1: index = VerseType.from_translated_string(verse_tag) if index is None: index = VerseType.from_string(verse_tag, None) else: verse_tags_translated = True if index is None: index = VerseType.from_tag(verse_tag) verse[0]['type'] = VerseType.tags[index] if verse[0]['label'] == '': verse[0]['label'] = '1' verse_def = '%s%s' % (verse[0]['type'], verse[0]['label']) item = QtGui.QTableWidgetItem(verse[1]) item.setData(QtCore.Qt.UserRole, verse_def) self.verse_list_widget.setItem(count, 0, item) else: verses = self.song.lyrics.split('\n\n') for count, verse in enumerate(verses): self.verse_list_widget.setRowCount(self.verse_list_widget.rowCount() + 1) item = QtGui.QTableWidgetItem(verse) verse_def = '%s%s' % (VerseType.tags[VerseType.Verse], str(count + 1)) item.setData(QtCore.Qt.UserRole, verse_def) self.verse_list_widget.setItem(count, 0, item) if self.song.verse_order: # we translate verse order translated = [] for verse_def in self.song.verse_order.split(): verse_index = None if verse_tags_translated: verse_index = VerseType.from_translated_tag(verse_def[0], None) if verse_index is None: verse_index = VerseType.from_tag(verse_def[0]) verse_tag = VerseType.translated_tags[verse_index].upper() translated.append('%s%s' % (verse_tag, verse_def[1:])) self.verse_order_edit.setText(' '.join(translated)) else: self.verse_order_edit.setText('') self.tag_rows() # clear the results self.authors_list_view.clear() for author in self.song.authors: author_name = QtGui.QListWidgetItem(str(author.display_name)) author_name.setData(QtCore.Qt.UserRole, author.id) self.authors_list_view.addItem(author_name) # clear the results self.topics_list_view.clear() for topic in self.song.topics: topic_name = QtGui.QListWidgetItem(str(topic.name)) topic_name.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_name) self.audio_list_widget.clear() for media in self.song.media_files: media_file = QtGui.QListWidgetItem(os.path.split(media.file_name)[1]) media_file.setData(QtCore.Qt.UserRole, media.file_name) self.audio_list_widget.addItem(media_file) self.title_edit.setFocus() # Hide or show the preview button. self.preview_button.setVisible(preview) # Check if all verse tags are used. self.on_verse_order_text_changed(self.verse_order_edit.text())
def parse(self, data, cell=False): """ Process the records :param data: The data to be processed :param cell: ? :return: """ if not cell and (len(data) == 0 or data[0:1] != b'[' or data.strip()[-1:] != b']'): self.log_error('File is malformed') return False i = 1 verse_type = VerseType.tags[VerseType.Verse] while i < len(data): # Data is held as #name: value pairs inside groups marked as []. # Now we are looking for the name. if data[i:i + 1] == b'#': name_end = data.find(b':', i + 1) name = data[i + 1:name_end].decode(self.encoding).upper() i = name_end + 1 while data[i:i + 1] == b' ': i += 1 if data[i:i + 1] == b'"': end = data.find(b'"', i + 1) value = data[i + 1:end] elif data[i:i + 1] == b'[': j = i inside_quotes = False while j < len(data): char = data[j:j + 1] if char == b'"': inside_quotes = not inside_quotes elif not inside_quotes and char == b']': end = j + 1 break j += 1 value = data[i:end] else: end = data.find(b',', i + 1) if data.find(b'(', i, end) != -1: end = data.find(b')', i) + 1 value = data[i:end] # If we are in the main group. if not cell: if name == 'TITLE': self.title = self.decode(self.unescape(value)) elif name == 'AUTHOR': author = self.decode(self.unescape(value)) if len(author): self.add_author(author) elif name == 'COPYRIGHT': self.add_copyright(self.decode(self.unescape(value))) elif name[0:4] == 'CELL': self.parse(value, cell=name[4:]) # We are in a verse group. else: if name == 'MARKER_NAME': value = self.decode(value).strip() if len(value): verse_type = VerseType.tags[ VerseType.from_loose_input(value[0])] if len(value) >= 2 and value[-1] in [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ]: verse_type = "{verse}{value}".format( verse=verse_type, value=value[-1]) elif name == 'HOTKEY': value = self.decode(value).strip() # HOTKEY always appears after MARKER_NAME, so it # effectively overrides MARKER_NAME, if present. if len(value) and value in list( HOTKEY_TO_VERSE_TYPE.keys()): verse_type = HOTKEY_TO_VERSE_TYPE[value] if name == 'RTF': value = self.unescape(value) value = self.decode(value) result = strip_rtf(value, self.encoding) if result is None: return False verse, self.encoding = result lines = verse.strip().split('\n') # If any line inside any verse contains CCLI or # only Public Domain, we treat this as special data: # we remove that line and add data to specific field. processed_lines = [] for i in range(len(lines)): line = lines[i].strip() if line[:3].lower() == 'ccl': m = re.search(r'[0-9]+', line) if m: self.ccli_number = int(m.group(0)) continue elif line.lower() == 'public domain': self.add_copyright('Public Domain') continue processed_lines.append(line) self.add_verse('\n'.join(processed_lines).strip(), verse_type) if end == -1: break i = end + 1 i += 1 return True
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, context=ServiceItemContext.Service): """ Generate the slide data. Needs to be implemented by the plugin. :param service_item: The service item to be built on :param item: The Song item to be used :param xml_version: The xml version (not used) :param remote: Triggered from remote :param context: Why is it being generated """ log.debug('generate_slide_data: {service}, {item}, {remote}'.format(service=service_item, item=item, remote=self.remote_song)) item_id = self._get_id_of_item_to_generate(item, self.remote_song) service_item.add_capability(ItemCapabilities.CanEdit) service_item.add_capability(ItemCapabilities.CanPreview) service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.OnLoadUpdate) service_item.add_capability(ItemCapabilities.AddIfNewItem) service_item.add_capability(ItemCapabilities.CanSoftBreak) song = self.plugin.manager.get_object(Song, item_id) service_item.theme = song.theme_name service_item.edit_id = item_id verse_list = SongXML().get_verses(song.lyrics) # no verse list or only 1 space (in error) verse_tags_translated = False if VerseType.from_translated_string(str(verse_list[0][0]['type'])) is not None: verse_tags_translated = True if not song.verse_order.strip(): for verse in verse_list: # We cannot use from_loose_input() here, because database is supposed to contain English lowercase # singlechar tags. verse_tag = verse[0]['type'] verse_index = None if len(verse_tag) > 1: verse_index = VerseType.from_translated_string(verse_tag) if verse_index is None: verse_index = VerseType.from_string(verse_tag, None) if verse_index is None: verse_index = VerseType.from_tag(verse_tag) verse_tag = VerseType.translated_tags[verse_index].upper() verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label']) service_item.add_from_text(str(verse[1]), verse_def) else: # Loop through the verse list and expand the song accordingly. for order in song.verse_order.lower().split(): if not order: break for verse in verse_list: if verse[0]['type'][0].lower() == \ order[0] and (verse[0]['label'].lower() == order[1:] or not order[1:]): if verse_tags_translated: verse_index = VerseType.from_translated_tag(verse[0]['type']) else: verse_index = VerseType.from_tag(verse[0]['type']) verse_tag = VerseType.translated_tags[verse_index] verse_def = '{tag}{text}'.format(tag=verse_tag, text=verse[0]['label']) service_item.add_from_text(verse[1], verse_def) service_item.title = song.title author_list = self.generate_footer(service_item, song) service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)} service_item.xml_version = self.open_lyrics.song_to_xml(song) # Add the audio file to the service item. if song.media_files: service_item.add_capability(ItemCapabilities.HasBackgroundAudio) service_item.background_audio = [m.file_name for m in song.media_files] return True
def generateSlideData(self, service_item, item=None, xmlVersion=False, remote=False, context=ServiceItemContext.Service): log.debug(u'generateSlideData: %s, %s, %s' % (service_item, item, self.remoteSong)) item_id = self._getIdOfItemToGenerate(item, self.remoteSong) service_item.add_capability(ItemCapabilities.CanEdit) service_item.add_capability(ItemCapabilities.CanPreview) service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.OnLoadUpdate) service_item.add_capability(ItemCapabilities.AddIfNewItem) service_item.add_capability(ItemCapabilities.CanSoftBreak) song = self.plugin.manager.get_object(Song, item_id) service_item.theme = song.theme_name service_item.edit_id = item_id if song.lyrics.startswith(u'<?xml version='): verse_list = SongXML().get_verses(song.lyrics) # no verse list or only 1 space (in error) verse_tags_translated = False if VerseType.from_translated_string(unicode(verse_list[0][0][u'type'])) is not None: verse_tags_translated = True if not song.verse_order.strip(): for verse in verse_list: # We cannot use from_loose_input() here, because database # is supposed to contain English lowercase singlechar tags. verse_tag = verse[0][u'type'] verse_index = None if len(verse_tag) > 1: verse_index = VerseType.from_translated_string(verse_tag) if verse_index is None: verse_index = VerseType.from_string(verse_tag, None) if verse_index is None: verse_index = VerseType.from_tag(verse_tag) verse_tag = VerseType.TranslatedTags[verse_index].upper() verse_def = u'%s%s' % (verse_tag, verse[0][u'label']) service_item.add_from_text(unicode(verse[1]), verse_def) else: # Loop through the verse list and expand the song accordingly. for order in song.verse_order.lower().split(): if not order: break for verse in verse_list: if verse[0][u'type'][0].lower() == order[0] and (verse[0][u'label'].lower() == order[1:] or \ not order[1:]): if verse_tags_translated: verse_index = VerseType.from_translated_tag(verse[0][u'type']) else: verse_index = VerseType.from_tag(verse[0][u'type']) verse_tag = VerseType.TranslatedTags[verse_index] verse_def = u'%s%s' % (verse_tag, verse[0][u'label']) service_item.add_from_text(verse[1], verse_def) else: verses = song.lyrics.split(u'\n\n') for slide in verses: service_item.add_from_text(unicode(slide)) service_item.title = song.title author_list = [unicode(author.display_name) for author in song.authors] service_item.raw_footer.append(song.title) service_item.raw_footer.append(create_separated_list(author_list)) service_item.raw_footer.append(song.copyright) if Settings().value(u'general/ccli number'): service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + Settings().value(u'general/ccli number')) service_item.audit = [ song.title, author_list, song.copyright, unicode(song.ccli_number) ] service_item.data_string = {u'title': song.search_title, u'authors': u', '.join(author_list)} service_item.xml_version = self.openLyrics.song_to_xml(song) # Add the audio file to the service item. if song.media_files: service_item.add_capability(ItemCapabilities.HasBackgroundAudio) service_item.background_audio = [m.file_name for m in song.media_files] return True
def do_import(self): """ Receive a CSV file to import. """ # Get encoding detect_file = open(self.import_source, 'rb') detect_content = detect_file.read() details = chardet.detect(detect_content) detect_file.close() songs_file = open(self.import_source, 'r', encoding=details['encoding']) songs_reader = csv.DictReader(songs_file, escapechar='\\') try: records = list(songs_reader) except csv.Error as e: self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Error reading CSV file.'), translate('SongsPlugin.WorshipAssistantImport', 'Line %d: %s') % (songs_reader.line_num, e)) return num_records = len(records) log.info('%s records found in CSV file' % num_records) self.import_wizard.progress_bar.setMaximum(num_records) # Create regex to strip html tags re_html_strip = re.compile(r'<[^>]+>') for index, record in enumerate(records, 1): if self.stop_import_flag: return # Ensure that all keys are uppercase record = dict((field.upper(), value) for field, value in record.items()) # The CSV file has a line in the middle of the file where the headers are repeated. # We need to skip this line. if record['TITLE'] == "TITLE" and record['AUTHOR'] == 'AUTHOR' and record['LYRICS2'] == 'LYRICS2': continue self.set_defaults() verse_order_list = [] try: self.title = record['TITLE'] if record['AUTHOR'] != EMPTY_STR: self.parse_author(record['AUTHOR']) if record['COPYRIGHT'] != EMPTY_STR: self.add_copyright(record['COPYRIGHT']) if record['CCLINR'] != EMPTY_STR: self.ccli_number = record['CCLINR'] if record['ROADMAP'] != EMPTY_STR: verse_order_list = [x.strip() for x in record['ROADMAP'].split(',')] lyrics = record['LYRICS2'] except UnicodeDecodeError as e: self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d' % index), translate('SongsPlugin.WorshipAssistantImport', 'Decoding error: %s') % e) continue except TypeError as e: self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'File not valid WorshipAssistant CSV format.'), 'TypeError: %s' % e) return verse = '' used_verses = [] verse_id = VerseType.tags[VerseType.Verse] + '1' for line in lyrics.splitlines(): if line.startswith('['): # verse marker # Add previous verse if verse: # remove trailing linebreak, part of the WA syntax self.add_verse(verse[:-1], verse_id) used_verses.append(verse_id) verse = '' # drop the square brackets right_bracket = line.find(']') content = line[1:right_bracket].lower() match = re.match('(\D*)(\d+)', content) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) else: # otherwise we assume number 1 and take the whole prefix as the verse tag verse_tag = content verse_num = '1' verse_index = VerseType.from_loose_input(verse_tag) if verse_tag else 0 verse_tag = VerseType.tags[verse_index] # Update verse order when the verse name has changed verse_id = verse_tag + verse_num # Make sure we've not choosen an id already used while verse_id in verse_order_list and content in verse_order_list: verse_num = str(int(verse_num) + 1) verse_id = verse_tag + verse_num if content != verse_id: for i in range(len(verse_order_list)): if verse_order_list[i].lower() == content.lower(): verse_order_list[i] = verse_id else: # add line text to verse. Strip out html verse += re_html_strip.sub('', line) + '\n' if verse: # remove trailing linebreak, part of the WA syntax if verse.endswith('\n\n'): verse = verse[:-1] self.add_verse(verse, verse_id) used_verses.append(verse_id) if verse_order_list: # Use the verse order in the import, but remove entries that doesn't have a text cleaned_verse_order_list = [] for verse in verse_order_list: if verse in used_verses: cleaned_verse_order_list.append(verse) self.verse_order_list = cleaned_verse_order_list if not self.finish(): self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d') % index + (': "' + self.title + '"' if self.title else '')) songs_file.close()
def _process_lyrics(self, foilpresenterfolie, song): """ Processes the verses and search_lyrics for the song. :param foilpresenterfolie: The foilpresenterfolie object (lxml.objectify.ObjectifiedElement). :param song: The song object. """ sxml = SongXML() temp_verse_order = {} temp_verse_order_backup = [] temp_sortnr_backup = 1 temp_sortnr_liste = [] verse_count = { VerseType.tags[VerseType.Verse]: 1, VerseType.tags[VerseType.Chorus]: 1, VerseType.tags[VerseType.Bridge]: 1, VerseType.tags[VerseType.Ending]: 1, VerseType.tags[VerseType.Other]: 1, VerseType.tags[VerseType.Intro]: 1, VerseType.tags[VerseType.PreChorus]: 1 } if not hasattr(foilpresenterfolie.strophen, 'strophe'): self.importer.log_error(self._child(foilpresenterfolie.titel), str(translate('SongsPlugin.FoilPresenterSongImport', 'Invalid Foilpresenter song file. No verses found.'))) self.save_song = False return for strophe in foilpresenterfolie.strophen.strophe: text = self._child(strophe.text_) if hasattr(strophe, 'text_') else '' verse_name = self._child(strophe.key) children = strophe.getchildren() sortnr = False for child in children: if child.tag == 'sortnr': verse_sortnr = self._child(strophe.sortnr) sortnr = True # In older Version there is no sortnr, but we need one if not sortnr: verse_sortnr = str(temp_sortnr_backup) temp_sortnr_backup += 1 # Foilpresenter allows e. g. "Ref" or "1", but we need "C1" or "V1". temp_sortnr_liste.append(verse_sortnr) temp_verse_name = re.compile('[0-9].*').sub('', verse_name) temp_verse_name = temp_verse_name[:3].lower() if temp_verse_name == 'ref': verse_type = VerseType.tags[VerseType.Chorus] elif temp_verse_name == 'r': verse_type = VerseType.tags[VerseType.Chorus] elif temp_verse_name == '': verse_type = VerseType.tags[VerseType.Verse] elif temp_verse_name == 'v': verse_type = VerseType.tags[VerseType.Verse] elif temp_verse_name == 'bri': verse_type = VerseType.tags[VerseType.Bridge] elif temp_verse_name == 'cod': verse_type = VerseType.tags[VerseType.Ending] elif temp_verse_name == 'sch': verse_type = VerseType.tags[VerseType.Ending] elif temp_verse_name == 'pre': verse_type = VerseType.tags[VerseType.PreChorus] elif temp_verse_name == 'int': verse_type = VerseType.tags[VerseType.Intro] else: verse_type = VerseType.tags[VerseType.Other] verse_number = re.compile('[a-zA-Z.+-_ ]*').sub('', verse_name) # Foilpresenter allows e. g. "C", but we need "C1". if not verse_number: verse_number = str(verse_count[verse_type]) verse_count[verse_type] += 1 else: # test if foilpresenter have the same versenumber two times with # different parts raise the verse number for value in temp_verse_order_backup: if value == ''.join((verse_type, verse_number)): verse_number = str(int(verse_number) + 1) verse_type_index = VerseType.from_tag(verse_type[0]) verse_type = VerseType.tags[verse_type_index] temp_verse_order[verse_sortnr] = ''.join((verse_type[0], verse_number)) temp_verse_order_backup.append(''.join((verse_type[0], verse_number))) sxml.add_verse_to_lyrics(verse_type, verse_number, text) song.lyrics = str(sxml.extract_xml(), 'utf-8') # Process verse order verse_order = [] verse_strophenr = [] try: for strophennummer in foilpresenterfolie.reihenfolge.strophennummer: verse_strophenr.append(strophennummer) except AttributeError: pass # Currently we do not support different "parts"! if '0' in temp_verse_order: for vers in temp_verse_order_backup: verse_order.append(vers) else: for number in verse_strophenr: numberx = temp_sortnr_liste[int(number)] verse_order.append(temp_verse_order[str(numberx)]) song.verse_order = ' '.join(verse_order)
def do_import_file(self, file): """ Process the OpenSong file - pass in a file-like object, not a file path. """ self.set_defaults() try: tree = objectify.parse(file) except (Error, LxmlError): self.log_error(file.name, SongStrings.XMLSyntaxError) log.exception('Error parsing XML') return root = tree.getroot() if root.tag != 'song': self.log_error(file.name, str( translate('SongsPlugin.OpenSongImport', 'Invalid OpenSong song file. Missing song tag.'))) return fields = dir(root) decode = { 'copyright': self.add_copyright, 'ccli': 'ccli_number', 'author': self.parse_author, 'title': 'title', 'aka': 'alternate_title', 'hymn_number': self.parse_song_book_name_and_number, 'user1': self.add_comment, 'user2': self.add_comment, 'user3': self.add_comment } for attr, fn_or_string in list(decode.items()): if attr in fields: ustring = str(root.__getattr__(attr)) if isinstance(fn_or_string, str): if attr in ['ccli']: if ustring: setattr(self, fn_or_string, int(ustring)) else: setattr(self, fn_or_string, None) else: setattr(self, fn_or_string, ustring) else: fn_or_string(ustring) # Themes look like "God: Awe/Wonder", but we just want # "Awe" and "Wonder". We use a set to ensure each topic # is only added once, in case it is already there, which # is actually quite likely if the alttheme is set topics = set(self.topics) if 'theme' in fields: theme = str(root.theme) subthemes = theme[theme.find(':')+1:].split('/') for topic in subthemes: topics.add(topic.strip()) if 'alttheme' in fields: theme = str(root.alttheme) subthemes = theme[theme.find(':')+1:].split('/') for topic in subthemes: topics.add(topic.strip()) self.topics = list(topics) self.topics.sort() # data storage while importing verses = {} # keep track of verses appearance order our_verse_order = [] # default verse verse_tag = VerseType.tags[VerseType.Verse] verse_num = '1' # for the case where song has several sections with same marker inst = 1 if 'lyrics' in fields: lyrics = str(root.lyrics) else: lyrics = '' for this_line in lyrics.split('\n'): if not this_line.strip(): continue # skip this line if it is a comment if this_line.startswith(';'): continue # skip guitar chords and page and column breaks if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'): continue # verse/chorus/etc. marker if this_line.startswith('['): # drop the square brackets right_bracket = this_line.find(']') content = this_line[1:right_bracket].lower() # have we got any digits? If so, verse number is everything from the digits to the end (openlp does not # have concept of part verses, so just ignore any non integers on the end (including floats)) match = re.match('(\D*)(\d+)', content) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) else: # otherwise we assume number 1 and take the whole prefix as the verse tag verse_tag = content verse_num = '1' verse_index = VerseType.from_loose_input(verse_tag) if verse_tag else 0 verse_tag = VerseType.tags[verse_index] inst = 1 if [verse_tag, verse_num, inst] in our_verse_order and verse_num in verses.get(verse_tag, {}): inst = len(verses[verse_tag][verse_num]) + 1 continue # number at start of line.. it's verse number if this_line[0].isdigit(): verse_num = this_line[0] this_line = this_line[1:].strip() verses.setdefault(verse_tag, {}) verses[verse_tag].setdefault(verse_num, {}) if inst not in verses[verse_tag][verse_num]: verses[verse_tag][verse_num][inst] = [] our_verse_order.append([verse_tag, verse_num, inst]) # Tidy text and remove the ____s from extended words this_line = self.tidy_text(this_line) this_line = this_line.replace('_', '') this_line = this_line.replace('||', '\n[---]\n') this_line = this_line.strip() # If the line consists solely of a '|', then just use the implicit newline # Otherwise, add a newline for each '|' if this_line == '|': this_line = '' else: this_line = this_line.replace('|', '\n') verses[verse_tag][verse_num][inst].append(this_line) # done parsing # add verses in original order verse_joints = {} for (verse_tag, verse_num, inst) in our_verse_order: lines = '\n'.join(verses[verse_tag][verse_num][inst]) length = 0 while length < len(verse_num) and verse_num[length].isnumeric(): length += 1 verse_def = '%s%s' % (verse_tag, verse_num[:length]) verse_joints[verse_def] = '%s\n[---]\n%s' % (verse_joints[verse_def], lines) \ if verse_def in verse_joints else lines # Parsing the dictionary produces the elements in a non-intuitive order. While it "works", it's not a # natural layout should the user come back to edit the song. Instead we sort by the verse type, so that we # get all the verses in order (v1, v2, ...), then the chorus(es), bridge(s), pre-chorus(es) etc. We use a # tuple for the key, since tuples naturally sort in this manner. verse_defs = sorted(verse_joints.keys(), key=lambda verse_def: (VerseType.from_tag(verse_def[0]), int(verse_def[1:]))) for verse_def in verse_defs: lines = verse_joints[verse_def] self.add_verse(lines, verse_def) if not self.verses: self.add_verse('') # figure out the presentation order, if present if 'presentation' in fields and root.presentation: order = str(root.presentation) # We make all the tags in the lyrics lower case, so match that here and then split into a list on the # whitespace. order = order.lower().split() for verse_def in order: match = re.match('(\D*)(\d+.*)', verse_def) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) if not verse_tag: verse_tag = VerseType.tags[VerseType.Verse] else: # Assume it's no.1 if there are no digits verse_tag = verse_def verse_num = '1' verse_index = VerseType.from_loose_input(verse_tag) verse_tag = VerseType.tags[verse_index] verse_def = '%s%s' % (verse_tag, verse_num) if verse_num in verses.get(verse_tag, {}): self.verse_order_list.append(verse_def) else: log.info('Got order %s but not in verse tags, dropping this item from presentation order', verse_def) if not self.finish(): self.log_error(file.name)
def parse(self, data, cell=False): """ Process the records :param data: The data to be processed :param cell: ? :return: """ if len(data) == 0 or data[0:1] != '[' or data[-1] != ']': self.log_error('File is malformed') return False i = 1 verse_type = VerseType.tags[VerseType.Verse] while i < len(data): # Data is held as #name: value pairs inside groups marked as []. # Now we are looking for the name. if data[i:i + 1] == '#': name_end = data.find(':', i + 1) name = data[i + 1:name_end].upper() i = name_end + 1 while data[i:i + 1] == ' ': i += 1 if data[i:i + 1] == '"': end = data.find('"', i + 1) value = data[i + 1:end] elif data[i:i + 1] == '[': j = i inside_quotes = False while j < len(data): char = data[j:j + 1] if char == '"': inside_quotes = not inside_quotes elif not inside_quotes and char == ']': end = j + 1 break j += 1 value = data[i:end] else: end = data.find(',', i + 1) if data.find('(', i, end) != -1: end = data.find(')', i) + 1 value = data[i:end] # If we are in the main group. if not cell: if name == 'TITLE': self.title = self.decode(self.unescape(value)) elif name == 'AUTHOR': author = self.decode(self.unescape(value)) if len(author): self.add_author(author) elif name == 'COPYRIGHT': self.copyright = self.decode(self.unescape(value)) elif name[0:4] == 'CELL': self.parse(value, cell=name[4:]) # We are in a verse group. else: if name == 'MARKER_NAME': value = value.strip() if len(value): verse_type = VerseType.tags[VerseType.from_loose_input(value[0])] if len(value) >= 2 and value[-1] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']: verse_type = "%s%s" % (verse_type, value[-1]) elif name == 'HOTKEY': # HOTKEY always appears after MARKER_NAME, so it # effectively overrides MARKER_NAME, if present. if len(value) and value in list(HOTKEY_TO_VERSE_TYPE.keys()): verse_type = HOTKEY_TO_VERSE_TYPE[value] if name == 'RTF': value = self.unescape(value) result = strip_rtf(value, self.encoding) if result is None: return verse, self.encoding = result lines = verse.strip().split('\n') # If any line inside any verse contains CCLI or # only Public Domain, we treat this as special data: # we remove that line and add data to specific field. processed_lines = [] for i in range(len(lines)): line = lines[i].strip() if line[:3].lower() == 'ccl': m = re.search(r'[0-9]+', line) if m: self.ccli_number = int(m.group(0)) continue elif line.lower() == 'public domain': self.copyright = 'Public Domain' continue processed_lines.append(line) self.add_verse('\n'.join(processed_lines).strip(), verse_type) if end == -1: break i = end + 1 i += 1 return True
def save_song(self, preview=False): """ Get all the data from the widgets on the form, and then save it to the database. The form has been validated and all reference items (Authors, Books and Topics) have been saved before this function is called. ``preview`` Should be ``True`` if the song is also previewed (boolean). """ # The Song() assignment. No database calls should be made while a # Song() is in a partially complete state. if not self.song: self.song = Song() self.song.title = self.title_edit.text() self.song.alternate_title = self.alternative_edit.text() self.song.copyright = self.copyright_edit.text() # Values will be set when cleaning the song. self.song.search_title = '' self.song.search_lyrics = '' self.song.verse_order = '' self.song.comments = self.comments_edit.toPlainText() ordertext = self.verse_order_edit.text() order = [] for item in ordertext.split(): verse_tag = VerseType.tags[VerseType.from_translated_tag(item[0])] verse_num = item[1:].lower() order.append('%s%s' % (verse_tag, verse_num)) self.song.verse_order = ' '.join(order) self.song.ccli_number = self.ccli_number_edit.text() self.song.song_number = self.song_book_number_edit.text() book_name = self.song_book_combo_box.currentText() if book_name: self.song.book = self.manager.get_object_filtered(Book, Book.name == book_name) else: self.song.book = None theme_name = self.theme_combo_box.currentText() if theme_name: self.song.theme_name = theme_name else: self.song.theme_name = None self._process_lyrics() self.song.authors = [] for row in range(self.authors_list_view.count()): item = self.authors_list_view.item(row) authorId = (item.data(QtCore.Qt.UserRole)) author = self.manager.get_object(Author, authorId) if author is not None: self.song.authors.append(author) self.song.topics = [] for row in range(self.topics_list_view.count()): item = self.topics_list_view.item(row) topicId = (item.data(QtCore.Qt.UserRole)) topic = self.manager.get_object(Topic, topicId) if topic is not None: self.song.topics.append(topic) # Save the song here because we need a valid id for the audio files. clean_song(self.manager, self.song) self.manager.save_object(self.song) audio_files = [a.file_name for a in self.song.media_files] log.debug(audio_files) save_path = os.path.join(AppLocation.get_section_data_path(self.media_item.plugin.name), 'audio', str(self.song.id)) check_directory_exists(save_path) self.song.media_files = [] files = [] for row in range(self.audio_list_widget.count()): item = self.audio_list_widget.item(row) filename = item.data(QtCore.Qt.UserRole) if not filename.startswith(save_path): oldfile, filename = filename, os.path.join(save_path, os.path.split(filename)[1]) shutil.copyfile(oldfile, filename) files.append(filename) media_file = MediaFile() media_file.file_name = filename media_file.type = 'audio' media_file.weight = row self.song.media_files.append(media_file) for audio in audio_files: if audio not in files: try: os.remove(audio) except: log.exception('Could not remove file: %s', audio) if not files: try: os.rmdir(save_path) except OSError: log.exception('Could not remove directory: %s', save_path) clean_song(self.manager, self.song) self.manager.save_object(self.song) self.media_item.auto_select_id = self.song.id
def do_import_file(self, file): """ Process the OpenSong file - pass in a file-like object, not a file path. """ self.set_defaults() try: tree = objectify.parse(file) except (Error, LxmlError): self.log_error(file.name, SongStrings.XMLSyntaxError) log.exception('Error parsing XML') return root = tree.getroot() if root.tag != 'song': self.log_error( file.name, str( translate( 'SongsPlugin.OpenSongImport', 'Invalid OpenSong song file. Missing song tag.'))) return fields = dir(root) decode = { 'copyright': self.add_copyright, 'ccli': 'ccli_number', 'author': self.parse_author, 'title': 'title', 'aka': 'alternate_title', 'hymn_number': self.parse_song_book_name_and_number, 'user1': self.add_comment, 'user2': self.add_comment, 'user3': self.add_comment } for attr, fn_or_string in list(decode.items()): if attr in fields: ustring = str(root.__getattr__(attr)) if isinstance(fn_or_string, str): if attr in ['ccli']: ustring = ''.join(re.findall('\d+', ustring)) if ustring: setattr(self, fn_or_string, int(ustring)) else: setattr(self, fn_or_string, None) else: setattr(self, fn_or_string, ustring) else: fn_or_string(ustring) # Themes look like "God: Awe/Wonder", but we just want # "Awe" and "Wonder". We use a set to ensure each topic # is only added once, in case it is already there, which # is actually quite likely if the alttheme is set topics = set(self.topics) if 'theme' in fields: theme = str(root.theme) subthemes = theme[theme.find(':') + 1:].split('/') for topic in subthemes: topics.add(topic.strip()) if 'alttheme' in fields: theme = str(root.alttheme) subthemes = theme[theme.find(':') + 1:].split('/') for topic in subthemes: topics.add(topic.strip()) self.topics = list(topics) self.topics.sort() # data storage while importing verses = {} # keep track of verses appearance order our_verse_order = [] # default verse verse_tag = VerseType.tags[VerseType.Verse] verse_num = '1' # for the case where song has several sections with same marker inst = 1 if 'lyrics' in fields: lyrics = str(root.lyrics) else: lyrics = '' for this_line in lyrics.split('\n'): if not this_line.strip(): continue # skip this line if it is a comment if this_line.startswith(';'): continue # skip guitar chords and page and column breaks if this_line.startswith('.') or this_line.startswith( '---') or this_line.startswith('-!!'): continue # verse/chorus/etc. marker if this_line.startswith('['): # drop the square brackets right_bracket = this_line.find(']') content = this_line[1:right_bracket].lower() # have we got any digits? If so, verse number is everything from the digits to the end (openlp does not # have concept of part verses, so just ignore any non integers on the end (including floats)) match = re.match('(\D*)(\d+)', content) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) else: # otherwise we assume number 1 and take the whole prefix as the verse tag verse_tag = content verse_num = '1' verse_index = VerseType.from_loose_input( verse_tag) if verse_tag else 0 verse_tag = VerseType.tags[verse_index] inst = 1 if [verse_tag, verse_num, inst ] in our_verse_order and verse_num in verses.get( verse_tag, {}): inst = len(verses[verse_tag][verse_num]) + 1 continue # number at start of line.. it's verse number if this_line[0].isdigit(): verse_num = this_line[0] this_line = this_line[1:].strip() verses.setdefault(verse_tag, {}) verses[verse_tag].setdefault(verse_num, {}) if inst not in verses[verse_tag][verse_num]: verses[verse_tag][verse_num][inst] = [] our_verse_order.append([verse_tag, verse_num, inst]) # Tidy text and remove the ____s from extended words this_line = self.tidy_text(this_line) this_line = this_line.replace('_', '') this_line = this_line.replace('||', '\n[---]\n') this_line = this_line.strip() # If the line consists solely of a '|', then just use the implicit newline # Otherwise, add a newline for each '|' if this_line == '|': this_line = '' else: this_line = this_line.replace('|', '\n') verses[verse_tag][verse_num][inst].append(this_line) # done parsing # add verses in original order verse_joints = {} for (verse_tag, verse_num, inst) in our_verse_order: lines = '\n'.join(verses[verse_tag][verse_num][inst]) length = 0 while length < len(verse_num) and verse_num[length].isnumeric(): length += 1 verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num[:length]) verse_joints[verse_def] = '{verse}\n[---]\n{lines}'.format(verse=verse_joints[verse_def], lines=lines) \ if verse_def in verse_joints else lines # Parsing the dictionary produces the elements in a non-intuitive order. While it "works", it's not a # natural layout should the user come back to edit the song. Instead we sort by the verse type, so that we # get all the verses in order (v1, v2, ...), then the chorus(es), bridge(s), pre-chorus(es) etc. We use a # tuple for the key, since tuples naturally sort in this manner. verse_defs = sorted( verse_joints.keys(), key=lambda verse_def: (VerseType.from_tag(verse_def[0]), int(verse_def[1:]))) for verse_def in verse_defs: lines = verse_joints[verse_def] self.add_verse(lines, verse_def) if not self.verses: self.add_verse('') # figure out the presentation order, if present if 'presentation' in fields and root.presentation: order = str(root.presentation) # We make all the tags in the lyrics lower case, so match that here and then split into a list on the # whitespace. order = order.lower().split() for verse_def in order: match = re.match('(\D*)(\d+.*)', verse_def) if match is not None: verse_tag = match.group(1) verse_num = match.group(2) if not verse_tag: verse_tag = VerseType.tags[VerseType.Verse] else: # Assume it's no.1 if there are no digits verse_tag = verse_def verse_num = '1' verse_index = VerseType.from_loose_input(verse_tag) verse_tag = VerseType.tags[verse_index] verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num) if verse_num in verses.get(verse_tag, {}): self.verse_order_list.append(verse_def) else: log.info( 'Got order {order} but not in verse tags, dropping this item from presentation ' 'order'.format(order=verse_def)) if not self.finish(): self.log_error(file.name)
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, context=ServiceItemContext.Service): """ Generate the slide data. Needs to be implemented by the plugin. :param service_item: The service item to be built on :param item: The Song item to be used :param xml_version: The xml version (not used) :param remote: Triggered from remote :param context: Why is it being generated """ log.debug('generate_slide_data: %s, %s, %s' % (service_item, item, self.remote_song)) item_id = self._get_id_of_item_to_generate(item, self.remote_song) service_item.add_capability(ItemCapabilities.CanEdit) service_item.add_capability(ItemCapabilities.CanPreview) service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.OnLoadUpdate) service_item.add_capability(ItemCapabilities.AddIfNewItem) service_item.add_capability(ItemCapabilities.CanSoftBreak) song = self.plugin.manager.get_object(Song, item_id) service_item.theme = song.theme_name service_item.edit_id = item_id verse_list = SongXML().get_verses(song.lyrics) # no verse list or only 1 space (in error) verse_tags_translated = False if VerseType.from_translated_string(str(verse_list[0][0]['type'])) is not None: verse_tags_translated = True if not song.verse_order.strip(): for verse in verse_list: # We cannot use from_loose_input() here, because database is supposed to contain English lowercase # singlechar tags. verse_tag = verse[0]['type'] verse_index = None if len(verse_tag) > 1: verse_index = VerseType.from_translated_string(verse_tag) if verse_index is None: verse_index = VerseType.from_string(verse_tag, None) if verse_index is None: verse_index = VerseType.from_tag(verse_tag) verse_tag = VerseType.translated_tags[verse_index].upper() verse_def = '%s%s' % (verse_tag, verse[0]['label']) service_item.add_from_text(str(verse[1]), verse_def) else: # Loop through the verse list and expand the song accordingly. for order in song.verse_order.lower().split(): if not order: break for verse in verse_list: if verse[0]['type'][0].lower() == \ order[0] and (verse[0]['label'].lower() == order[1:] or not order[1:]): if verse_tags_translated: verse_index = VerseType.from_translated_tag(verse[0]['type']) else: verse_index = VerseType.from_tag(verse[0]['type']) verse_tag = VerseType.translated_tags[verse_index] verse_def = '%s%s' % (verse_tag, verse[0]['label']) service_item.add_from_text(verse[1], verse_def) service_item.title = song.title author_list = self.generate_footer(service_item, song) service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)} service_item.set_extra_data_dict(self.open_lyrics.song_to_line_dict(song)) service_item.xml_version = self.open_lyrics.song_to_xml(song) # Add the audio file to the service item. if song.media_files: service_item.add_capability(ItemCapabilities.HasBackgroundAudio) service_item.background_audio = [m.file_name for m in song.media_files] return True