openlp/openlp/plugins/songs/lib/importers/powersong.py

216 lines
8.6 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-02-01 10:10:57 +00:00
# Copyright (c) 2008-2022 OpenLP Developers #
2019-04-13 13:00:22 +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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
2014-07-04 09:35:10 +00:00
The :mod:`powersong` module provides the functionality for importing
PowerSong songs into the OpenLP database.
"""
import logging
from pathlib import Path
2017-10-07 07:05:07 +00:00
from openlp.core.common.i18n import translate
2014-07-04 09:31:06 +00:00
from openlp.plugins.songs.lib.importers.songimport import SongImport
2018-10-02 04:39:42 +00:00
log = logging.getLogger(__name__)
2014-03-06 22:05:15 +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-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
2014-03-05 18:58:22 +00:00
def is_valid_source(import_source):
"""
Checks if source is a PowerSong 1.0 folder:
* is a directory
2018-07-04 20:42:55 +00:00
* contains at least one * .song file
2017-09-30 20:16:30 +00:00
:param Path import_source: Should be a Path object that fulfills the above criteria
2017-09-30 20:16:30 +00:00
:return: If the source is valid
:rtype: bool
"""
2017-09-30 20:16:30 +00:00
if import_source.is_dir():
for file_path in import_source.iterdir():
if file_path.suffix == '.song':
return True
return False
2014-03-06 20:40:08 +00:00
def do_import(self):
"""
Receive either a list of files or a folder (unicode) to import.
"""
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')
2017-12-17 04:29:53 +00:00
if isinstance(self.import_source, Path):
if self.import_source.is_dir():
2013-03-07 08:05:43 +00:00
dir = self.import_source
self.import_source = []
2017-12-17 04:29:53 +00:00
for path in dir.glob('*.song'):
self.import_source.append(path)
else:
2017-12-17 04:29:53 +00:00
self.import_source = None
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.'),
translate('SongsPlugin.PowerSongImport', 'No {text} files found.').format(text=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))
2017-12-17 04:29:53 +00:00
for file_path 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
2017-12-17 04:29:53 +00:00
with file_path.open('rb') as song_data:
2012-05-02 09:14:30 +00:00
while True:
try:
2014-03-06 22:05:15 +00:00
label = self._read_string(song_data)
if not label:
break
2014-03-06 22:05:15 +00:00
field = self._read_string(song_data)
except ValueError:
2012-05-02 09:14:30 +00:00
parse_error = True
2017-12-17 04:29:53 +00:00
self.log_error(file_path.name,
translate('SongsPlugin.PowerSongImport',
'Invalid {text} file. Unexpected byte value.').format(text=ps_string))
2012-05-02 09:14:30 +00:00
break
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':
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)
if parse_error:
continue
# Check that file had TITLE field
if not self.title:
2017-12-17 04:29:53 +00:00
self.log_error(file_path.name,
translate('SongsPlugin.PowerSongImport',
'Invalid {text} file. Missing "TITLE" header.').format(text=ps_string))
continue
# Check that file had COPYRIGHTLINE label
if not found_copyright:
self.log_error(self.title,
translate('SongsPlugin.PowerSongImport',
'Invalid {text} file. Missing "COPYRIGHTLINE" header.').format(text=ps_string))
continue
# Check that file had at least one verse
if not self.verses:
self.log_error(self.title,
translate('SongsPlugin.PowerSongImport', 'Verses not found. Missing "PART" header.'))
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)
2014-03-06 22:05:15 +00:00
def _read_string(self, file_object):
"""
Reads in next variable-length string.
"""
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')
2014-03-06 22:05:15 +00:00
def _read_7_bit_encoded_integer(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
2014-03-06 22:05:15 +00:00
byte = self._read_byte(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
2014-03-06 22:05:15 +00:00
def _read_byte(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
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