forked from openlp/openlp
r1968
This commit is contained in:
commit
43892f7d53
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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'<br>')
|
||||
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
195
openlp/plugins/songs/lib/powersongimport.py
Normal file
195
openlp/plugins/songs/lib/powersongimport.py
Normal file
@ -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
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -33,7 +33,7 @@ The basic XML for storing the lyrics in the song database looks like this::
|
||||
<song version="1.0">
|
||||
<lyrics>
|
||||
<verse type="c" label="1" lang="en">
|
||||
<![CDATA[Chorus virtual slide 1[---]Chorus virtual slide 2]]>
|
||||
<![CDATA[Chorus optional split 1[---]Chorus optional split 2]]>
|
||||
</verse>
|
||||
</lyrics>
|
||||
</song>
|
||||
@ -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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user