openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #15501
[Merge] lp:~sfindlay/openlp/songs-import-powersong into lp:openlp
Samuel Findlay has proposed merging lp:~sfindlay/openlp/songs-import-powersong into lp:openlp.
Requested reviews:
Raoul Snyman (raoul-snyman)
For more details, see:
https://code.launchpad.net/~sfindlay/openlp/songs-import-powersong/+merge/104482
* Code tidy ups.
---
* As per some of Phill's suggestions, rewrote PowerSongImport class to read variable-length strings directly from file, rather than searching for them. Doesn't seem to be any faster (32s vs 31s to import 1057 songs), but the new class is more true/accurate to the file format.
* Also elected to show song title with any "could not import" error messages, since PowerSong song filenames are generally a meaningless (to user) db ID.
---
Added PowerSong song importer.
* PowerSong is open source and windows-only <http://www.powersong.org/>
* This importer imports individual song files.
* Tested on win7 x64 and ubuntu 11.10 x64 with test set of 1057 songs, which now seems to be missing from website, this is a copy: <https://docs.google.com/open?id=0B076ddXXPC2WUnA1VXZoY1FCVzA>
Also fixed a few typos in comments in importer, songimport, wowimport in songs.lib
--
https://code.launchpad.net/~sfindlay/openlp/songs-import-powersong/+merge/104482
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/ui/wizard.py'
--- openlp/core/ui/wizard.py 2012-04-25 18:50:08 +0000
+++ openlp/core/ui/wizard.py 2012-05-03 04:44:18 +0000
@@ -53,6 +53,7 @@
OL = u'OpenLyrics'
OS = u'OpenSong'
OSIS = u'OSIS'
+ PS = u'PowerSong'
SB = u'SongBeamer'
SoF = u'Songs of Fellowship'
SSP = u'SongShow Plus'
=== modified file 'openlp/plugins/songs/forms/songimportform.py'
--- openlp/plugins/songs/forms/songimportform.py 2012-04-25 18:50:08 +0000
+++ openlp/plugins/songs/forms/songimportform.py 2012-05-03 04:44:18 +0000
@@ -171,6 +171,12 @@
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 @@
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
@@ -265,6 +273,8 @@
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(
SongFormat.SongShowPlus, WizardStrings.SSP)
@@ -305,6 +315,10 @@
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 @@
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 @@
"""
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 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 @@
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 @@
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,
=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py 2012-04-25 18:50:08 +0000
+++ openlp/plugins/songs/lib/importer.py 2012-05-03 04:44:18 +0000
@@ -36,6 +36,7 @@
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 @@
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 @@
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 @@
SongFormat.EasyWorship,
SongFormat.FoilPresenter,
SongFormat.OpenSong,
+ SongFormat.PowerSong,
SongFormat.SongBeamer,
SongFormat.SongShowPlus,
SongFormat.SongsOfFellowship,
=== added file 'openlp/plugins/songs/lib/powersongimport.py'
--- openlp/plugins/songs/lib/powersongimport.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/lib/powersongimport.py 2012-05-03 04:44:18 +0000
@@ -0,0 +1,174 @@
+# -*- 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 Song File Format:**
+
+ The file has a number of label-field pairs of variable length.
+
+ Labels and Fields:
+ * Every label and field is preceded by an integer which specifies its
+ byte-length.
+ * If the length < 128 bytes, only one byte is used to encode
+ the length integer.
+ * But if it's greater, as many bytes are used as necessary:
+ * the first byte = (length % 128) + 128
+ * the next byte = length / 128
+ * another byte is only used if (length / 128) >= 128
+ * and so on (3 bytes needed iff length > 16383)
+
+ Metadata fields:
+ * Every PowerSong file begins with a TITLE field.
+ * This is followed by zero or more AUTHOR fields.
+ * The next label is always COPYRIGHTLINE, 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):
+ 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 self.song_file:
+ # Get title to check file is valid PowerSong song format
+ label, field = self.readLabelField()
+ if label == u'TITLE':
+ self.title = field.replace(u'\n', u' ')
+ else:
+ self.logError(file, unicode(
+ translate('SongsPlugin.PowerSongSongImport', \
+ 'Invalid PowerSong file. Missing "TITLE" header.')))
+ continue
+ # Get rest of fields from file
+ while True:
+ label, field = self.readLabelField()
+ if not label:
+ break
+ if label == u'AUTHOR':
+ self.parseAuthor(field)
+ elif label == u'COPYRIGHTLINE':
+ found_copyright = True
+ self.parseCopyrightCCLI(field)
+ elif label == u'PART':
+ self.addVerse(field)
+ else:
+ parse_error = True
+ self.logError(file, unicode(
+ translate('SongsPlugin.PowerSongSongImport', \
+ '"%s" Invalid PowerSong file. Unknown header: "%s".'
+ % (self.title, label))))
+ break
+ if parse_error:
+ continue
+ # Check that file had COPYRIGHTLINE label
+ if not found_copyright:
+ self.logError(file, unicode(
+ translate('SongsPlugin.PowerSongSongImport', \
+ '"%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.PowerSongSongImport', \
+ '"%s" Verses not found. Missing "PART" header.'
+ % self.title)))
+ continue
+ if not self.finish():
+ self.logError(file)
+
+ def readLabelField(self):
+ """
+ Read (as a 2-tuple) the next two variable-length strings
+ """
+ label = unicode(self.song_file.read(
+ self.readLength()), u'utf-8', u'ignore')
+ if label:
+ field = unicode(self.song_file.read(
+ self.readLength()), u'utf-8', u'ignore')
+ else:
+ field = u''
+ return label, field
+
+ def readLength(self):
+ """
+ Read the byte-length of the next variable-length string
+
+ If at the end of the file, returns 0.
+ """
+ this_byte = self.song_file.read(1)
+ if not this_byte:
+ return 0
+ this_byte_val = ord(this_byte)
+ if this_byte_val < 128:
+ return this_byte_val
+ else:
+ return (self.readLength() * 128) + (this_byte_val - 128)
+
+ 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
=== modified file 'openlp/plugins/songs/lib/songimport.py'
--- openlp/plugins/songs/lib/songimport.py 2012-04-29 15:31:56 +0000
+++ openlp/plugins/songs/lib/songimport.py 2012-05-03 04:44:18 +0000
@@ -107,11 +107,11 @@
``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()
=== modified file 'openlp/plugins/songs/lib/wowimport.py'
--- openlp/plugins/songs/lib/wowimport.py 2012-04-04 07:26:51 +0000
+++ openlp/plugins/songs/lib/wowimport.py 2012-05-03 04:44:18 +0000
@@ -71,7 +71,7 @@
* ``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 @@
('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 @@
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)