← Back to team overview

openlp-core team mailing list archive

[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