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

266 lines
13 KiB
Python
Raw Normal View History

2016-02-29 21:35:53 +00:00
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
2017-12-29 09:15:48 +00:00
# Copyright (c) 2008-2018 OpenLP Developers #
2016-02-29 21:35:53 +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 #
###############################################################################
"""
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
2016-02-29 21:35:53 +00:00
import logging
import re
2016-03-07 22:27:28 +00:00
import struct
2016-02-29 21:35:53 +00:00
2017-12-28 08:08:12 +00:00
import pyodbc
2017-10-07 07:05:07 +00:00
from openlp.core.common.i18n import translate
2016-02-29 21:35:53 +00:00
from openlp.plugins.songs.lib.importers.songimport import SongImport
log = logging.getLogger(__name__)
2016-03-20 08:28:41 +00:00
class OPSProImport(SongImport):
2016-02-29 21:35:53 +00:00
"""
2016-03-20 08:28:41 +00:00
The :class:`OPSProImport` class provides the ability to import the
2016-02-29 21:35:53 +00:00
WorshipCenter Pro Access Database
"""
def __init__(self, manager, **kwargs):
"""
Initialise the WorshipCenter Pro importer.
"""
2016-03-20 08:28:41 +00:00
super(OPSProImport, self).__init__(manager, **kwargs)
2016-02-29 21:35:53 +00:00
def do_import(self):
"""
Receive a single file to import.
"""
2016-03-07 22:27:28 +00:00
password = self.extract_mdb_password()
2016-02-29 21:35:53 +00:00
try:
conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};'
'PWD={password}'.format(source=self.import_source, password=password))
2016-02-29 21:35:53 +00:00
except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e:
log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source,
error=str(e)))
2016-02-29 21:35:53 +00:00
# Unfortunately no specific exception type
2016-03-20 08:28:41 +00:00
self.log_error(self.import_source, translate('SongsPlugin.OPSProImport',
2016-02-29 21:35:53 +00:00
'Unable to connect the OPS Pro database.'))
return
cursor = conn.cursor()
2016-03-07 22:27:28 +00:00
cursor.execute('SELECT Song.ID, SongNumber, SongBookName, Title, CopyrightText, Version, Origin FROM Song '
'LEFT JOIN SongBook ON Song.SongBookID = SongBook.ID ORDER BY Title')
2016-02-29 21:35:53 +00:00
songs = cursor.fetchall()
self.import_wizard.progress_bar.setMaximum(len(songs))
for song in songs:
if self.stop_import_flag:
break
2016-03-20 20:23:01 +00:00
# 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))
2016-03-07 22:27:28 +00:00
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))
2016-02-29 21:35:53 +00:00
topics = cursor.fetchall()
2016-03-20 20:23:01 +00:00
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))
2016-02-29 21:35:53 +00:00
2016-03-07 22:27:28 +00:00
def process_song(self, song, lyrics, topics):
2016-02-29 21:35:53 +00:00
"""
Create the song, i.e. title, verse etc.
2016-03-09 21:44:15 +00:00
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
2016-02-29 21:35:53 +00:00
"""
self.set_defaults()
self.title = song.Title
2016-03-07 22:27:28 +00:00
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)
2016-03-07 22:27:28 +00:00
if song.Origin:
self.comments = song.Origin
if song.SongBookName:
self.song_book_name = song.SongBookName
if song.SongNumber:
self.song_number = song.SongNumber
2016-02-29 21:35:53 +00:00
for topic in topics:
2016-03-07 22:27:28 +00:00
self.topics.append(topic.CategoryName)
# Try to split lyrics based on various rules
if lyrics:
lyrics_text = lyrics.Lyrics
2018-07-02 20:38:47 +00:00
verses = re.split(r'\r\n\s*?\r\n', lyrics_text)
2016-03-07 22:27:28 +00:00
verse_tag_defs = {}
verse_tag_texts = {}
for verse_text in verses:
if verse_text.strip() == '':
continue
2016-03-07 22:27:28 +00:00
verse_def = 'v'
2016-03-18 22:09:49 +00:00
# Detect verse number
2018-07-02 20:38:47 +00:00
verse_number = re.match(r'^(\d+)\r\n', verse_text)
2016-03-07 22:27:28 +00:00
if verse_number:
2018-07-02 20:38:47 +00:00
verse_text = re.sub(r'^\d+\r\n', '', verse_text)
2016-03-07 22:27:28 +00:00
verse_def = 'v' + verse_number.group(1)
# Detect verse tags
2018-07-02 20:38:47 +00:00
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)
2016-03-16 21:28:29 +00:00
if 'refrein' in tag or 'chorus' in tag:
2016-03-07 22:27:28 +00:00
verse_def = 'c'
elif 'bridge' in tag:
2016-03-07 22:27:28 +00:00
verse_def = 'b'
verse_tag_defs[tag] = verse_def
verse_tag_texts[tag] = verse_text
# Detect tag reference
2018-07-02 20:38:47 +00:00
elif re.match(r'^\(.*?\)$', verse_text):
tag_match = re.match(r'^\((.*?)\)$', verse_text)
tag = tag_match.group(1).lower()
2016-03-07 22:27:28 +00:00
if tag in verse_tag_defs:
verse_text = verse_tag_texts[tag]
verse_def = verse_tag_defs[tag]
2016-03-18 22:09:49 +00:00
# Detect end tag
2018-07-02 20:38:47 +00:00
elif re.match(r'^\[slot\]\r\n', verse_text, re.IGNORECASE):
2016-03-07 22:27:28 +00:00
verse_def = 'e'
2018-07-02 20:38:47 +00:00
verse_text = re.sub(r'^\[slot\]\r\n', '', verse_text, flags=re.IGNORECASE)
2016-03-07 22:27:28 +00:00
# Replace the join tag with line breaks
2016-03-20 20:23:01 +00:00
verse_text = verse_text.replace('[join]', '')
2016-03-07 22:27:28 +00:00
# Replace the split tag with line breaks and an optional split
2018-07-02 20:38:47 +00:00
verse_text = re.sub(r'\[splits?\]', '\r\n[---]', verse_text)
2016-03-07 22:27:28 +00:00
# Handle translations
2016-03-18 22:09:49 +00:00
if lyrics.IsDualLanguage:
verse_text = self.handle_translation(verse_text)
2016-03-07 22:27:28 +00:00
# Remove comments
2018-07-02 20:38:47 +00:00
verse_text = re.sub(r'\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE)
2016-03-07 22:27:28 +00:00
self.add_verse(verse_text, verse_def)
2016-02-29 21:35:53 +00:00
self.finish()
2016-03-07 22:27:28 +00:00
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
2016-03-20 20:23:01 +00:00
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
2016-03-07 22:27:28 +00:00
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)
2016-03-07 22:27:28 +00:00
# Access97 XOR of the source
xor_pattern_97 = (0x86, 0xfb, 0xec, 0x37, 0x5d, 0x44, 0x9c, 0xfa, 0xc6, 0x5e, 0x28, 0xe6, 0x13)
2017-09-30 20:16:30 +00:00
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)
2016-03-07 22:27:28 +00:00
# "Decrypt" the password based on the version
decrypted_password = ''
if version < 0x01:
# Access 97
if int(encrypted_password[0] ^ xor_pattern_97[0]) == 0:
2016-03-07 22:27:28 +00:00
# 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:
2016-03-07 22:27:28 +00:00
# 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])
2016-03-07 22:27:28 +00:00
else:
t1 = chr(encrypted_password[j * 2] ^ xor_pattern_2k[j])
decrypted_password = decrypted_password + t1
2016-03-07 22:27:28 +00:00
if ord(decrypted_password[1]) < 0x20 or ord(decrypted_password[1]) > 0x7e:
decrypted_password = ''
return decrypted_password