diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 5ec7f45e1..5776dd21a 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -73,12 +73,12 @@ class SongImportForm(QtGui.QWizard, Ui_SongImportWizard): QtCore.QObject.connect(self.openLP1BrowseButton, QtCore.SIGNAL(u'clicked()'), self.onOpenLP1BrowseButtonClicked) - #QtCore.QObject.connect(self.openLyricsAddButton, - # QtCore.SIGNAL(u'clicked()'), - # self.onOpenLyricsAddButtonClicked) - #QtCore.QObject.connect(self.openLyricsRemoveButton, - # QtCore.SIGNAL(u'clicked()'), - # self.onOpenLyricsRemoveButtonClicked) + QtCore.QObject.connect(self.openLyricsAddButton, + QtCore.SIGNAL(u'clicked()'), + self.onOpenLyricsAddButtonClicked) + QtCore.QObject.connect(self.openLyricsRemoveButton, + QtCore.SIGNAL(u'clicked()'), + self.onOpenLyricsRemoveButtonClicked) QtCore.QObject.connect(self.openSongAddButton, QtCore.SIGNAL(u'clicked()'), self.onOpenSongAddButtonClicked) @@ -167,16 +167,15 @@ class SongImportForm(QtGui.QWizard, Ui_SongImportWizard): self.openLP1BrowseButton.setFocus() return False elif source_format == SongFormat.OpenLyrics: -# if self.openLyricsFileListWidget.count() == 0: -# QtGui.QMessageBox.critical(self, -# translate('SongsPlugin.ImportWizardForm', -# 'No OpenLyrics Files Selected'), -# translate('SongsPlugin.ImportWizardForm', -# 'You need to add at least one OpenLyrics ' -# 'song file to import from.')) -# self.openLyricsAddButton.setFocus() -# return False - return False + if self.openLyricsFileListWidget.count() == 0: + QtGui.QMessageBox.critical(self, + translate('SongsPlugin.ImportWizardForm', + 'No OpenLyrics Files Selected'), + translate('SongsPlugin.ImportWizardForm', + 'You need to add at least one OpenLyrics ' + 'song file to import from.')) + self.openLyricsAddButton.setFocus() + return False elif source_format == SongFormat.OpenSong: if self.openSongFileListWidget.count() == 0: QtGui.QMessageBox.critical(self, @@ -337,15 +336,15 @@ class SongImportForm(QtGui.QWizard, Ui_SongImportWizard): 'openlp.org v1.x Databases') ) - #def onOpenLyricsAddButtonClicked(self): - # self.getFiles( - # translate('SongsPlugin.ImportWizardForm', - # 'Select OpenLyrics Files'), - # self.openLyricsFileListWidget - # ) + def onOpenLyricsAddButtonClicked(self): + self.getFiles( + translate('SongsPlugin.ImportWizardForm', + 'Select OpenLyrics Files'), + self.openLyricsFileListWidget + ) - #def onOpenLyricsRemoveButtonClicked(self): - # self.removeSelectedItems(self.openLyricsFileListWidget) + def onOpenLyricsRemoveButtonClicked(self): + self.removeSelectedItems(self.openLyricsFileListWidget) def onOpenSongAddButtonClicked(self): self.getFiles( @@ -435,7 +434,7 @@ class SongImportForm(QtGui.QWizard, Ui_SongImportWizard): self.formatComboBox.setCurrentIndex(0) self.openLP2FilenameEdit.setText(u'') self.openLP1FilenameEdit.setText(u'') - #self.openLyricsFileListWidget.clear() + self.openLyricsFileListWidget.clear() self.openSongFileListWidget.clear() self.wordsOfWorshipFileListWidget.clear() self.ccliFileListWidget.clear() diff --git a/openlp/plugins/songs/forms/songimportwizard.py b/openlp/plugins/songs/forms/songimportwizard.py index 85fbb07fe..6eccff9b4 100644 --- a/openlp/plugins/songs/forms/songimportwizard.py +++ b/openlp/plugins/songs/forms/songimportwizard.py @@ -81,9 +81,6 @@ class Ui_SongImportWizard(object): self.addSingleFileSelectItem(u'openLP1', None, True) # OpenLyrics self.addMultiFileSelectItem(u'openLyrics', u'OpenLyrics', True) - # set OpenLyrics to disabled by default - self.openLyricsDisabledWidget.setVisible(True) - self.openLyricsImportWidget.setVisible(False) # Open Song self.addMultiFileSelectItem(u'openSong', u'OpenSong') # Words of Worship @@ -177,10 +174,10 @@ class Ui_SongImportWizard(object): 'importer has been disabled due to a missing Python module. If ' 'you want to use this importer, you will need to install the ' '"python-sqlite" module.')) - #self.openLyricsAddButton.setText( - # translate('SongsPlugin.ImportWizardForm', 'Add Files...')) - #self.openLyricsRemoveButton.setText( - # translate('SongsPlugin.ImportWizardForm', 'Remove File(s)')) + self.openLyricsAddButton.setText( + translate('SongsPlugin.ImportWizardForm', 'Add Files...')) + self.openLyricsRemoveButton.setText( + translate('SongsPlugin.ImportWizardForm', 'Remove File(s)')) self.openLyricsDisabledLabel.setText( translate('SongsPlugin.ImportWizardForm', 'The OpenLyrics ' 'importer has not yet been developed, but as you can see, we are ' diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index b82e14c12..128d80138 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -26,6 +26,7 @@ from opensongimport import OpenSongImport from olpimport import OpenLPSongImport +from openlyricsimport import OpenLyricsImport from wowimport import WowImport from cclifileimport import CCLIFileImport from ewimport import EasyWorshipSongImport @@ -77,8 +78,10 @@ class SongFormat(object): """ if format == SongFormat.OpenLP2: return OpenLPSongImport - if format == SongFormat.OpenLP1: + elif format == SongFormat.OpenLP1: return OpenLP1SongImport + elif format == SongFormat.OpenLyrics: + return OpenLyricsImport elif format == SongFormat.OpenSong: return OpenSongImport elif format == SongFormat.SongsOfFellowship: @@ -93,7 +96,6 @@ class SongFormat(object): return EasyWorshipSongImport elif format == SongFormat.SongBeamer: return SongBeamerImport -# else: return None @staticmethod diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index cd305877c..93d648146 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -314,15 +314,14 @@ class SongMediaItem(MediaManagerItem): translate('SongsPlugin.MediaItem', 'You must select an item to delete.')): items = self.listView.selectedIndexes() - ans = QtGui.QMessageBox.question(self, + if QtGui.QMessageBox.question(self, translate('SongsPlugin.MediaItem', 'Delete Song(s)?'), translate('SongsPlugin.MediaItem', 'Are you sure you want to delete the %n selected song(s)?', '', QtCore.QCoreApplication.CodecForTr, len(items)), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok| - QtGui.QMessageBox.Cancel), - QtGui.QMessageBox.Ok) - if ans == QtGui.QMessageBox.Cancel: + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok | + QtGui.QMessageBox.Cancel), + QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel: return for item in items: item_id = (item.data(QtCore.Qt.UserRole)).toInt()[0] @@ -396,8 +395,8 @@ class SongMediaItem(MediaManagerItem): service_item.audit = [ song.title, author_audit, song.copyright, unicode(song.ccli_number) ] - service_item.data_string = {u'title':song.search_title, - u'authors':author_list} + service_item.data_string = {u'title': song.search_title, + u'authors': author_list} service_item.xml_version = self.openLyrics.song_to_xml(song) return True @@ -409,7 +408,7 @@ class SongMediaItem(MediaManagerItem): if item.data_string: search_results = self.parent.manager.get_all_objects(Song, Song.search_title == - item.data_string[u'title'].split(u'@')[0].lower() , + item.data_string[u'title'].split(u'@')[0].lower(), Song.search_title.asc()) author_list = item.data_string[u'authors'].split(u', ') # The service item always has an author (at least it has u'' as @@ -418,7 +417,6 @@ class SongMediaItem(MediaManagerItem): if u'' in author_list: author_list.remove(u'') editId = 0 - uuid = item._uuid add_song = True if search_results: for song in search_results: @@ -445,7 +443,7 @@ class SongMediaItem(MediaManagerItem): # Update service with correct song id. if editId != 0: Receiver.send_message(u'service_item_update', - u'%s:%s' %(editId, uuid)) + u'%s:%s' % (editId, item._uuid)) def collateSongTitles(self, song_1, song_2): """ diff --git a/openlp/plugins/songs/lib/openlyricsimport.py b/openlp/plugins/songs/lib/openlyricsimport.py new file mode 100644 index 000000000..5346b5803 --- /dev/null +++ b/openlp/plugins/songs/lib/openlyricsimport.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2011 Raoul Snyman # +# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael # +# Gorven, Scott Guerrieri, Meinert Jordan, Andreas Preikschat, Christian # +# Richter, Philip Ridout, Maikel Stuivenberg, Martin Thompson, Jon Tibble, # +# Carsten Tinggaard, Frode Woldsund # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`openlyricsimport` module provides the functionality for importing +songs which are saved as OpenLyrics files. +""" + +import os + +from openlp.core.lib import translate +from openlp.plugins.songs.lib.songimport import SongImport +from openlp.plugins.songs.lib import OpenLyricsParser + +class OpenLyricsImport(SongImport): + """ + This provides the Openlyrics import. + """ + def __init__(self, master_manager, **kwargs): + """ + Initialise the import. + """ + SongImport.__init__(self, master_manager) + self.master_manager = master_manager + self.openLyricsParser = OpenLyricsParser(master_manager) + if kwargs.has_key(u'filename'): + self.import_source = kwargs[u'filename'] + if kwargs.has_key(u'filenames'): + self.import_source = kwargs[u'filenames'] + + def do_import(self): + """ + Imports the songs. + """ + self.import_wizard.importProgressBar.setMaximum(len(self.import_source)) + for file_path in self.import_source: + if self.stop_import_flag: + return False + file = open(file_path) + lines = file.readlines() + file.close() + lines = [line.strip() for line in lines] + xml = u''.join(lines) + self.import_wizard.incrementProgressBar(unicode(translate( + 'SongsPlugin.OpenLyricsImport', 'Importing %s...')) % + os.path.basename(file_path)) + if self.openLyricsParser.xml_to_song(xml) == 0: + # Importing this song failed! For now we stop import. + return False + return True diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py index f00711bb6..326f97537 100644 --- a/openlp/plugins/songs/lib/xml.py +++ b/openlp/plugins/songs/lib/xml.py @@ -42,8 +42,10 @@ 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__) @@ -80,8 +82,8 @@ 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) @@ -194,9 +196,7 @@ class LyricsXML(object): 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 + for counter, verse in enumerate(verses): self.languages[0][u'verses'].append({ u'type': u'verse', u'label': unicode(counter), @@ -245,14 +245,16 @@ class LyricsXML(object): class OpenLyricsParser(object): """ - This class represents the converter for Song to/from OpenLyrics XML. + This class represents the converter for Song to/from + `OpenLyrics `_ XML. """ + # TODO: complete OpenLyrics standard implementation as fare as possible! def __init__(self, manager): self.manager = manager def song_to_xml(self, song): """ - 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() @@ -263,16 +265,33 @@ class OpenLyricsParser(object): self._add_text_to_element(u'title', titles, song.title) 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) + 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' % ( @@ -286,77 +305,142 @@ class OpenLyricsParser(object): 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. + That means, that we for example have to remove chords. """ - # 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': + # Process Copyright + try: + song.copyright = self._text(properties.copyright) + except AttributeError: 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 + # Process CCLI number try: - song.ccli_number = unicode(properties.ccliNo.text) - except: + song.ccli_number = self._text(properties.ccliNo) + except AttributeError: 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.title = self._text(title) song.search_title = unicode(song.title) song.alternate_title = u'' else: - song.alternate_title = unicode(title.text) + 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() # Process Lyrics sxml = SongXMLBuilder() search_text = u'' + song.verse_order = u'' for lyrics in song_xml.lyrics: - for verse in song_xml.lyrics.verse: + for verse in lyrics.verse: text = u'' - for line in verse.lines.line: - line = unicode(line) - if not text: - text = line - else: - text += u'\n' + line + for line in verse.lines: + for line in line.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) + # TODO: test this verse_order thing! + song.verse_order += u'%s%s ' % (type[0], + verse.attrib[u'name'][1]) search_text = search_text + text song.search_lyrics = search_text.lower() song.lyrics = unicode(sxml.extract_xml(), u'utf-8') + song.verse_order = song.verse_order.strip() + # Process verse order + try: + song.verse_order = self._text(properties.verseOrder) + except AttributeError: + # Do not worry, as the verse order has cautionary already been + # saved while creating the verses. + pass + if song.verse_order == u'None': + song.verse_order = u'' + # Process Comments song.comments = u'' - song.song_number = u'' + try: + for comment in properties.comments.comment: + if not song.comments: + song.comments = self._text(comment) + else: + song.comments += u'\n' + self._text(comment) + except AttributeError: + pass # Process Authors try: for author in properties.authors.author: - self._process_author(author.text, song) - except: - # No Author in XML so ignore + self._process_author(self._text(author), song) + except AttributeError: pass + if not song.authors: + # Add "Author unknown" (can be translated) + self._process_author(unicode(translate('SongsPlugin.XML', + 'Author unknown')), song) + # Process Song Book and Song Number + song.song_book_id = 0 + song.song_number = u'' + try: + for songbook in properties.songbooks.songbook: + self._process_songbook(self._get(songbook, u'name'), song) + if songbook.get(u'entry'): + song.song_number = self._get(songbook, u'entry') + # OpenLp does only support one song book, so take the first one. + break + except AttributeError: + pass + # Process Topcis + try: + for topic in properties.themes.theme: + self._process_topic(self._text(topic), song) + except AttributeError: + pass + # Properties not yet supported. + song.theme_name = u'' self.manager.save_object(song) return song.id + def _get(self, element, attribute): + """ + This takes care of empty attributes. It returns the element's attribute. + + ``element`` + The element. + + ``attribute`` + The element's attribute (unicode). + """ + if element.get(attribute) is not None: + return element.get(attribute) + return u'' + + def _text(self, element): + """ + This takes care of empty texts. It returns the element's text. + + ``element`` + The element. + """ + if element.text is not None: + return unicode(element.text) + return u'' + def _add_text_to_element(self, tag, parent, text=None, label=None): if label: element = etree.Element(tag, name=unicode(label)) @@ -383,17 +467,65 @@ class OpenLyricsParser(object): def _process_author(self, name, song): """ - Find or create an Author from display_name. + Finds an existing Author or creates a new Author and adds it to the song + object. + + ``name`` + The display_name of the song (unicode). + + ``song`` + The song the object. """ - name = unicode(name) + if not name: + # Wrong use of XML here, as no text has been supplied. + return 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 + if author is None: + # We need to create a new author, as the author does not exist. + author = Author.populate(first_name=name.rsplit(u' ', 1)[0], + last_name=name.rsplit(u' ', 1)[1], display_name=name) + self.manager.save_object(author) + song.authors.append(author) + + def _process_topic(self, topictext, song): + """ + Finds an existing topic or creates a new topic and adds it to the song + object. + + ``topictext`` + The topictext of the topic (unicode). + + ``song`` + The song object. + """ + if not topictext: + # Wrong use of XML here, as no text has been supplied. + return + topic = self.manager.get_object_filtered(Topic, Topic.name == topictext) + if topic is None: + # We need to create a new topic, as the topic does not exist. + topic = Topic.populate(name=topictext) + self.manager.save_object(topic) + song.topics.append(topic) + + def _process_songbook(self, bookname, song): + """ + Finds an existing book or creates a new book and adds it to the song + object. + + ``bookname`` + The name of the book (unicode). + + ``song`` + The song object. + """ + if not bookname: + # Wrong use of XML here, as no text has been supplied. + return + book = self.manager.get_object_filtered(Book, Book.name == bookname) + if book is None: + # We need to create a new book, as the book does not exist. + book = Book.populate(name=bookname, publisher=u'') + self.manager.save_object(book) + song.song_book_id = book.id