openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #15506
[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/104616
* Implemented BinaryReader.Read7BitEncodedInt from .NET (15% faster).
* Changed 'PowerSong' to 'PowerSong 1.0'.
* 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/104616
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 20:08:21 +0000
@@ -53,6 +53,7 @@
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'
=== 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 20:08:21 +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 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 @@
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 20:08:21 +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 20:08:21 +0000
@@ -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
=== 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 20:08:21 +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 20:08:21 +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)
Follow ups