forked from openlp/openlp
227 lines
9.4 KiB
Python
227 lines
9.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2022 OpenLP Developers #
|
|
# ---------------------------------------------------------------------- #
|
|
# 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/>. #
|
|
##########################################################################
|
|
"""
|
|
The :mod:`wordsofworship` module provides the functionality for importing Words of
|
|
Worship songs into the OpenLP database.
|
|
"""
|
|
import logging
|
|
import os
|
|
|
|
from openlp.core.common.i18n import UiStrings, translate
|
|
from openlp.core.lib import DataType, read_int, read_or_fail, seek_or_fail
|
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
|
|
|
|
|
BLOCK_TYPES = ('V', 'C', 'B')
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class WordsOfWorshipImport(SongImport):
|
|
"""
|
|
The :class:`WordsOfWorshipImport` class provides the ability to import song files from Words of Worship.
|
|
|
|
**Words Of Worship Song File Format:**
|
|
|
|
The Words Of Worship song file format is as follows:
|
|
|
|
* The song title is the file name minus the extension.
|
|
* The song has a header, a number of blocks, followed by footer containing
|
|
the author and the copyright.
|
|
* A block can be a verse, chorus or bridge.
|
|
|
|
Little endian is used.
|
|
|
|
File Header:
|
|
Bytes are counted from one, i.e. the first byte is byte 1.
|
|
|
|
0x00 - 0x13 Should be "WoW File \nSong Words\n"
|
|
0x14 - 0x1F Minimum version of Words Of Worship required to open this file
|
|
0x20 - 0x2B Minimum version of Words Of Worship required to save this file without data loss
|
|
0x2C - 0x37 The version of Words of Worship that this file is from. From test data, it looks like this might be
|
|
the version that originally created this file, not the last version to save it.
|
|
|
|
The Words Of Worship versioning system seems to be in the format:
|
|
``Major.Minor.Patch``
|
|
|
|
Where each part of the version number is stored by a 32-bit int
|
|
|
|
0x38 - 0x3B Specifies how many blocks there are.
|
|
|
|
0x42 - 0x51 Should be "CSongDoc::CBlock"
|
|
|
|
0x52 The first song blocks start from here.
|
|
|
|
Blocks:
|
|
Each block starts with a 32-bit int which specifies how many lines are in that block.
|
|
|
|
Then there are a number of lines corresponding to the value above.
|
|
|
|
Each block ends with a 32 bit number, which defines what type of
|
|
block it is:
|
|
|
|
* 0x00000000 = Verse
|
|
* 0x01000000 = Chorus
|
|
* 0x02000000 = Bridge
|
|
|
|
Blocks are separated by two bytes. The first byte is 0x01, and the
|
|
second byte is 0x80.
|
|
|
|
Lines:
|
|
Each line consists of a "Pascal" string.
|
|
In later versions, a byte follows which denotes the formatting of the line:
|
|
|
|
* 0x00 = Normal
|
|
* 0x01 = Minor
|
|
|
|
It looks like this may have been introduced in Words of Worship song version 2.1.0, though this is an educated
|
|
guess.
|
|
|
|
Footer:
|
|
The footer follows on after the last block. Its format is as follows:
|
|
|
|
Author String (as a 'Pascal' string)
|
|
Copyright String (as a 'Pascal' string)
|
|
|
|
Finally in newer versions of Word Of Worship song files there is a 32 bit int describing the copyright.
|
|
|
|
0x00000000 = Covered by CCL
|
|
0x01000000 = Authors explicit permission
|
|
0x02000000 = Public Domain
|
|
0x03000000 = Copyright expired
|
|
0x04000000 = Other
|
|
|
|
Pascal Strings:
|
|
Strings are preceded by a variable length integer which specifies how many bytes are in the string. An example
|
|
of the variable length integer is below.
|
|
|
|
Lentgh bytes 'Little'| Str len
|
|
-------------------------------
|
|
01 | 01
|
|
02 | 02
|
|
.... |
|
|
FD | FD
|
|
FE | FE
|
|
FF FF 00 | FF
|
|
FF 00 01 | 01 00
|
|
FF 01 01 | 01 01
|
|
FF 02 01 | 01 02
|
|
.... |
|
|
FF FC FF | FF FC
|
|
FF FD FF | FF FD
|
|
FF FF FF FE FF | FF FE
|
|
FF FF FF FF FF 00 00 | FF FF
|
|
FF FF FF 00 00 01 00 | 01 00 00
|
|
FF FF FF 01 00 01 00 | 01 00 01
|
|
FF FF FF 02 00 02 00 | 01 00 02
|
|
|
|
Valid extensions for a Words of Worship song file are:
|
|
|
|
* .wsg
|
|
* .wow-song
|
|
"""
|
|
@staticmethod
|
|
def parse_string(song_data):
|
|
length_bytes = song_data.read(DataType.U8)
|
|
if length_bytes == b'\xff':
|
|
length_bytes = song_data.read(DataType.U16)
|
|
length = int.from_bytes(length_bytes, 'little')
|
|
return read_or_fail(song_data, length).decode('cp1252')
|
|
|
|
def parse_lines(self, song_data):
|
|
lines = []
|
|
lines_to_read = read_int(song_data, DataType.U32, 'little')
|
|
for line_no in range(0, lines_to_read):
|
|
line_text = self.parse_string(song_data)
|
|
if self.read_version >= (2, 1, 0):
|
|
if read_or_fail(song_data, DataType.U8) == b'\x01':
|
|
line_text = '{{minor}}{text}{{/minor}}'.format(text=line_text)
|
|
lines.append(line_text)
|
|
return '\n'.join(lines)
|
|
|
|
@staticmethod
|
|
def parse_version(song_data):
|
|
return (read_int(song_data, DataType.U32, 'little'),
|
|
read_int(song_data, DataType.U32, 'little'),
|
|
read_int(song_data, DataType.U32, 'little'))
|
|
|
|
def vaildate(self, file_path, song_data):
|
|
seek_or_fail(song_data, 0x00)
|
|
err_text = b''
|
|
data = read_or_fail(song_data, 20)
|
|
if data != b'WoW File\nSong Words\n':
|
|
err_text = data
|
|
seek_or_fail(song_data, 0x42)
|
|
data = read_or_fail(song_data, 16)
|
|
if data != b'CSongDoc::CBlock':
|
|
err_text = data
|
|
if err_text:
|
|
self.log_error(file_path,
|
|
translate('SongsPlugin.WordsofWorshipSongImport',
|
|
'Invalid Words of Worship song file. Missing {text!r} header.'
|
|
).format(text=err_text))
|
|
return False
|
|
return True
|
|
|
|
def do_import(self):
|
|
"""
|
|
Receive a single file or a list of files to import.
|
|
"""
|
|
if isinstance(self.import_source, list):
|
|
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
|
|
for file_path in self.import_source:
|
|
if self.stop_import_flag:
|
|
return
|
|
log.debug('Importing %s', file_path)
|
|
try:
|
|
self.set_defaults()
|
|
# Get the song title
|
|
self.title = file_path.stem
|
|
with file_path.open('rb') as song_data:
|
|
if not self.vaildate(file_path, song_data):
|
|
continue
|
|
seek_or_fail(song_data, 20)
|
|
self.read_version = self.parse_version(song_data)
|
|
# Seek to byte which stores number of blocks in the song
|
|
seek_or_fail(song_data, 56)
|
|
no_of_blocks = read_int(song_data, DataType.U8)
|
|
|
|
# Seek to the beginning of the first block
|
|
seek_or_fail(song_data, 82)
|
|
for block_no in range(no_of_blocks):
|
|
# Blocks are separated by 2 bytes, skip them, but not if this is the last block!
|
|
if block_no != 0:
|
|
seek_or_fail(song_data, 2, os.SEEK_CUR)
|
|
text = self.parse_lines(song_data)
|
|
block_type = BLOCK_TYPES[read_int(song_data, DataType.U32, 'little')]
|
|
self.add_verse(text, block_type)
|
|
|
|
# Now to extract the author
|
|
self.parse_author(self.parse_string(song_data))
|
|
# Finally the copyright
|
|
self.add_copyright(self.parse_string(song_data))
|
|
if not self.finish():
|
|
self.log_error(file_path)
|
|
except IndexError:
|
|
self.log_error(file_path, UiStrings().FileCorrupt)
|
|
except Exception as e:
|
|
self.log_error(file_path, e)
|