forked from openlp/openlp
266 lines
12 KiB
Python
266 lines
12 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:`opspro` module provides the functionality for importing
|
|
a OPS Pro database into the OpenLP database.
|
|
"""
|
|
|
|
# WARNING: See https://docs.python.org/2/library/sqlite3.html for value substitution
|
|
# in SQL statements
|
|
|
|
import logging
|
|
import re
|
|
import struct
|
|
|
|
import pyodbc
|
|
|
|
from openlp.core.common.i18n import translate
|
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class OPSProImport(SongImport):
|
|
"""
|
|
The :class:`OPSProImport` class provides the ability to import the
|
|
WorshipCenter Pro Access Database
|
|
"""
|
|
def __init__(self, manager, **kwargs):
|
|
"""
|
|
Initialise the WorshipCenter Pro importer.
|
|
"""
|
|
super(OPSProImport, self).__init__(manager, **kwargs)
|
|
|
|
def do_import(self):
|
|
"""
|
|
Receive a single file to import.
|
|
"""
|
|
password = self.extract_mdb_password()
|
|
try:
|
|
conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};'
|
|
'PWD={password}'.format(source=self.import_source, password=password))
|
|
except Exception as e:
|
|
log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source,
|
|
error=str(e)))
|
|
# Unfortunately no specific exception type
|
|
self.log_error(self.import_source, translate('SongsPlugin.OPSProImport',
|
|
'Unable to connect the OPS Pro database.'))
|
|
return
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT Song.ID, SongNumber, SongBookName, Title, CopyrightText, Version, Origin FROM Song '
|
|
'LEFT JOIN SongBook ON Song.SongBookID = SongBook.ID ORDER BY Title')
|
|
songs = cursor.fetchall()
|
|
self.import_wizard.progress_bar.setMaximum(len(songs))
|
|
for song in songs:
|
|
if self.stop_import_flag:
|
|
break
|
|
# Type means: 0=Original, 1=Projection, 2=Own
|
|
cursor.execute('SELECT Lyrics, Type, IsDualLanguage FROM Lyrics WHERE SongID = ? AND Type < 2 '
|
|
'ORDER BY Type DESC', float(song.ID))
|
|
lyrics = cursor.fetchone()
|
|
cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory '
|
|
'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = ? '
|
|
'ORDER BY CategoryName', float(song.ID))
|
|
topics = cursor.fetchall()
|
|
try:
|
|
self.process_song(song, lyrics, topics)
|
|
except Exception as e:
|
|
self.log_error(self.import_source,
|
|
translate('SongsPlugin.OPSProImport',
|
|
'"{title}" could not be imported. {error}').format(title=song.Title, error=e))
|
|
|
|
def process_song(self, song, lyrics, topics):
|
|
"""
|
|
Create the song, i.e. title, verse etc.
|
|
|
|
The OPS Pro format is a fairly simple text format using tags and anchors/labels. Linebreaks are \r\n.
|
|
Double linebreaks are slide dividers. OPS Pro support dual language using tags.
|
|
Tags are in [], see the liste below:
|
|
[join] are used to separate verses that should be keept on the same slide.
|
|
[split] or [splits] can be used to split a verse over several slides, while still being the same verse
|
|
Dual language tags:
|
|
[trans off] or [vertaal uit] turns dual language mode off for the following text
|
|
[trans on] or [vertaal aan] turns dual language mode on for the following text
|
|
[taal a] means the following lines are language a
|
|
[taal b] means the following lines are language b
|
|
"""
|
|
self.set_defaults()
|
|
self.title = song.Title
|
|
if song.CopyrightText:
|
|
for line in song.CopyrightText.splitlines():
|
|
if line.startswith('©') or line.lower().startswith('copyright'):
|
|
self.add_copyright(line)
|
|
else:
|
|
self.parse_author(line)
|
|
if song.Origin:
|
|
self.comments = song.Origin
|
|
if song.SongBookName:
|
|
self.song_book_name = song.SongBookName
|
|
if song.SongNumber:
|
|
self.song_number = song.SongNumber
|
|
for topic in topics:
|
|
self.topics.append(topic.CategoryName)
|
|
# Try to split lyrics based on various rules
|
|
if lyrics:
|
|
lyrics_text = lyrics.Lyrics
|
|
verses = re.split(r'\r\n\s*?\r\n', lyrics_text)
|
|
verse_tag_defs = {}
|
|
verse_tag_texts = {}
|
|
for verse_text in verses:
|
|
if verse_text.strip() == '':
|
|
continue
|
|
verse_def = 'v'
|
|
# Detect verse number
|
|
verse_number = re.match(r'^(\d+)\r\n', verse_text)
|
|
if verse_number:
|
|
verse_text = re.sub(r'^\d+\r\n', '', verse_text)
|
|
verse_def = 'v' + verse_number.group(1)
|
|
# Detect verse tags
|
|
elif re.match(r'^.+?:\r\n', verse_text):
|
|
tag_match = re.match(r'^(.+?):\r\n(.*)', verse_text, flags=re.DOTALL)
|
|
tag = tag_match.group(1).lower()
|
|
tag = tag.split(' ')[0]
|
|
verse_text = tag_match.group(2)
|
|
if 'refrein' in tag or 'chorus' in tag:
|
|
verse_def = 'c'
|
|
elif 'bridge' in tag:
|
|
verse_def = 'b'
|
|
verse_tag_defs[tag] = verse_def
|
|
verse_tag_texts[tag] = verse_text
|
|
# Detect tag reference
|
|
elif re.match(r'^\(.*?\)$', verse_text):
|
|
tag_match = re.match(r'^\((.*?)\)$', verse_text)
|
|
tag = tag_match.group(1).lower()
|
|
if tag in verse_tag_defs:
|
|
verse_text = verse_tag_texts[tag]
|
|
verse_def = verse_tag_defs[tag]
|
|
# Detect end tag
|
|
elif re.match(r'^\[slot\]\r\n', verse_text, re.IGNORECASE):
|
|
verse_def = 'e'
|
|
verse_text = re.sub(r'^\[slot\]\r\n', '', verse_text, flags=re.IGNORECASE)
|
|
# Replace the join tag with line breaks
|
|
verse_text = verse_text.replace('[join]', '')
|
|
# Replace the split tag with line breaks and an optional split
|
|
verse_text = re.sub(r'\[splits?\]', '\r\n[---]', verse_text)
|
|
# Handle translations
|
|
if lyrics.IsDualLanguage:
|
|
verse_text = self.handle_translation(verse_text)
|
|
# Remove comments
|
|
verse_text = re.sub(r'\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE)
|
|
self.add_verse(verse_text, verse_def)
|
|
self.finish()
|
|
|
|
def handle_translation(self, verse_text):
|
|
"""
|
|
Replace OPS Pro translation tags with a {translation} tag
|
|
|
|
:param verse_text: the verse text
|
|
:return: the verse text with replaced tags
|
|
"""
|
|
language = None
|
|
translation = True
|
|
translation_verse_text = ''
|
|
start_tag = '{translation}'
|
|
end_tag = '{/translation}'
|
|
verse_text_lines = verse_text.splitlines()
|
|
idx = 0
|
|
while idx < len(verse_text_lines):
|
|
# Detect if translation is turned on or off
|
|
if verse_text_lines[idx] in ['[trans off]', '[vertaal uit]']:
|
|
translation = False
|
|
idx += 1
|
|
elif verse_text_lines[idx] in ['[trans on]', '[vertaal aan]']:
|
|
translation = True
|
|
idx += 1
|
|
elif verse_text_lines[idx] == '[taal a]':
|
|
language = 'a'
|
|
idx += 1
|
|
elif verse_text_lines[idx] == '[taal b]':
|
|
language = 'b'
|
|
idx += 1
|
|
if not idx < len(verse_text_lines):
|
|
break
|
|
# Handle the text based on whether translation is off or on
|
|
if language:
|
|
if language == 'b':
|
|
translation_verse_text += start_tag
|
|
while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['):
|
|
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
|
idx += 1
|
|
if language == 'b':
|
|
translation_verse_text += end_tag
|
|
language = None
|
|
elif translation:
|
|
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
|
idx += 1
|
|
if idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['):
|
|
translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n'
|
|
idx += 1
|
|
else:
|
|
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
|
idx += 1
|
|
while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['):
|
|
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
|
idx += 1
|
|
return translation_verse_text
|
|
|
|
def extract_mdb_password(self):
|
|
"""
|
|
Extract password from mdb. Based on code from
|
|
http://tutorialsto.com/database/access/crack-access-*.-mdb-all-current-versions-of-the-password.html
|
|
"""
|
|
# The definition of 13 bytes as the source XOR Access2000. Encrypted with the corresponding signs are 0x13
|
|
xor_pattern_2k = (0xa1, 0xec, 0x7a, 0x9c, 0xe1, 0x28, 0x34, 0x8a, 0x73, 0x7b, 0xd2, 0xdf, 0x50)
|
|
# Access97 XOR of the source
|
|
xor_pattern_97 = (0x86, 0xfb, 0xec, 0x37, 0x5d, 0x44, 0x9c, 0xfa, 0xc6, 0x5e, 0x28, 0xe6, 0x13)
|
|
with self.import_source.open('rb') as mdb_file:
|
|
mdb_file.seek(0x14)
|
|
version = struct.unpack('B', mdb_file.read(1))[0]
|
|
# Get encrypted logo
|
|
mdb_file.seek(0x62)
|
|
EncrypFlag = struct.unpack('B', mdb_file.read(1))[0]
|
|
# Get encrypted password
|
|
mdb_file.seek(0x42)
|
|
encrypted_password = mdb_file.read(26)
|
|
# "Decrypt" the password based on the version
|
|
decrypted_password = ''
|
|
if version < 0x01:
|
|
# Access 97
|
|
if int(encrypted_password[0] ^ xor_pattern_97[0]) == 0:
|
|
# No password
|
|
decrypted_password = ''
|
|
else:
|
|
for j in range(0, 12):
|
|
decrypted_password = decrypted_password + chr(encrypted_password[j] ^ xor_pattern_97[j])
|
|
else:
|
|
# Access 2000 or 2002
|
|
for j in range(0, 12):
|
|
if j % 2 == 0:
|
|
# Every byte with a different sign or encrypt. Encryption signs here for the 0x13
|
|
t1 = chr(0x13 ^ EncrypFlag ^ encrypted_password[j * 2] ^ xor_pattern_2k[j])
|
|
else:
|
|
t1 = chr(encrypted_password[j * 2] ^ xor_pattern_2k[j])
|
|
decrypted_password = decrypted_password + t1
|
|
if ord(decrypted_password[1]) < 0x20 or ord(decrypted_password[1]) > 0x7e:
|
|
decrypted_password = ''
|
|
return decrypted_password
|