1
# -*- coding: utf-8 -*-
2
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
4
###############################################################################
5
# OpenLP - Open Source Lyrics Projection #
6
# --------------------------------------------------------------------------- #
7
# Copyright (c) 2008-2012 Raoul Snyman #
8
# Portions copyright (c) 2008-2012 Tim Bentley, Gerald Britton, Jonathan #
9
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
10
# Meinert Jordan, Armin Köhler, Edwin Lunando, Joshua Miller, Stevan Pettit, #
11
# Andreas Preikschat, Mattias Põldaru, Christian Richter, Philip Ridout, #
12
# Simon Scudder, Jeffrey Smith, Maikel Stuivenberg, Martin Thompson, Jon #
13
# Tibble, Dave Warnock, Frode Woldsund #
14
# --------------------------------------------------------------------------- #
15
# This program is free software; you can redistribute it and/or modify it #
16
# under the terms of the GNU General Public License as published by the Free #
17
# Software Foundation; version 2 of the License. #
19
# This program is distributed in the hope that it will be useful, but WITHOUT #
20
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
21
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
24
# You should have received a copy of the GNU General Public License along #
25
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
26
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
27
###############################################################################
29
The :mod:`powersongimport` module provides the functionality for importing
30
PowerSong songs into the OpenLP database.
36
from openlp.core.lib import translate
37
from openlp.plugins.songs.lib.songimport import SongImport
39
log = logging.getLogger(__name__)
41
class PowerSongImport(SongImport):
43
The :class:`PowerSongImport` class provides the ability to import song files
46
**PowerSong 1.0 Song File Format:**
48
The file has a number of label-field (think key-value) pairs.
50
Label and Field strings:
52
* Every label and field is a variable length string preceded by an
53
integer specifying it's byte length.
54
* Integer is 32-bit but is encoded in 7-bit format to save space. Thus
55
if length will fit in 7 bits (ie <= 127) it takes up only one byte.
59
* Every PowerSong file has a TITLE field.
60
* There is zero or more AUTHOR fields.
61
* There is always a COPYRIGHTLINE label, but its field may be empty.
62
This field may also contain a CCLI number: e.g. "CCLI 176263".
66
* Each verse is contained in a PART field.
67
* Lines have Windows line endings ``CRLF`` (0x0d, 0x0a).
68
* There is no concept of verse types.
70
Valid extensions for a PowerSong song file are:
75
def isValidSource(import_source):
77
Checks if source is a PowerSong 1.0 folder:
79
* contains at least one *.song file
81
if os.path.isdir(import_source):
82
for file in os.listdir(import_source):
83
if fnmatch.fnmatch(file, u'*.song'):
89
Receive either a list of files or a folder (unicode) to import.
91
from importer import SongFormat
92
PS_string = SongFormat.get(SongFormat.PowerSong, u'name')
93
if isinstance(self.importSource, unicode):
94
if os.path.isdir(self.importSource):
95
dir = self.importSource
96
self.importSource = []
97
for file in os.listdir(dir):
98
if fnmatch.fnmatch(file, u'*.song'):
99
self.importSource.append(os.path.join(dir, file))
101
self.importSource = u''
102
if not self.importSource or not isinstance(self.importSource, list):
103
self.logError(unicode(translate('SongsPlugin.PowerSongImport',
104
'No songs to import.')),
105
unicode(translate('SongsPlugin.PowerSongImport',
106
'No %s files found.' % PS_string)))
108
self.importWizard.progressBar.setMaximum(len(self.importSource))
109
for file in self.importSource:
110
if self.stopImportFlag:
114
with open(file, 'rb') as song_data:
117
label = self._readString(song_data)
120
field = self._readString(song_data)
123
self.logError(os.path.basename(file), unicode(
124
translate('SongsPlugin.PowerSongImport',
125
'Invalid %s file. Unexpected byte value.'
129
if label == u'TITLE':
130
self.title = field.replace(u'\n', u' ')
131
elif label == u'AUTHOR':
132
self.parseAuthor(field)
133
elif label == u'COPYRIGHTLINE':
134
found_copyright = True
135
self._parseCopyrightCCLI(field)
136
elif label == u'PART':
140
# Check that file had TITLE field
142
self.logError(os.path.basename(file), unicode(
143
translate('SongsPlugin.PowerSongImport',
144
'Invalid %s file. Missing "TITLE" header.' % PS_string)))
146
# Check that file had COPYRIGHTLINE label
147
if not found_copyright:
148
self.logError(self.title, unicode(
149
translate('SongsPlugin.PowerSongImport',
150
'Invalid %s file. Missing "COPYRIGHTLINE" '
151
'header.' % PS_string)))
153
# Check that file had at least one verse
155
self.logError(self.title, unicode(
156
translate('SongsPlugin.PowerSongImport',
157
'Verses not found. Missing "PART" header.')))
159
if not self.finish():
160
self.logError(self.title)
162
def _readString(self, file_object):
164
Reads in next variable-length string.
166
string_len = self._read7BitEncodedInteger(file_object)
167
return unicode(file_object.read(string_len), u'utf-8', u'ignore')
169
def _read7BitEncodedInteger(self, file_object):
171
Reads in a 32-bit integer in compressed 7-bit format.
173
Accomplished by reading the integer 7 bits at a time. The high bit
174
of the byte when set means to continue reading more bytes.
175
If the integer will fit in 7 bits (ie <= 127), it only takes up one
176
byte. Otherwise, it may take up to 5 bytes.
178
Reference: .NET method System.IO.BinaryReader.Read7BitEncodedInt
184
# Check for corrupted stream (since max 5 bytes per 32-bit integer)
187
byte = self._readByte(file_object)
188
# Strip high bit and shift left
189
val += (byte & 0x7f) << shift
191
high_bit_set = byte & 0x80
197
def _readByte(self, file_object):
199
Reads in next byte as an unsigned integer
201
Note: returns 0 at end of file.
203
byte_str = file_object.read(1)
204
# If read result is empty, then reached end of file
210
def _parseCopyrightCCLI(self, field):
212
Look for CCLI song number, and get copyright
214
copyright, sep, ccli_no = field.rpartition(u'CCLI')
219
self.addCopyright(copyright.rstrip(u'\n').replace(u'\n', u' '))
221
ccli_no = ccli_no.strip(u' :')
222
if ccli_no.isdigit():
223
self.ccliNumber = ccli_no