diff --git a/openlp/core/lib/formattingtags.py b/openlp/core/lib/formattingtags.py index bec2db63a..11fd898c3 100644 --- a/openlp/core/lib/formattingtags.py +++ b/openlp/core/lib/formattingtags.py @@ -46,13 +46,36 @@ class FormattingTags(object): """ Provide access to the html_expands list. """ - # Load user defined tags otherwise user defined tags are not present. return FormattingTags.html_expands @staticmethod - def reset_html_tags(): + def save_html_tags(): """ - Resets the html_expands list. + Saves all formatting tags except protected ones. + """ + tags = [] + for tag in FormattingTags.html_expands: + if not tag[u'protected'] and not tag.get(u'temporary'): + # Using dict ensures that copy is made and encoding of values + # a little later does not affect tags in the original list + tags.append(dict(tag)) + tag = tags[-1] + # Remove key 'temporary' from tags. + # It is not needed to be saved. + if u'temporary' in tag: + del tag[u'temporary'] + for element in tag: + if isinstance(tag[element], unicode): + tag[element] = tag[element].encode('utf8') + # Formatting Tags were also known as display tags. + QtCore.QSettings().setValue(u'displayTags/html_tags', + QtCore.QVariant(cPickle.dumps(tags) if tags else u'')) + + @staticmethod + def load_tags(): + """ + Load the Tags from store so can be used in the system or used to + update the display. """ temporary_tags = [tag for tag in FormattingTags.html_expands if tag.get(u'temporary')] @@ -140,38 +163,6 @@ class FormattingTags(object): FormattingTags.add_html_tags(base_tags) FormattingTags.add_html_tags(temporary_tags) - @staticmethod - def save_html_tags(): - """ - Saves all formatting tags except protected ones. - """ - tags = [] - for tag in FormattingTags.html_expands: - if not tag[u'protected'] and not tag.get(u'temporary'): - # Using dict ensures that copy is made and encoding of values - # a little later does not affect tags in the original list - tags.append(dict(tag)) - tag = tags[-1] - # Remove key 'temporary' from tags. - # It is not needed to be saved. - if u'temporary' in tag: - del tag[u'temporary'] - for element in tag: - if isinstance(tag[element], unicode): - tag[element] = tag[element].encode('utf8') - # Formatting Tags were also known as display tags. - QtCore.QSettings().setValue(u'displayTags/html_tags', - QtCore.QVariant(cPickle.dumps(tags) if tags else u'')) - - @staticmethod - def load_tags(): - """ - Load the Tags from store so can be used in the system or used to - update the display. If Cancel was selected this is needed to reset the - dsiplay to the correct version. - """ - # Initial Load of the Tags - FormattingTags.reset_html_tags() # Formatting Tags were also known as display tags. user_expands = QtCore.QSettings().value(u'displayTags/html_tags', QtCore.QVariant(u'')).toString() @@ -187,17 +178,13 @@ class FormattingTags(object): FormattingTags.add_html_tags(user_tags) @staticmethod - def add_html_tags(tags, save=False): + def add_html_tags(tags): """ Add a list of tags to the list. ``tags`` The list with tags to add. - ``save`` - Defaults to ``False``. If set to ``True`` the given ``tags`` are - saved to the config. - Each **tag** has to be a ``dict`` and should have the following keys: * desc @@ -225,8 +212,6 @@ class FormattingTags(object): displaying text containing the tag. It has to be a ``boolean``. """ FormattingTags.html_expands.extend(tags) - if save: - FormattingTags.save_html_tags() @staticmethod def remove_html_tag(tag_id): diff --git a/openlp/core/lib/imagemanager.py b/openlp/core/lib/imagemanager.py index 0139cd001..b32e36194 100644 --- a/openlp/core/lib/imagemanager.py +++ b/openlp/core/lib/imagemanager.py @@ -100,6 +100,7 @@ class Image(object): variables ``image`` and ``image_bytes`` to ``None`` and add the image object to the queue of images to process. """ + secondary_priority = 0 def __init__(self, name, path, source, background): self.name = name self.path = path @@ -108,25 +109,40 @@ class Image(object): self.priority = Priority.Normal self.source = source self.background = background + self.secondary_priority = Image.secondary_priority + Image.secondary_priority += 1 class PriorityQueue(Queue.PriorityQueue): """ Customised ``Queue.PriorityQueue``. + + Each item in the queue must be tuple with three values. The first value + is the :class:`Image`'s ``priority`` attribute, the second value + the :class:`Image`'s ``secondary_priority`` attribute. The last value the + :class:`Image` instance itself:: + + (image.priority, image.secondary_priority, image) + + Doing this, the :class:`Queue.PriorityQueue` will sort the images according + to their priorities, but also according to there number. However, the number + only has an impact on the result if there are more images with the same + priority. In such case the image which has been added earlier is privileged. """ def modify_priority(self, image, new_priority): """ Modifies the priority of the given ``image``. ``image`` - The image to remove. This should be an ``Image`` instance. + The image to remove. This should be an :class:`Image` instance. ``new_priority`` - The image's new priority. + The image's new priority. See the :class:`Priority` class for + priorities. """ self.remove(image) image.priority = new_priority - self.put((image.priority, image)) + self.put((image.priority, image.secondary_priority, image)) def remove(self, image): """ @@ -135,8 +151,8 @@ class PriorityQueue(Queue.PriorityQueue): ``image`` The image to remove. This should be an ``Image`` instance. """ - if (image.priority, image) in self.queue: - self.queue.remove((image.priority, image)) + if (image.priority, image.secondary_priority, image) in self.queue: + self.queue.remove((image.priority, image.secondary_priority, image)) class ImageManager(QtCore.QObject): @@ -261,7 +277,8 @@ class ImageManager(QtCore.QObject): if not name in self._cache: image = Image(name, path, source, background) self._cache[name] = image - self._conversion_queue.put((image.priority, image)) + self._conversion_queue.put( + (image.priority, image.secondary_priority, image)) else: log.debug(u'Image in cache %s:%s' % (name, path)) # We want only one thread. @@ -282,7 +299,7 @@ class ImageManager(QtCore.QObject): Actually does the work. """ log.debug(u'_process_cache') - image = self._conversion_queue.get()[1] + image = self._conversion_queue.get()[2] # Generate the QImage for the image. if image.image is None: image.image = resize_image(image.path, self.width, self.height, diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index 32ea0fb90..8694ca6b6 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -235,8 +235,8 @@ class Renderer(object): # the first two slides (and neglect the last for now). if len(slides) == 3: html_text = expand_tags(u'\n'.join(slides[:2])) - # We check both slides to determine if the optional break is - # needed (there is only one optional break). + # We check both slides to determine if the optional split is + # needed (there is only one optional split). else: html_text = expand_tags(u'\n'.join(slides)) html_text = html_text.replace(u'\n', u'
') @@ -247,14 +247,18 @@ class Renderer(object): else: # The first optional slide fits, which means we have to # render the first optional slide. - text_contains_break = u'[---]' in text - if text_contains_break: + text_contains_split = u'[---]' in text + if text_contains_split: try: text_to_render, text = \ text.split(u'\n[---]\n', 1) except: text_to_render = text.split(u'\n[---]\n')[0] text = u'' + text_to_render, raw_tags, html_tags = \ + self._get_start_tags(text_to_render) + if text: + text = raw_tags + text else: text_to_render = text text = u'' @@ -263,7 +267,7 @@ class Renderer(object): if len(slides) > 1 and text: # Add all slides apart from the last one the list. pages.extend(slides[:-1]) - if text_contains_break: + if text_contains_split: text = slides[-1] + u'\n[---]\n' + text else: text = slides[-1] + u'\n'+ text @@ -492,7 +496,7 @@ class Renderer(object): (raw_text.find(tag[u'start tag']), tag[u'start tag'], tag[u'end tag'])) html_tags.append( - (raw_text.find(tag[u'start tag']), tag[u'start html'])) + (raw_text.find(tag[u'start tag']), tag[u'start html'])) # Sort the lists, so that the tags which were opened first on the first # slide (the text we are checking) will be opened first on the next # slide as well. diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index d6f880e3f..1084d6a3d 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -57,6 +57,14 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): QtCore.SIGNAL(u'clicked()'), self.onDeleteClicked) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(u'rejected()'), self.close) + QtCore.QObject.connect(self.descriptionLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) + QtCore.QObject.connect(self.tagLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) + QtCore.QObject.connect(self.startTagLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) + QtCore.QObject.connect(self.endTagLineEdit, + QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited) # Forces reloading of tags from openlp configuration. FormattingTags.load_tags() @@ -65,7 +73,7 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): Load Display and set field state. """ # Create initial copy from master - self._resetTable() + self._reloadTable() self.selected = -1 return QtGui.QDialog.exec_(self) @@ -73,9 +81,9 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ Table Row selected so display items and set field state. """ - row = self.tagTableWidget.currentRow() - html = FormattingTags.html_expands[row] - self.selected = row + self.savePushButton.setEnabled(False) + self.selected = self.tagTableWidget.currentRow() + html = FormattingTags.get_html_tags()[self.selected] self.descriptionLineEdit.setText(html[u'desc']) self.tagLineEdit.setText(self._strip(html[u'start tag'])) self.startTagLineEdit.setText(html[u'start html']) @@ -85,21 +93,26 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): self.tagLineEdit.setEnabled(False) self.startTagLineEdit.setEnabled(False) self.endTagLineEdit.setEnabled(False) - self.savePushButton.setEnabled(False) self.deletePushButton.setEnabled(False) else: self.descriptionLineEdit.setEnabled(True) self.tagLineEdit.setEnabled(True) self.startTagLineEdit.setEnabled(True) self.endTagLineEdit.setEnabled(True) - self.savePushButton.setEnabled(True) self.deletePushButton.setEnabled(True) + def onTextEdited(self, text): + """ + Enable the ``savePushButton`` when any of the selected tag's properties + has been changed. + """ + self.savePushButton.setEnabled(True) + def onNewClicked(self): """ Add a new tag to list only if it is not a duplicate. """ - for html in FormattingTags.html_expands: + for html in FormattingTags.get_html_tags(): if self._strip(html[u'start tag']) == u'n': critical_error_message_box( translate('OpenLP.FormattingTagForm', 'Update Error'), @@ -117,11 +130,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): u'temporary': False } FormattingTags.add_html_tags([tag]) - self._resetTable() + FormattingTags.save_html_tags() + self._reloadTable() # Highlight new row self.tagTableWidget.selectRow(self.tagTableWidget.rowCount() - 1) self.onRowSelected() self.tagTableWidget.scrollToBottom() + #self.savePushButton.setEnabled(False) def onDeleteClicked(self): """ @@ -130,14 +145,14 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): if self.selected != -1: FormattingTags.remove_html_tag(self.selected) self.selected = -1 - self._resetTable() - FormattingTags.save_html_tags() + FormattingTags.save_html_tags() + self._reloadTable() def onSavedClicked(self): """ Update Custom Tag details if not duplicate and save the data. """ - html_expands = FormattingTags.html_expands + html_expands = FormattingTags.get_html_tags() if self.selected != -1: html = html_expands[self.selected] tag = unicode(self.tagLineEdit.text()) @@ -157,14 +172,13 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): # Keep temporary tags when the user changes one. html[u'temporary'] = False self.selected = -1 - self._resetTable() FormattingTags.save_html_tags() + self._reloadTable() - def _resetTable(self): + def _reloadTable(self): """ Reset List for loading. """ - FormattingTags.load_tags() self.tagTableWidget.clearContents() self.tagTableWidget.setRowCount(0) self.newPushButton.setEnabled(True) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 5369c9799..500d958fd 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -53,6 +53,7 @@ class WizardStrings(object): OL = u'OpenLyrics' OS = u'OpenSong' OSIS = u'OSIS' + PS = u'PowerSong 1.0' SB = u'SongBeamer' SoF = u'Songs of Fellowship' SSP = u'SongShow Plus' diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 4a44c30ef..d5f7715ea 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -171,6 +171,12 @@ class SongImportForm(OpenLPWizard): QtCore.QObject.connect(self.foilPresenterRemoveButton, QtCore.SIGNAL(u'clicked()'), self.onFoilPresenterRemoveButtonClicked) + QtCore.QObject.connect(self.powerSongAddButton, + QtCore.SIGNAL(u'clicked()'), + self.onPowerSongAddButtonClicked) + QtCore.QObject.connect(self.powerSongRemoveButton, + QtCore.SIGNAL(u'clicked()'), + self.onPowerSongRemoveButtonClicked) def addCustomPages(self): """ @@ -217,6 +223,8 @@ class SongImportForm(OpenLPWizard): self.addFileSelectItem(u'foilPresenter') # Open Song self.addFileSelectItem(u'openSong', u'OpenSong') + # PowerSong + self.addFileSelectItem(u'powerSong') # SongBeamer self.addFileSelectItem(u'songBeamer') # Song Show Plus @@ -264,6 +272,8 @@ class SongImportForm(OpenLPWizard): self.formatComboBox.setItemText( SongFormat.FoilPresenter, WizardStrings.FP) self.formatComboBox.setItemText(SongFormat.OpenSong, WizardStrings.OS) + self.formatComboBox.setItemText( + SongFormat.PowerSong, WizardStrings.PS) self.formatComboBox.setItemText( SongFormat.SongBeamer, WizardStrings.SB) self.formatComboBox.setItemText( @@ -305,6 +315,10 @@ class SongImportForm(OpenLPWizard): translate('SongsPlugin.ImportWizardForm', 'Add Files...')) self.dreamBeamRemoveButton.setText( translate('SongsPlugin.ImportWizardForm', 'Remove File(s)')) + self.powerSongAddButton.setText( + translate('SongsPlugin.ImportWizardForm', 'Add Files...')) + self.powerSongRemoveButton.setText( + translate('SongsPlugin.ImportWizardForm', 'Remove File(s)')) self.songsOfFellowshipAddButton.setText( translate('SongsPlugin.ImportWizardForm', 'Add Files...')) self.songsOfFellowshipRemoveButton.setText( @@ -417,6 +431,12 @@ class SongImportForm(OpenLPWizard): WizardStrings.YouSpecifyFile % WizardStrings.DB) self.dreamBeamAddButton.setFocus() return False + elif source_format == SongFormat.PowerSong: + if self.powerSongFileListWidget.count() == 0: + critical_error_message_box(UiStrings().NFSp, + WizardStrings.YouSpecifyFile % WizardStrings.PS) + self.powerSongAddButton.setFocus() + return False elif source_format == SongFormat.SongsOfFellowship: if self.songsOfFellowshipFileListWidget.count() == 0: critical_error_message_box(UiStrings().NFSp, @@ -600,6 +620,22 @@ class SongImportForm(OpenLPWizard): """ self.removeSelectedItems(self.dreamBeamFileListWidget) + def onPowerSongAddButtonClicked(self): + """ + Get PowerSong song database files + """ + self.getFiles(WizardStrings.OpenTypeFile % WizardStrings.PS, + self.powerSongFileListWidget, u'%s (*.song)' + % translate('SongsPlugin.ImportWizardForm', + 'PowerSong 1.0 Song Files') + ) + + def onPowerSongRemoveButtonClicked(self): + """ + Remove selected PowerSong files from the import list + """ + self.removeSelectedItems(self.powerSongFileListWidget) + def onSongsOfFellowshipAddButtonClicked(self): """ Get Songs of Fellowship song database files @@ -717,6 +753,7 @@ class SongImportForm(OpenLPWizard): self.wordsOfWorshipFileListWidget.clear() self.ccliFileListWidget.clear() self.dreamBeamFileListWidget.clear() + self.powerSongFileListWidget.clear() self.songsOfFellowshipFileListWidget.clear() self.genericFileListWidget.clear() self.easySlidesFilenameEdit.setText(u'') @@ -784,6 +821,12 @@ class SongImportForm(OpenLPWizard): filenames=self.getListOfFiles( self.dreamBeamFileListWidget) ) + elif source_format == SongFormat.PowerSong: + # Import PowerSong songs + importer = self.plugin.importSongs(SongFormat.PowerSong, + filenames=self.getListOfFiles( + self.powerSongFileListWidget) + ) elif source_format == SongFormat.SongsOfFellowship: # Import a Songs of Fellowship RTF file importer = self.plugin.importSongs(SongFormat.SongsOfFellowship, diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 28a57339e..16d943a73 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -36,6 +36,7 @@ from openlyricsimport import OpenLyricsImport from wowimport import WowImport from cclifileimport import CCLIFileImport from dreambeamimport import DreamBeamImport +from powersongimport import PowerSongImport from ewimport import EasyWorshipSongImport from songbeamerimport import SongBeamerImport from songshowplusimport import SongShowPlusImport @@ -79,16 +80,17 @@ class SongFormat(object): EasyWorship = 7 FoilPresenter = 8 OpenSong = 9 - SongBeamer = 10 - SongShowPlus = 11 - SongsOfFellowship = 12 - WordsOfWorship = 13 - #CSV = 14 + PowerSong = 10 + SongBeamer = 11 + SongShowPlus = 12 + SongsOfFellowship = 13 + WordsOfWorship = 14 + #CSV = 15 @staticmethod def get_class(format): """ - Return the appropriate imeplementation class. + Return the appropriate implementation class. ``format`` The song format. @@ -111,6 +113,8 @@ class SongFormat(object): return CCLIFileImport elif format == SongFormat.DreamBeam: return DreamBeamImport + elif format == SongFormat.PowerSong: + return PowerSongImport elif format == SongFormat.EasySlides: return EasySlidesImport elif format == SongFormat.EasyWorship: @@ -139,6 +143,7 @@ class SongFormat(object): SongFormat.EasyWorship, SongFormat.FoilPresenter, SongFormat.OpenSong, + SongFormat.PowerSong, SongFormat.SongBeamer, SongFormat.SongShowPlus, SongFormat.SongsOfFellowship, diff --git a/openlp/plugins/songs/lib/powersongimport.py b/openlp/plugins/songs/lib/powersongimport.py new file mode 100644 index 000000000..31491398c --- /dev/null +++ b/openlp/plugins/songs/lib/powersongimport.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2012 Raoul Snyman # +# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # +# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # +# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, 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:`powersongimport` module provides the functionality for importing +PowerSong songs into the OpenLP database. +""" +import logging + +from openlp.core.lib import translate +from openlp.plugins.songs.lib.songimport import SongImport + +log = logging.getLogger(__name__) + +class PowerSongImport(SongImport): + """ + The :class:`PowerSongImport` class provides the ability to import song files + from PowerSong. + + **PowerSong 1.0 Song File Format:** + + The file has a number of label-field (think key-value) pairs. + + Label and Field strings: + + * Every label and field is a variable length string preceded by an + integer specifying it's byte length. + * Integer is 32-bit but is encoded in 7-bit format to save space. Thus + if length will fit in 7 bits (ie <= 127) it takes up only one byte. + + Metadata fields: + + * Every PowerSong file has a TITLE field. + * There is zero or more AUTHOR fields. + * There is always a COPYRIGHTLINE label, but its field may be empty. + This field may also contain a CCLI number: e.g. "CCLI 176263". + + Lyrics fields: + + * Each verse is contained in a PART field. + * Lines have Windows line endings ``CRLF`` (0x0d, 0x0a). + * There is no concept of verse types. + + Valid extensions for a PowerSong song file are: + + * .song + """ + + def doImport(self): + """ + Receive a list of files to import. + """ + if not isinstance(self.importSource, list): + self.logError(unicode(translate('SongsPlugin.PowerSongImport', + 'No files to import.'))) + return + self.importWizard.progressBar.setMaximum(len(self.importSource)) + for file in self.importSource: + if self.stopImportFlag: + return + self.setDefaults() + parse_error = False + with open(file, 'rb') as song_data: + while True: + try: + label = self._readString(song_data) + if not label: + break + field = self._readString(song_data) + except ValueError: + parse_error = True + self.logError(file, unicode( + translate('SongsPlugin.PowerSongImport', + 'Invalid PowerSong file. Unexpected byte value.'))) + break + else: + if label == u'TITLE': + self.title = field.replace(u'\n', u' ') + elif label == u'AUTHOR': + self.parseAuthor(field) + elif label == u'COPYRIGHTLINE': + found_copyright = True + self._parseCopyrightCCLI(field) + elif label == u'PART': + self.addVerse(field) + if parse_error: + continue + # Check that file had TITLE field + if not self.title: + self.logError(file, unicode( + translate('SongsPlugin.PowerSongImport', + 'Invalid PowerSong file. Missing "TITLE" header.'))) + continue + # Check that file had COPYRIGHTLINE label + if not found_copyright: + self.logError(file, unicode( + translate('SongsPlugin.PowerSongImport', + '"%s" Invalid PowerSong file. Missing "COPYRIGHTLINE" ' + 'header.' % self.title))) + continue + # Check that file had at least one verse + if not self.verses: + self.logError(file, unicode( + translate('SongsPlugin.PowerSongImport', + '"%s" Verses not found. Missing "PART" header.' + % self.title))) + continue + if not self.finish(): + self.logError(file) + + def _readString(self, file_object): + """ + Reads in next variable-length string. + """ + string_len = self._read7BitEncodedInteger(file_object) + return unicode(file_object.read(string_len), u'utf-8', u'ignore') + + def _read7BitEncodedInteger(self, file_object): + """ + Reads in a 32-bit integer in compressed 7-bit format. + + Accomplished by reading the integer 7 bits at a time. The high bit + of the byte when set means to continue reading more bytes. + If the integer will fit in 7 bits (ie <= 127), it only takes up one + byte. Otherwise, it may take up to 5 bytes. + + Reference: .NET method System.IO.BinaryReader.Read7BitEncodedInt + """ + val = 0 + shift = 0 + i = 0 + while True: + # Check for corrupted stream (since max 5 bytes per 32-bit integer) + if i == 5: + raise ValueError + byte = self._readByte(file_object) + # Strip high bit and shift left + val += (byte & 0x7f) << shift + shift += 7 + high_bit_set = byte & 0x80 + if not high_bit_set: + break + i += 1 + return val + + def _readByte(self, file_object): + """ + Reads in next byte as an unsigned integer + + Note: returns 0 at end of file. + """ + byte_str = file_object.read(1) + # If read result is empty, then reached end of file + if not byte_str: + return 0 + else: + return ord(byte_str) + + def _parseCopyrightCCLI(self, field): + """ + Look for CCLI song number, and get copyright + """ + copyright, sep, ccli_no = field.rpartition(u'CCLI') + if not sep: + copyright = ccli_no + ccli_no = u'' + if copyright: + self.addCopyright(copyright.rstrip(u'\n').replace(u'\n', u' ')) + if ccli_no: + ccli_no = ccli_no.strip(u' :') + if ccli_no.isdigit(): + self.ccliNumber = ccli_no diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index 74767d793..9bfdce124 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -107,11 +107,11 @@ class SongImport(QtCore.QObject): ``filepath`` This should be the file path if ``self.importSource`` is a list - with different files. If it is not a list, but a single file (for + with different files. If it is not a list, but a single file (for instance a database), then this should be the song's title. ``reason`` - The reason, why the import failed. The string should be as + The reason why the import failed. The string should be as informative as possible. """ self.setDefaults() diff --git a/openlp/plugins/songs/lib/wowimport.py b/openlp/plugins/songs/lib/wowimport.py index 99f448736..97a11d873 100644 --- a/openlp/plugins/songs/lib/wowimport.py +++ b/openlp/plugins/songs/lib/wowimport.py @@ -71,7 +71,7 @@ class WowImport(SongImport): * ``SOH`` (0x01) - Chorus * ``STX`` (0x02) - Bridge - Blocks are seperated by two bytes. The first byte is 0x01, and the + Blocks are separated by two bytes. The first byte is 0x01, and the second byte is 0x80. Lines: @@ -126,7 +126,7 @@ class WowImport(SongImport): ('Invalid Words of Worship song file. Missing ' '"CSongDoc::CBlock" string.')))) continue - # Seek to the beging of the first block + # Seek to the beginning of the first block song_data.seek(82) for block in range(no_of_blocks): self.linesToRead = ord(song_data.read(4)[:1]) @@ -140,7 +140,7 @@ class WowImport(SongImport): block_text += self.lineText self.linesToRead -= 1 block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])] - # Blocks are seperated by 2 bytes, skip them, but not if + # Blocks are separated by 2 bytes, skip them, but not if # this is the last block! if block + 1 < no_of_blocks: song_data.seek(2, os.SEEK_CUR) diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py index fdcb1dd60..45dac95b9 100644 --- a/openlp/plugins/songs/lib/xml.py +++ b/openlp/plugins/songs/lib/xml.py @@ -33,7 +33,7 @@ The basic XML for storing the lyrics in the song database looks like this:: - + @@ -135,7 +135,7 @@ class SongXML(object): The returned list has the following format:: [[{'type': 'v', 'label': '1'}, - u"virtual slide 1[---]virtual slide 2"], + u"optional slide split 1[---]optional slide split 2"], [{'lang': 'en', 'type': 'c', 'label': '1'}, u"English chorus"]] """ self.song_xml = None @@ -317,9 +317,7 @@ class OpenLyrics(object): tags_element = None match = re.search(u'\{/?\w+\}', song.lyrics, re.UNICODE) if match: - # Reset available tags. - FormattingTags.reset_html_tags() - # Named 'formatting' - 'format' is built-in fuction in Python. + # Named 'format_' - 'format' is built-in fuction in Python. format_ = etree.SubElement(song_xml, u'format') tags_element = etree.SubElement(format_, u'tags') tags_element.set(u'application', u'OpenLP') @@ -334,18 +332,59 @@ class OpenLyrics(object): self._add_text_to_element(u'verse', lyrics, None, verse_def) if u'lang' in verse[0]: verse_element.set(u'lang', verse[0][u'lang']) - # Create a list with all "virtual" verses. - virtual_verses = cgi.escape(verse[1]) - virtual_verses = virtual_verses.split(u'[---]') - for index, virtual_verse in enumerate(virtual_verses): + # Create a list with all "optional" verses. + optional_verses = cgi.escape(verse[1]) + optional_verses = optional_verses.split(u'\n[---]\n') + start_tags = u'' + end_tags = u'' + for index, optional_verse in enumerate(optional_verses): + # Fix up missing end and start tags such as {r} or {/r}. + optional_verse = start_tags + optional_verse + start_tags, end_tags = self._get_missing_tags(optional_verse) + optional_verse += end_tags # Add formatting tags to text lines_element = self._add_text_with_tags_to_lines(verse_element, - virtual_verse, tags_element) + optional_verse, tags_element) # Do not add the break attribute to the last lines element. - if index < len(virtual_verses) - 1: + if index < len(optional_verses) - 1: lines_element.set(u'break', u'optional') return self._extract_xml(song_xml) + def _get_missing_tags(self, text): + """ + Tests the given text for not closed formatting tags and returns a tuple + consisting of two unicode strings:: + + (u'{st}{r}', u'{/r}{/st}') + + The first unicode string are the start tags (for the next slide). The + second unicode string are the end tags. + + ``text`` + The text to test. The text must **not** contain html tags, only + OpenLP formatting tags are allowed:: + + {st}{r}Text text text + """ + tags = [] + for tag in FormattingTags.get_html_tags(): + if tag[u'start tag'] == u'{br}': + continue + if text.count(tag[u'start tag']) != text.count(tag[u'end tag']): + tags.append((text.find(tag[u'start tag']), + tag[u'start tag'], tag[u'end tag'])) + # Sort the lists, so that the tags which were opened first on the first + # slide (the text we are checking) will be opened first on the next + # slide as well. + tags.sort(key=lambda tag: tag[0]) + end_tags = [] + start_tags = [] + for tag in tags: + start_tags.append(tag[1]) + end_tags.append(tag[2]) + end_tags.reverse() + return u''.join(start_tags), u''.join(end_tags) + def xml_to_song(self, xml, parse_and_temporary_save=False): """ Create and save a song from OpenLyrics format xml to the database. Since @@ -572,7 +611,8 @@ class OpenLyrics(object): for tag in FormattingTags.get_html_tags()] new_tags = [tag for tag in found_tags if tag[u'start tag'] not in existing_tag_ids] - FormattingTags.add_html_tags(new_tags, True) + FormattingTags.add_html_tags(new_tags) + FormattingTags.save_html_tags() def _process_lines_mixed_content(self, element, newlines=True): """