← 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/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)