openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #15448
[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