diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 5a030e841..698216365 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -35,11 +35,11 @@ class HideMode(object): ``Blank`` This mode is used to hide all output, specifically by covering the display with a black screen. - + ``Theme`` This mode is used to hide all output, but covers the display with the current theme background, as opposed to black. - + ``Desktop`` This mode hides all output by minimising the display, leaving the user's desktop showing. diff --git a/openlp/plugins/bibles/forms/bibleimportwizard.py b/openlp/plugins/bibles/forms/bibleimportwizard.py index d22103fcb..c6a6775a5 100644 --- a/openlp/plugins/bibles/forms/bibleimportwizard.py +++ b/openlp/plugins/bibles/forms/bibleimportwizard.py @@ -234,7 +234,7 @@ class Ui_BibleImportWizard(object): QtGui.QSizePolicy.Minimum) self.openlp1Layout.setItem(1, QtGui.QFormLayout.LabelRole, self.openlp1Spacer) - self.selectStack.addWidget(self.openlp1Widget) + self.selectStack.addWidget(self.openlp1Widget) self.selectPageLayout.addLayout(self.selectStack) bibleImportWizard.addPage(self.selectPage) # License Page diff --git a/openlp/plugins/songs/forms/authorsdialog.py b/openlp/plugins/songs/forms/authorsdialog.py index 28083ae05..6f1c7f2a4 100644 --- a/openlp/plugins/songs/forms/authorsdialog.py +++ b/openlp/plugins/songs/forms/authorsdialog.py @@ -54,7 +54,7 @@ class Ui_AuthorsDialog(object): self.displayEdit.setObjectName(u'displayEdit') self.displayLabel.setBuddy(self.displayEdit) self.authorLayout.addRow(self.displayLabel, self.displayEdit) - self.dialogLayout.addLayout(self.authorLayout) + self.dialogLayout.addLayout(self.authorLayout) self.buttonBox = QtGui.QDialogButtonBox(authorsDialog) self.buttonBox.setStandardButtons( QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 202cc43fe..86249f024 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -31,7 +31,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import Receiver, translate from openlp.plugins.songs.forms import EditVerseForm -from openlp.plugins.songs.lib import SongXMLBuilder, SongXMLParser, VerseType +from openlp.plugins.songs.lib import SongXML, VerseType from openlp.plugins.songs.lib.db import Book, Song, Author, Topic from editsongdialog import Ui_EditSongDialog @@ -263,8 +263,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): if isinstance(self.song.lyrics, buffer): self.song.lyrics = unicode(self.song.lyrics) if self.song.lyrics.startswith(u' - + + + +The XML of `OpenLyrics `_ songs is of the format:: + + + + + Amazing Grace + + + + + + Amazing grace how sweet the sound + + + + """ import logging import re from lxml import etree, objectify + +from openlp.core.lib import translate from openlp.plugins.songs.lib import VerseType -from openlp.plugins.songs.lib.db import Author, Song +from openlp.plugins.songs.lib.db import Author, Book, Song, Topic log = logging.getLogger(__name__) -class SongXMLBuilder(object): +class SongXML(object): """ - This class builds the XML used to describe songs. + This class builds and parses the XML used to describe songs. """ - log.info(u'SongXMLBuilder Loaded') + log.info(u'SongXML Loaded') - def __init__(self, song_language=None): + def __init__(self): """ - Set up the song builder. - - ``song_language`` - The language used in this song + Set up the default variables. """ - lang = u'en' - if song_language: - lang = song_language self.song_xml = objectify.fromstring(u'') - self.lyrics = etree.SubElement(self.song_xml, u'lyrics', language=lang) + self.lyrics = etree.SubElement(self.song_xml, u'lyrics') def add_verse_to_lyrics(self, type, number, content): """ - Add a verse to the ```` tag. + Add a verse to the ** tag. ``type`` - A string denoting the type of verse. Possible values are "Chorus", - "Verse", "Bridge", and "Custom". + A string denoting the type of verse. Possible values are "V", + "C", "B", "P", "I", "E" and "O". ``number`` An integer denoting the number of the item, for example: verse 1. @@ -80,18 +98,11 @@ class SongXMLBuilder(object): ``content`` The actual text of the verse to be stored. """ - verse = etree.Element(u'verse', type = unicode(type), - label = unicode(number)) + verse = etree.Element(u'verse', type=unicode(type), + label=unicode(number)) verse.text = etree.CDATA(content) self.lyrics.append(verse) - def dump_xml(self): - """ - Debugging aid to dump XML so that we can see what we have. - """ - return etree.tostring(self.song_xml, encoding=u'UTF-8', - xml_declaration=True, pretty_print=True) - def extract_xml(self): """ Extract our newly created XML song. @@ -99,16 +110,10 @@ class SongXMLBuilder(object): return etree.tostring(self.song_xml, encoding=u'UTF-8', xml_declaration=True) - -class SongXMLParser(object): - """ - A class to read in and parse a song's XML. - """ - log.info(u'SongXMLParser Loaded') - - def __init__(self, xml): + def get_verses(self, xml): """ - Set up our song XML parser. + Iterates through the verses in the XML and returns a list of verses + and their attributes. ``xml`` The XML of the song to be parsed. @@ -120,12 +125,6 @@ class SongXMLParser(object): self.song_xml = objectify.fromstring(xml) except etree.XMLSyntaxError: log.exception(u'Invalid xml %s', xml) - - def get_verses(self): - """ - Iterates through the verses in the XML and returns a list of verses - and their attributes. - """ xml_iter = self.song_xml.getiterator() verse_list = [] for element in xml_iter: @@ -142,137 +141,109 @@ class SongXMLParser(object): return etree.dump(self.song_xml) -class LyricsXML(object): +class OpenLyrics(object): """ - This class represents the XML in the ``lyrics`` field of a song. - """ - def __init__(self, song=None): - if song: - if song.lyrics.startswith(u'* + OpenLP does not support the attribute *type* and *lang*. - def extract(self, text): - """ - If the ``lyrics`` field in the database is not XML, this method is - called and used to construct the verse structure similar to the output - of the ``parse`` function. + ** + This property is not supported. - ``text`` - The text to pull verses out of. - """ - text = text.replace('\r\n', '\n') - verses = text.split('\n\n') - self.languages = [{u'language': u'en', u'verses': []}] - counter = 0 - for verse in verses: - counter = counter + 1 - self.languages[0][u'verses'].append({ - u'type': u'verse', - u'label': unicode(counter), - u'text': verse - }) - return True + ** + The ** property is fully supported. But comments in lyrics + are not supported. - def add_verse(self, type, label, text): - """ - Add a verse to the list of verses. + ** + This property is fully supported. - ``type`` - The type of list, one of "verse", "chorus", "bridge", "pre-chorus", - "intro", "outtro". + ** + This property is not supported. - ``label`` - The number associated with this verse, like 1 or 2. + ** + This property is not supported. - ``text`` - The text of the verse. - """ - self.verses.append({ - u'type': type, - u'label': label, - u'text': text - }) + ** + This property is not supported. - def export(self): - """ - Build up the XML for the verse structure. - """ - lyrics_output = u'' - for language in self.languages: - verse_output = u'' - for verse in language[u'verses']: - verse_output = verse_output + \ - u'' % \ - (verse[u'type'], verse[u'label'], verse[u'text']) - lyrics_output = lyrics_output + \ - u'%s' % \ - (language[u'language'], verse_output) - song_output = u'' + \ - u'%s' % lyrics_output - return song_output + ** + The attribute *part* is not supported. + ** + This property is not supported. -class OpenLyricsParser(object): - """ - This class represents the converter for Song to/from OpenLyrics XML. + ** + As OpenLP does only support one songbook, we cannot consider more than + one songbook. + + ** + This property is not supported. + + ** + Topics, as they are called in OpenLP, are fully supported, whereby only + the topic text (e. g. Grace) is considered, but neither the *id* nor + *lang*. + + ** + This property is not supported. + + ** + This property is not supported. + + ** + The attribute *translit* and *lang* are not supported. + + ** + OpenLP supports this property. """ def __init__(self, manager): self.manager = manager - def song_to_xml(self, song): + def song_to_xml(self, song, pretty_print=False): """ - Convert the song to OpenLyrics Format + Convert the song to OpenLyrics Format. """ - song_xml_parser = SongXMLParser(song.lyrics) - verse_list = song_xml_parser.get_verses() + sxml = SongXML() + verse_list = sxml.get_verses(song.lyrics) song_xml = objectify.fromstring( u'') properties = etree.SubElement(song_xml, u'properties') titles = etree.SubElement(properties, u'titles') - self._add_text_to_element(u'title', titles, song.title) + self._add_text_to_element(u'title', titles, song.title.strip()) if song.alternate_title: - self._add_text_to_element(u'title', titles, song.alternate_title) - if song.theme_name: - themes = etree.SubElement(properties, u'themes') - self._add_text_to_element(u'theme', themes, song.theme_name) - self._add_text_to_element(u'copyright', properties, song.copyright) - self._add_text_to_element(u'verseOrder', properties, song.verse_order) + self._add_text_to_element( + u'title', titles, song.alternate_title.strip()) + if song.comments: + comments = etree.SubElement(properties, u'comments') + self._add_text_to_element(u'comment', comments, song.comments) + if song.copyright: + self._add_text_to_element(u'copyright', properties, song.copyright) + if song.verse_order: + self._add_text_to_element( + u'verseOrder', properties, song.verse_order) if song.ccli_number: self._add_text_to_element(u'ccliNo', properties, song.ccli_number) - authors = etree.SubElement(properties, u'authors') - for author in song.authors: - self._add_text_to_element(u'author', authors, author.display_name) + if song.authors: + authors = etree.SubElement(properties, u'authors') + for author in song.authors: + self._add_text_to_element( + u'author', authors, author.display_name) + book = self.manager.get_object_filtered( + Book, Book.id == song.song_book_id) + if book is not None: + book = book.name + songbooks = etree.SubElement(properties, u'songbooks') + element = self._add_text_to_element( + u'songbook', songbooks, None, book) + element.set(u'entry', song.song_number) + if song.topics: + themes = etree.SubElement(properties, u'themes') + for topic in song.topics: + self._add_text_to_element(u'theme', themes, topic.name) lyrics = etree.SubElement(song_xml, u'lyrics') for verse in verse_list: verse_tag = u'%s%s' % ( @@ -282,78 +253,36 @@ class OpenLyricsParser(object): element = self._add_text_to_element(u'lines', element) for line in unicode(verse[1]).split(u'\n'): self._add_text_to_element(u'line', element, line) - return self._extract_xml(song_xml) + return self._extract_xml(song_xml, pretty_print) def xml_to_song(self, xml): """ - Create a Song from OpenLyrics format xml + Create and save a song from OpenLyrics format xml to the database. Since + we also export XML from external sources (e. g. OpenLyrics import), we + cannot ensure, that it completely conforms to the OpenLyrics standard. + + ``xml`` + The XML to parse (unicode). """ - # No xml get out of here + # No xml get out of here. if not xml: return 0 song = Song() if xml[:5] == u'').sub(u'', xml) song_xml = objectify.fromstring(xml) properties = song_xml.properties - song.copyright = unicode(properties.copyright.text) - if song.copyright == u'None': - song.copyright = u'' - song.verse_order = unicode(properties.verseOrder.text) - if song.verse_order == u'None': - song.verse_order = u'' - song.topics = [] - song.book = None - theme_name = None - try: - song.ccli_number = unicode(properties.ccliNo.text) - except: - song.ccli_number = u'' - try: - theme_name = unicode(properties.themes.theme) - except: - pass - if theme_name: - song.theme_name = theme_name - else: - song.theme_name = u'' - # Process Titles - for title in properties.titles.title: - if not song.title: - song.title = unicode(title.text) - song.search_title = unicode(song.title) - song.alternate_title = u'' - else: - song.alternate_title = unicode(title.text) - song.search_title += u'@' + song.alternate_title - song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'', - unicode(song.search_title)).lower() - # Process Lyrics - sxml = SongXMLBuilder() - search_text = u'' - for lyrics in song_xml.lyrics: - for verse in song_xml.lyrics.verse: - text = u'' - for line in verse.lines.line: - line = unicode(line) - if not text: - text = line - else: - text += u'\n' + line - type = VerseType.expand_string(verse.attrib[u'name'][0]) - sxml.add_verse_to_lyrics(type, verse.attrib[u'name'][1], text) - search_text = search_text + text - song.search_lyrics = search_text.lower() - song.lyrics = unicode(sxml.extract_xml(), u'utf-8') - song.comments = u'' - song.song_number = u'' - # Process Authors - try: - for author in properties.authors.author: - self._process_author(author.text, song) - except: - # No Author in XML so ignore - pass + self._process_copyright(properties, song) + self._process_cclinumber(properties, song) + self._process_titles(properties, song) + # The verse order is processed with the lyrics! + self._process_lyrics(properties, song_xml.lyrics, song) + self._process_comments(properties, song) + self._process_authors(properties, song) + self._process_songbooks(properties, song) + self._process_topics(properties, song) self.manager.save_object(song) return song.id @@ -367,33 +296,258 @@ class OpenLyricsParser(object): parent.append(element) return element + def _extract_xml(self, xml, pretty_print): + """ + Extract our newly created XML song. + """ + return etree.tostring(xml, encoding=u'UTF-8', + xml_declaration=True, pretty_print=pretty_print) + + def _get(self, element, attribute): + """ + This returns the element's attribute as unicode string. + + ``element`` + The element. + + ``attribute`` + The element's attribute (unicode). + """ + if element.get(attribute) is not None: + return unicode(element.get(attribute)) + return u'' + + def _text(self, element): + """ + This returns the text of an element as unicode string. + + ``element`` + The element. + """ + if element.text is not None: + return unicode(element.text) + return u'' + + def _process_authors(self, properties, song): + """ + Adds the authors specified in the XML to the song. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + authors = [] + try: + for author in properties.authors.author: + display_name = self._text(author) + if display_name: + authors.append(display_name) + except AttributeError: + pass + if not authors: + # Add "Author unknown" (can be translated). + authors.append((unicode(translate('SongsPlugin.XML', + 'Author unknown')))) + for display_name in authors: + author = self.manager.get_object_filtered(Author, + Author.display_name == display_name) + if author is None: + # We need to create a new author, as the author does not exist. + author = Author.populate(display_name=display_name, + last_name=display_name.split(u' ')[-1], + first_name=u' '.join(display_name.split(u' ')[:-1])) + self.manager.save_object(author) + song.authors.append(author) + + def _process_cclinumber(self, properties, song): + """ + Adds the CCLI number to the song. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + try: + song.ccli_number = self._text(properties.ccliNo) + except AttributeError: + song.ccli_number = u'' + + def _process_comments(self, properties, song): + """ + Joins the comments specified in the XML and add it to the song. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + try: + comments_list = [] + for comment in properties.comments.comment: + commenttext = self._text(comment) + if commenttext: + comments_list.append(commenttext) + song.comments = u'\n'.join(comments_list) + except AttributeError: + song.comments = u'' + + def _process_copyright(self, properties, song): + """ + Adds the copyright to the song. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + try: + song.copyright = self._text(properties.copyright) + except AttributeError: + song.copyright = u'' + + def _process_lyrics(self, properties, lyrics, song): + """ + Processes the verses and search_lyrics for the song. + + ``properties`` + The properties object (lxml.objectify.ObjectifiedElement). + + ``lyrics`` + The lyrics object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + sxml = SongXML() + search_text = u'' + temp_verse_order = [] + for verse in lyrics.verse: + text = u'' + for lines in verse.lines: + if text: + text += u'\n' + text += u'\n'.join([unicode(line) for line in lines.line]) + verse_name = self._get(verse, u'name') + verse_type = unicode(VerseType.expand_string(verse_name[0]))[0] + verse_number = re.compile(u'[a-zA-Z]*').sub(u'', verse_name) + verse_part = re.compile(u'[0-9]*').sub(u'', verse_name[1:]) + # OpenLyrics allows e. g. "c", but we need "c1". + if not verse_number: + verse_number = u'1' + temp_verse_order.append((verse_type, verse_number, verse_part)) + sxml.add_verse_to_lyrics(verse_type, verse_number, text) + search_text = search_text + text + song.search_lyrics = search_text.lower() + song.lyrics = unicode(sxml.extract_xml(), u'utf-8') + # Process verse order + try: + song.verse_order = self._text(properties.verseOrder) + except AttributeError: + # We have to process the temp_verse_order, as the verseOrder + # property is not present. + previous_type = u'' + previous_number = u'' + previous_part = u'' + verse_order = [] + # Currently we do not support different "parts"! + for name in temp_verse_order: + if name[0] == previous_type: + if name[1] != previous_number: + verse_order.append(u''.join((name[0], name[1]))) + else: + verse_order.append(u''.join((name[0], name[1]))) + previous_type = name[0] + previous_number = name[1] + previous_part = name[2] + song.verse_order = u' '.join(verse_order) + + def _process_songbooks(self, properties, song): + """ + Adds the song book and song number specified in the XML to the song. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + song.song_book_id = 0 + song.song_number = u'' + try: + for songbook in properties.songbooks.songbook: + bookname = self._get(songbook, u'name') + if bookname: + book = self.manager.get_object_filtered(Book, + Book.name == bookname) + if book is None: + # We need to create a book, because it does not exist. + book = Book.populate(name=bookname, publisher=u'') + self.manager.save_object(book) + song.song_book_id = book.id + try: + if self._get(songbook, u'entry'): + song.song_number = self._get(songbook, u'entry') + except AttributeError: + pass + # We does only support one song book, so take the first one. + break + except AttributeError: + pass + + def _process_titles(self, properties, song): + """ + Processes the titles specified in the song's XML. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + for title in properties.titles.title: + if not song.title: + song.title = self._text(title) + song.search_title = unicode(song.title) + song.alternate_title = u'' + else: + song.alternate_title = self._text(title) + song.search_title += u'@' + song.alternate_title + song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'', + unicode(song.search_title)).lower() + + def _process_topics(self, properties, song): + """ + Adds the topics to the song. + + ``properties`` + The property object (lxml.objectify.ObjectifiedElement). + + ``song`` + The song object. + """ + try: + for topictext in properties.themes.theme: + topictext = self._text(topictext) + if topictext: + topic = self.manager.get_object_filtered(Topic, + Topic.name == topictext) + if topic is None: + # We need to create a topic, because it does not exist. + topic = Topic.populate(name=topictext) + self.manager.save_object(topic) + song.topics.append(topic) + except AttributeError: + pass + def _dump_xml(self, xml): """ Debugging aid to dump XML so that we can see what we have. """ return etree.tostring(xml, encoding=u'UTF-8', xml_declaration=True, pretty_print=True) - - def _extract_xml(self, xml): - """ - Extract our newly created XML song. - """ - return etree.tostring(xml, encoding=u'UTF-8', - xml_declaration=True) - - def _process_author(self, name, song): - """ - Find or create an Author from display_name. - """ - name = unicode(name) - author = self.manager.get_object_filtered(Author, - Author.display_name == name) - if author: - # should only be one! so take the first - song.authors.append(author) - else: - # Need a new author - new_author = Author.populate(first_name=name.rsplit(u' ', 1)[0], - last_name=name.rsplit(u' ', 1)[1], display_name=name) - self.manager.save_object(new_author) - song.authors.append(new_author) \ No newline at end of file diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 545497acb..17e609fd4 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -31,7 +31,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import Plugin, StringContent, build_icon, translate from openlp.core.lib.db import Manager -from openlp.plugins.songs.lib import SongMediaItem, SongsTab, SongXMLParser +from openlp.plugins.songs.lib import SongMediaItem, SongsTab, SongXML from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.importer import SongFormat @@ -153,7 +153,7 @@ class SongsPlugin(Plugin): song.search_title = self.whitespace.sub(u' ', song.title.lower() + \ u' ' + song.alternate_title.lower()) lyrics = u'' - verses = SongXMLParser(song.lyrics).get_verses() + verses = SongXML().get_verses(song.lyrics) for verse in verses: lyrics = lyrics + self.whitespace.sub(u' ', verse[1]) + u' ' song.search_lyrics = lyrics.lower()