2012-04-29 14:08:25 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2013-01-06 17:25:49 +00:00
|
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
2012-04-29 14:08:25 +00:00
|
|
|
|
|
|
|
###############################################################################
|
|
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
|
|
# --------------------------------------------------------------------------- #
|
2015-12-31 22:46:06 +00:00
|
|
|
# Copyright (c) 2008-2016 OpenLP Developers #
|
2012-04-29 14:08:25 +00:00
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# 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 #
|
|
|
|
###############################################################################
|
|
|
|
"""
|
2014-07-04 09:35:10 +00:00
|
|
|
The :mod:`powersong` module provides the functionality for importing
|
2012-04-29 14:08:25 +00:00
|
|
|
PowerSong songs into the OpenLP database.
|
|
|
|
"""
|
|
|
|
import logging
|
2012-05-07 13:10:26 +00:00
|
|
|
import fnmatch
|
2012-05-07 13:38:02 +00:00
|
|
|
import os
|
2012-04-29 14:08:25 +00:00
|
|
|
|
2013-10-13 20:36:42 +00:00
|
|
|
from openlp.core.common import translate
|
2014-07-04 09:31:06 +00:00
|
|
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
2012-04-29 14:08:25 +00:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2014-03-06 22:05:15 +00:00
|
|
|
|
2012-04-29 14:08:25 +00:00
|
|
|
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-04-29 14:08:25 +00:00
|
|
|
|
2012-05-03 12:50:10 +00:00
|
|
|
The file has a number of label-field (think key-value) pairs.
|
2012-05-03 12:41:49 +00:00
|
|
|
|
|
|
|
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.
|
2012-04-29 14:08:25 +00:00
|
|
|
|
|
|
|
Metadata fields:
|
2012-05-03 12:41:49 +00:00
|
|
|
|
|
|
|
* 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.
|
2012-05-01 13:51:46 +00:00
|
|
|
This field may also contain a CCLI number: e.g. "CCLI 176263".
|
2012-04-29 14:08:25 +00:00
|
|
|
|
|
|
|
Lyrics fields:
|
2012-05-03 12:41:49 +00:00
|
|
|
|
2012-05-01 13:51:46 +00:00
|
|
|
* Each verse is contained in a PART field.
|
2012-04-30 10:57:44 +00:00
|
|
|
* Lines have Windows line endings ``CRLF`` (0x0d, 0x0a).
|
2012-04-29 14:08:25 +00:00
|
|
|
* There is no concept of verse types.
|
|
|
|
|
|
|
|
Valid extensions for a PowerSong song file are:
|
2012-05-03 12:41:49 +00:00
|
|
|
|
2012-04-30 12:19:36 +00:00
|
|
|
* .song
|
2012-04-29 14:08:25 +00:00
|
|
|
"""
|
2012-05-19 10:43:19 +00:00
|
|
|
@staticmethod
|
2014-03-05 18:58:22 +00:00
|
|
|
def is_valid_source(import_source):
|
2012-05-19 10:43:19 +00:00
|
|
|
"""
|
|
|
|
Checks if source is a PowerSong 1.0 folder:
|
|
|
|
* is a directory
|
2015-09-08 19:13:59 +00:00
|
|
|
* contains at least one \*.song file
|
2012-05-19 10:43:19 +00:00
|
|
|
"""
|
2012-05-31 09:15:45 +00:00
|
|
|
if os.path.isdir(import_source):
|
|
|
|
for file in os.listdir(import_source):
|
2013-08-31 18:17:38 +00:00
|
|
|
if fnmatch.fnmatch(file, '*.song'):
|
2012-05-31 09:15:45 +00:00
|
|
|
return True
|
2012-05-19 10:43:19 +00:00
|
|
|
return False
|
2012-04-29 14:08:25 +00:00
|
|
|
|
2014-03-06 20:40:08 +00:00
|
|
|
def do_import(self):
|
2012-04-29 14:08:25 +00:00
|
|
|
"""
|
2012-05-07 13:10:26 +00:00
|
|
|
Receive either a list of files or a folder (unicode) to import.
|
2012-04-29 14:08:25 +00:00
|
|
|
"""
|
2014-07-04 09:31:06 +00:00
|
|
|
from openlp.plugins.songs.lib.importer import SongFormat
|
2014-03-06 22:05:15 +00:00
|
|
|
ps_string = SongFormat.get(SongFormat.PowerSong, 'name')
|
2013-08-31 18:17:38 +00:00
|
|
|
if isinstance(self.import_source, str):
|
2013-03-07 08:05:43 +00:00
|
|
|
if os.path.isdir(self.import_source):
|
|
|
|
dir = self.import_source
|
|
|
|
self.import_source = []
|
2012-05-07 13:10:26 +00:00
|
|
|
for file in os.listdir(dir):
|
2013-08-31 18:17:38 +00:00
|
|
|
if fnmatch.fnmatch(file, '*.song'):
|
2013-03-07 08:05:43 +00:00
|
|
|
self.import_source.append(os.path.join(dir, file))
|
2012-05-07 13:10:26 +00:00
|
|
|
else:
|
2013-08-31 18:17:38 +00:00
|
|
|
self.import_source = ''
|
2013-03-07 08:05:43 +00:00
|
|
|
if not self.import_source or not isinstance(self.import_source, list):
|
2014-03-05 18:58:22 +00:00
|
|
|
self.log_error(translate('SongsPlugin.PowerSongImport', 'No songs to import.'),
|
2014-03-06 22:05:15 +00:00
|
|
|
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
|
2014-03-05 18:58:22 +00:00
|
|
|
self.set_defaults()
|
2012-05-02 09:14:30 +00:00
|
|
|
parse_error = False
|
2012-05-03 12:41:49 +00:00
|
|
|
with open(file, 'rb') as song_data:
|
2012-05-02 09:14:30 +00:00
|
|
|
while True:
|
2012-05-03 12:41:49 +00:00
|
|
|
try:
|
2014-03-06 22:05:15 +00:00
|
|
|
label = self._read_string(song_data)
|
2012-05-03 12:41:49 +00:00
|
|
|
if not label:
|
|
|
|
break
|
2014-03-06 22:05:15 +00:00
|
|
|
field = self._read_string(song_data)
|
2012-05-03 12:41:49 +00:00
|
|
|
except ValueError:
|
2012-05-02 09:14:30 +00:00
|
|
|
parse_error = True
|
2014-03-05 18:58:22 +00:00
|
|
|
self.log_error(os.path.basename(file), str(
|
2013-01-06 17:25:49 +00:00
|
|
|
translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Unexpected byte value.')) %
|
2014-03-21 21:38:08 +00:00
|
|
|
ps_string)
|
2012-05-02 09:14:30 +00:00
|
|
|
break
|
2012-05-03 12:41:49 +00:00
|
|
|
else:
|
2013-08-31 18:17:38 +00:00
|
|
|
if label == 'TITLE':
|
|
|
|
self.title = field.replace('\n', ' ')
|
|
|
|
elif label == 'AUTHOR':
|
2013-03-07 08:05:43 +00:00
|
|
|
self.parse_author(field)
|
2013-08-31 18:17:38 +00:00
|
|
|
elif label == 'COPYRIGHTLINE':
|
2012-05-03 12:41:49 +00:00
|
|
|
found_copyright = True
|
2014-03-06 22:05:15 +00:00
|
|
|
self._parse_copyright_cCCLI(field)
|
2013-08-31 18:17:38 +00:00
|
|
|
elif label == 'PART':
|
2014-03-05 18:58:22 +00:00
|
|
|
self.add_verse(field)
|
2012-05-03 12:41:49 +00:00
|
|
|
if parse_error:
|
|
|
|
continue
|
|
|
|
# Check that file had TITLE field
|
|
|
|
if not self.title:
|
2014-03-05 18:58:22 +00:00
|
|
|
self.log_error(os.path.basename(file), str(
|
2014-03-06 22:05:15 +00:00
|
|
|
translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Missing "TITLE" header.')) % ps_string)
|
2012-05-03 12:41:49 +00:00
|
|
|
continue
|
|
|
|
# Check that file had COPYRIGHTLINE label
|
|
|
|
if not found_copyright:
|
2014-03-05 18:58:22 +00:00
|
|
|
self.log_error(self.title, str(
|
2013-01-06 17:25:49 +00:00
|
|
|
translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Missing "COPYRIGHTLINE" header.')) %
|
2014-03-21 21:38:08 +00:00
|
|
|
ps_string)
|
2012-05-03 12:41:49 +00:00
|
|
|
continue
|
|
|
|
# Check that file had at least one verse
|
|
|
|
if not self.verses:
|
2014-03-05 18:58:22 +00:00
|
|
|
self.log_error(self.title, str(
|
2013-01-06 17:25:49 +00:00
|
|
|
translate('SongsPlugin.PowerSongImport', 'Verses not found. Missing "PART" header.')))
|
2012-05-03 12:41:49 +00:00
|
|
|
continue
|
2012-05-02 09:14:30 +00:00
|
|
|
if not self.finish():
|
2014-03-05 18:58:22 +00:00
|
|
|
self.log_error(self.title)
|
2012-04-30 10:57:44 +00:00
|
|
|
|
2014-03-06 22:05:15 +00:00
|
|
|
def _read_string(self, file_object):
|
2012-04-30 10:57:44 +00:00
|
|
|
"""
|
2012-05-03 12:41:49 +00:00
|
|
|
Reads in next variable-length string.
|
2012-05-01 13:51:46 +00:00
|
|
|
"""
|
2014-03-06 22:05:15 +00:00
|
|
|
string_len = self._read_7_bit_encoded_integer(file_object)
|
2013-08-31 18:17:38 +00:00
|
|
|
return str(file_object.read(string_len), 'utf-8', 'ignore')
|
2012-05-03 12:41:49 +00:00
|
|
|
|
2014-03-06 22:05:15 +00:00
|
|
|
def _read_7_bit_encoded_integer(self, file_object):
|
2012-05-03 12:41:49 +00:00
|
|
|
"""
|
|
|
|
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
|
2014-03-06 22:05:15 +00:00
|
|
|
byte = self._read_byte(file_object)
|
2012-05-03 12:41:49 +00:00
|
|
|
# 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
|
2012-05-01 13:51:46 +00:00
|
|
|
|
2014-03-06 22:05:15 +00:00
|
|
|
def _read_byte(self, file_object):
|
2012-05-01 13:51:46 +00:00
|
|
|
"""
|
2012-05-03 12:41:49 +00:00
|
|
|
Reads in next byte as an unsigned integer
|
2012-05-02 09:14:30 +00:00
|
|
|
|
2012-05-03 12:41:49 +00:00
|
|
|
Note: returns 0 at end of file.
|
2012-04-30 10:57:44 +00:00
|
|
|
"""
|
2012-05-03 12:41:49 +00:00
|
|
|
byte_str = file_object.read(1)
|
|
|
|
# If read result is empty, then reached end of file
|
|
|
|
if not byte_str:
|
2012-05-01 13:51:46 +00:00
|
|
|
return 0
|
|
|
|
else:
|
2012-05-03 12:41:49 +00:00
|
|
|
return ord(byte_str)
|
2012-05-02 09:14:30 +00:00
|
|
|
|
2014-03-06 22:05:15 +00:00
|
|
|
def _parse_copyright_cCCLI(self, field):
|
2012-05-02 09:14:30 +00:00
|
|
|
"""
|
|
|
|
Look for CCLI song number, and get copyright
|
|
|
|
"""
|
2013-08-31 18:17:38 +00:00
|
|
|
copyright, sep, ccli_no = field.rpartition('CCLI')
|
2012-05-02 09:14:30 +00:00
|
|
|
if not sep:
|
|
|
|
copyright = ccli_no
|
2013-08-31 18:17:38 +00:00
|
|
|
ccli_no = ''
|
2012-05-02 09:14:30 +00:00
|
|
|
if copyright:
|
2014-03-05 18:58:22 +00:00
|
|
|
self.add_copyright(copyright.rstrip('\n').replace('\n', ' '))
|
2012-05-02 09:14:30 +00:00
|
|
|
if ccli_no:
|
2013-08-31 18:17:38 +00:00
|
|
|
ccli_no = ccli_no.strip(' :')
|
2012-05-02 09:14:30 +00:00
|
|
|
if ccli_no.isdigit():
|
2014-03-05 18:58:22 +00:00
|
|
|
self.ccli_number = ccli_no
|