openlp/openlp/plugins/songs/lib/powersongimport.py

219 lines
9.1 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2013-01-06 17:25:49 +00:00
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
2012-12-29 20:56:56 +00:00
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
2012-11-11 21:16:14 +00:00
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
2012-10-21 13:16:22 +00:00
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# 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 fnmatch
2012-05-07 13:38:02 +00:00
import os
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.
2012-05-03 12:50:10 +00:00
**PowerSong 1.0 Song File Format:**
2012-05-03 12:50:10 +00:00
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
"""
@staticmethod
def isValidSource(import_source):
"""
Checks if source is a PowerSong 1.0 folder:
* is a directory
* contains at least one *.song file
"""
if os.path.isdir(import_source):
for file in os.listdir(import_source):
if fnmatch.fnmatch(file, u'*.song'):
return True
return False
def doImport(self):
"""
Receive either a list of files or a folder (unicode) to import.
"""
2012-06-03 15:32:21 +00:00
from importer import SongFormat
PS_string = SongFormat.get(SongFormat.PowerSong, u'name')
2013-03-07 08:05:43 +00:00
if isinstance(self.import_source, unicode):
if os.path.isdir(self.import_source):
dir = self.import_source
self.import_source = []
for file in os.listdir(dir):
if fnmatch.fnmatch(file, u'*.song'):
2013-03-07 08:05:43 +00:00
self.import_source.append(os.path.join(dir, file))
else:
2013-03-07 08:05:43 +00:00
self.import_source = u''
if not self.import_source or not isinstance(self.import_source, list):
2013-01-06 17:25:49 +00:00
self.logError(translate('SongsPlugin.PowerSongImport', 'No songs to import.'),
translate('SongsPlugin.PowerSongImport', 'No %s files found.') % PS_string)
2012-05-02 09:14:30 +00:00
return
2013-03-07 08:05:43 +00:00
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
for file in self.import_source:
2013-02-07 11:33:47 +00:00
if self.stop_import_flag:
2012-05-02 09:14:30 +00:00
return
self.setDefaults()
parse_error = False
with open(file, 'rb') as song_data:
2012-05-02 09:14:30 +00:00
while True:
try:
label = self._readString(song_data)
if not label:
break
field = self._readString(song_data)
except ValueError:
2012-05-02 09:14:30 +00:00
parse_error = True
self.logError(os.path.basename(file), unicode(
2013-01-06 17:25:49 +00:00
translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Unexpected byte value.')) %
PS_string)
2012-05-02 09:14:30 +00:00
break
else:
if label == u'TITLE':
self.title = field.replace(u'\n', u' ')
elif label == u'AUTHOR':
2013-03-07 08:05:43 +00:00
self.parse_author(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(os.path.basename(file), unicode(
2013-01-06 17:25:49 +00:00
translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Missing "TITLE" header.')) % PS_string)
continue
# Check that file had COPYRIGHTLINE label
if not found_copyright:
self.logError(self.title, unicode(
2013-01-06 17:25:49 +00:00
translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Missing "COPYRIGHTLINE" header.')) %
PS_string)
continue
# Check that file had at least one verse
if not self.verses:
self.logError(self.title, unicode(
2013-01-06 17:25:49 +00:00
translate('SongsPlugin.PowerSongImport', 'Verses not found. Missing "PART" header.')))
continue
2012-05-02 09:14:30 +00:00
if not self.finish():
self.logError(self.title)
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
2012-05-02 09:14:30 +00:00
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)
2012-05-02 09:14:30 +00:00
def _parseCopyrightCCLI(self, field):
2012-05-02 09:14:30 +00:00
"""
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