← 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:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~sfindlay/openlp/songs-import-powersong/+merge/104102

Added PowerSong song importer.
* PowerSong is open source and windows-only <http://www.powersong.org/>
* Songs are stored in flat file db.
* 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/104102
Your team OpenLP Core is requested to review the proposed merge of lp:~sfindlay/openlp/songs-import-powersong into 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-04-30 12:48:21 +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-04-30 12:48: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 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-04-30 12:48: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-04-30 12:48:21 +0000
@@ -0,0 +1,160 @@
+# -*- 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
+import re
+
+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:**
+
+    * Encoded as UTF-8.
+    * The file has a number of fields, with the song metadata fields first,
+      followed by the lyrics fields.
+
+    Fields:
+        Each field begins with one of four labels, each of which begin with one
+        non-printing byte:
+
+        * ``ENQ`` (0x05) ``TITLE``
+        * ``ACK`` (0x06) ``AUTHOR``
+        * ``CR`` (0x0d) ``COPYRIGHTLINE``
+        * ``EOT`` (0x04) ``PART``
+
+        The field label is separated from the field contents by one random byte.
+        Each field ends at the next field label, or at the end of the file.
+
+    Metadata fields:
+        * Every PowerSong file begins with a TITLE field.
+        * This is followed by zero or more AUTHOR fields.
+        * The next field is always COPYRIGHTLINE, but it may be empty (in which
+          case the byte following the label is the null byte 0x00).
+          When the field contents are not empty, the first byte is 0xc2 and
+          should be discarded.
+          This field may contain a CCLI number at the end: e.g. "CCLI 176263"
+
+    Lyrics fields:
+        * The COPYRIGHTLINE field is followed by zero or more PART fields, each
+          of which contains one verse.
+        * 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 __init__(self, manager, **kwargs):
+        """
+        Initialise the PowerSong importer.
+        """
+        SongImport.__init__(self, manager, **kwargs)
+
+    def doImport(self):
+        """
+        Receive a single file or a list of files to import.
+        """
+        if isinstance(self.importSource, list):
+            self.importWizard.progressBar.setMaximum(len(self.importSource))
+            for file in self.importSource:
+                if self.stopImportFlag:
+                    return
+                self.setDefaults()
+                with open(file, 'rb') as song_file:
+                    # Check file is valid PowerSong song format
+                    if song_file.read(6) != u'\x05TITLE':
+                        self.logError(file, unicode(
+                            translate('SongsPlugin.PowerSongSongImport',
+                            ('Invalid PowerSong song file. Missing '
+                                '"\x05TITLE" header.'))))
+                        continue
+                    song_data = unicode(song_file.read(), u'utf-8', u'replace')
+                    # Extract title and author fields
+                    first_part, sep, song_data = song_data.partition(
+                        u'\x0DCOPYRIGHTLINE')
+                    if not sep:
+                        self.logError(file, unicode(
+                            translate('SongsPlugin.PowerSongSongImport',
+                                ('Invalid PowerSong song file. Missing '
+                                 '"\x0DCOPYRIGHTLINE" string.'))))
+                        continue
+                    title_authors = first_part.split(u'\x06AUTHOR')
+                    # Get the song title
+                    self.title = self.stripControlChars(title_authors[0][1:])
+                    # Extract the author(s)
+                    for author in title_authors[1:]:
+                        self.parseAuthor(self.stripControlChars(author[1:]))
+                    # Get copyright and CCLI number
+                    copyright, sep, song_data = song_data.partition(
+                        u'\x04PART')
+                    if not sep:
+                        self.logError(file, unicode(
+                            translate('SongsPlugin.PowerSongSongImport',
+                                ('No verses found. Missing '
+                                 '"\x04PART" string.'))))
+                        continue
+                    copyright, sep, ccli_no = copyright[1:].rpartition(u'CCLI ')
+                    if not sep:
+                        copyright = ccli_no
+                        ccli_no = u''
+                    if copyright:
+                        if copyright[0] == u'\u00c2':
+                            copyright = copyright[1:]
+                        self.addCopyright(self.stripControlChars(
+                            copyright.rstrip(u'\n')))
+                    if ccli_no:
+                        ccli_no = ccli_no.strip()
+                        if ccli_no.isdigit():
+                            self.ccliNumber = self.stripControlChars(ccli_no)
+                    # Get the verse(s)
+                    verses = song_data.split(u'\x04PART')
+                    for verse in verses:
+                        self.addVerse(self.stripControlChars(verse[1:]))
+                if not self.finish():
+                    self.logError(file)
+
+    def stripControlChars(self, text):
+        """
+        Get rid of ASCII control characters.
+
+        Illegals chars are ASCII code points 0-31 and 127, except:
+            * ``HT`` (0x09) - Tab
+            * ``LF`` (0x0a) - Line feed
+            * ``CR`` (0x0d) - Carriage return
+        """
+        ILLEGAL_CHARS = u'([\x00-\x08\x0b-\x0c\x0e-\x1f\x7f])'
+        return re.sub(ILLEGAL_CHARS, '', text)
\ No newline at end of file

=== modified file 'openlp/plugins/songs/lib/songimport.py'
--- openlp/plugins/songs/lib/songimport.py	2012-04-03 17:58:42 +0000
+++ openlp/plugins/songs/lib/songimport.py	2012-04-30 12:48:21 +0000
@@ -111,7 +111,7 @@
             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-04-30 12:48: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