openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #28930
[Merge] lp:~tomasgroth/openlp/opspro-import into lp:openlp
Tomas Groth has proposed merging lp:~tomasgroth/openlp/opspro-import into lp:openlp.
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~tomasgroth/openlp/opspro-import/+merge/289587
Add support for importing OPS Pro song DB. Translations are supported using a {translation} tag.
Fix bug that prevents song book entries to be imported.
--
Your team OpenLP Core is requested to review the proposed merge of lp:~tomasgroth/openlp/opspro-import into lp:openlp.
=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py 2016-01-10 00:17:58 +0000
+++ openlp/plugins/songs/lib/importer.py 2016-03-20 20:33:48 +0000
@@ -78,6 +78,13 @@
HAS_WORSHIPCENTERPRO = True
except ImportError:
log.exception('Error importing %s', 'WorshipCenterProImport')
+HAS_OPSPRO = False
+if is_win():
+ try:
+ from .importers.opspro import OPSProImport
+ HAS_OPSPRO = True
+ except ImportError:
+ log.exception('Error importing %s', 'OPSProImport')
class SongFormatSelect(object):
@@ -156,20 +163,21 @@
Lyrix = 9
MediaShout = 10
OpenSong = 11
- PowerPraise = 12
- PowerSong = 13
- PresentationManager = 14
- ProPresenter = 15
- SongBeamer = 16
- SongPro = 17
- SongShowPlus = 18
- SongsOfFellowship = 19
- SundayPlus = 20
- VideoPsalm = 21
- WordsOfWorship = 22
- WorshipAssistant = 23
- WorshipCenterPro = 24
- ZionWorx = 25
+ OPSPro = 12
+ PowerPraise = 13
+ PowerSong = 14
+ PresentationManager = 15
+ ProPresenter = 16
+ SongBeamer = 17
+ SongPro = 18
+ SongShowPlus = 19
+ SongsOfFellowship = 20
+ SundayPlus = 21
+ VideoPsalm = 22
+ WordsOfWorship = 23
+ WorshipAssistant = 24
+ WorshipCenterPro = 25
+ ZionWorx = 26
# Set optional attribute defaults
__defaults__ = {
@@ -272,6 +280,17 @@
'name': WizardStrings.OS,
'prefix': 'openSong'
},
+ OPSPro: {
+ 'name': 'OPS Pro',
+ 'prefix': 'OPSPro',
+ 'canDisable': True,
+ 'selectMode': SongFormatSelect.SingleFile,
+ 'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'OPS Pro database'),
+ 'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
+ 'The OPS Pro importer is only supported on Windows. It has been '
+ 'disabled due to a missing Python module. If you want to use this '
+ 'importer, you will need to install the "pyodbc" module.')
+ },
PowerPraise: {
'class': PowerPraiseImport,
'name': 'PowerPraise',
@@ -403,6 +422,7 @@
SongFormat.Lyrix,
SongFormat.MediaShout,
SongFormat.OpenSong,
+ SongFormat.OPSPro,
SongFormat.PowerPraise,
SongFormat.PowerSong,
SongFormat.PresentationManager,
@@ -416,7 +436,7 @@
SongFormat.WordsOfWorship,
SongFormat.WorshipAssistant,
SongFormat.WorshipCenterPro,
- SongFormat.ZionWorx,
+ SongFormat.ZionWorx
])
@staticmethod
@@ -465,6 +485,9 @@
SongFormat.set(SongFormat.WorshipCenterPro, 'availability', HAS_WORSHIPCENTERPRO)
if HAS_WORSHIPCENTERPRO:
SongFormat.set(SongFormat.WorshipCenterPro, 'class', WorshipCenterProImport)
+SongFormat.set(SongFormat.OPSPro, 'availability', HAS_OPSPRO)
+if HAS_OPSPRO:
+ SongFormat.set(SongFormat.OPSPro, 'class', OPSProImport)
__all__ = ['SongFormat', 'SongFormatSelect']
=== added file 'openlp/plugins/songs/lib/importers/opspro.py'
--- openlp/plugins/songs/lib/importers/opspro.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/lib/importers/opspro.py 2016-03-20 20:33:48 +0000
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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; 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.
+"""
+import logging
+import re
+import pyodbc
+import struct
+
+from openlp.core.common 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=%s;PWD=%s' % (self.import_source,
+ password))
+ except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e:
+ log.warning('Unable to connect the OPS Pro database %s. %s', self.import_source, 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 = %d AND Type < 2 '
+ 'ORDER BY Type DESC' % song.ID)
+ lyrics = cursor.fetchone()
+ cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory '
+ 'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = %d '
+ 'ORDER BY CategoryName' % 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', '"%s" could not be imported. %s')
+ % (song.Title, 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\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('^(\d+)\r\n', verse_text)
+ if verse_number:
+ verse_text = re.sub('^\d+\r\n', '', verse_text)
+ verse_def = 'v' + verse_number.group(1)
+ # Detect verse tags
+ elif re.match('^.+?\:\r\n', verse_text):
+ tag_match = re.match('^(.+?)\:\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('^\(.*?\)$', verse_text):
+ tag_match = re.match('^\((.*?)\)$', 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('^\[slot\]\r\n', verse_text, re.IGNORECASE):
+ verse_def = 'e'
+ verse_text = re.sub('^\[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('\[splits?\]', '\r\n[---]', verse_text)
+ # Handle translations
+ if lyrics.IsDualLanguage:
+ verse_text = self.handle_translation(verse_text)
+ # Remove comments
+ verse_text = re.sub('\(.*?\)\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)
+ mdb = open(self.import_source, 'rb')
+ mdb.seek(0x14)
+ version = struct.unpack('B', mdb.read(1))[0]
+ # Get encrypted logo
+ mdb.seek(0x62)
+ EncrypFlag = struct.unpack('B', mdb.read(1))[0]
+ # Get encrypted password
+ mdb.seek(0x42)
+ encrypted_password = mdb.read(26)
+ mdb.close()
+ # "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
=== modified file 'openlp/plugins/songs/lib/importers/songimport.py'
--- openlp/plugins/songs/lib/importers/songimport.py 2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/songimport.py 2016-03-20 20:33:48 +0000
@@ -371,7 +371,7 @@
song_book = self.manager.get_object_filtered(Book, Book.name == self.song_book_name)
if song_book is None:
song_book = Book.populate(name=self.song_book_name, publisher=self.song_book_pub)
- song.book = song_book
+ song.add_songbook_entry(song_book, song.song_number)
for topic_text in self.topics:
if not topic_text:
continue
=== added file 'tests/functional/openlp_plugins/songs/test_opsproimport.py'
--- tests/functional/openlp_plugins/songs/test_opsproimport.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/songs/test_opsproimport.py 2016-03-20 20:33:48 +0000
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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; 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 #
+###############################################################################
+"""
+This module contains tests for the WorshipCenter Pro song importer.
+"""
+import os
+import json
+from unittest import TestCase, SkipTest
+
+if os.name != 'nt':
+ raise SkipTest('Not Windows, skipping test')
+
+from tests.functional import patch, MagicMock
+
+from openlp.core.common import Registry
+from openlp.plugins.songs.lib.importers.opspro import OPSProImport
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'opsprosongs'))
+
+
+class TestOpsProSongImport(TestCase):
+ """
+ Test the functions in the :mod:`opsproimport` module.
+ """
+ def setUp(self):
+ """
+ Create the registry
+ """
+ Registry.create()
+
+ @patch('openlp.plugins.songs.lib.importers.opspro.SongImport')
+ def create_importer_test(self, mocked_songimport):
+ """
+ Test creating an instance of the OPS Pro file importer
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ mocked_manager = MagicMock()
+
+ # WHEN: An importer object is created
+ importer = OPSProImport(mocked_manager, filenames=[])
+
+ # THEN: The importer object should not be None
+ self.assertIsNotNone(importer, 'Import should not be none')
+
+ @patch('openlp.plugins.songs.lib.importers.opspro.SongImport')
+ def detect_chorus_test(self, mocked_songimport):
+ """
+ Test importing lyrics with a chorus in OPS Pro
+ """
+ # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry
+ mocked_manager = MagicMock()
+ importer = OPSProImport(mocked_manager, filenames=[])
+ importer.finish = MagicMock()
+ song, lyrics = self._build_test_data('you are so faithfull.txt', False)
+
+ # WHEN: An importer object is created
+ importer.process_song(song, lyrics, [])
+
+ # THEN: The imported data should look like expected
+ result_file = open(os.path.join(TEST_PATH, 'You are so faithful.json'), 'rb')
+ result_data = json.loads(result_file.read().decode())
+ self.assertListEqual(importer.verses, self._get_data(result_data, 'verses'))
+ self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list'))
+
+ @patch('openlp.plugins.songs.lib.importers.opspro.SongImport')
+ def join_and_split_test(self, mocked_songimport):
+ """
+ Test importing lyrics with a split and join tags works in OPS Pro
+ """
+ # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry
+ mocked_manager = MagicMock()
+ importer = OPSProImport(mocked_manager, filenames=[])
+ importer.finish = MagicMock()
+ song, lyrics = self._build_test_data('amazing grace.txt', False)
+
+ # WHEN: An importer object is created
+ importer.process_song(song, lyrics, [])
+
+ # THEN: The imported data should look like expected
+ result_file = open(os.path.join(TEST_PATH, 'Amazing Grace.json'), 'rb')
+ result_data = json.loads(result_file.read().decode())
+ self.assertListEqual(importer.verses, self._get_data(result_data, 'verses'))
+ self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list'))
+
+ @patch('openlp.plugins.songs.lib.importers.opspro.SongImport')
+ def trans_off_tag_test(self, mocked_songimport):
+ """
+ Test importing lyrics with a split and join and translations tags works in OPS Pro
+ """
+ # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry
+ mocked_manager = MagicMock()
+ importer = OPSProImport(mocked_manager, filenames=[])
+ importer.finish = MagicMock()
+ song, lyrics = self._build_test_data('amazing grace2.txt', True)
+
+ # WHEN: An importer object is created
+ importer.process_song(song, lyrics, [])
+
+ # THEN: The imported data should look like expected
+ result_file = open(os.path.join(TEST_PATH, 'Amazing Grace.json'), 'rb')
+ result_data = json.loads(result_file.read().decode())
+ self.assertListEqual(importer.verses, self._get_data(result_data, 'verses'))
+ self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list'))
+
+ @patch('openlp.plugins.songs.lib.importers.opspro.SongImport')
+ def trans_tag_test(self, mocked_songimport):
+ """
+ Test importing lyrics with various translations tags works in OPS Pro
+ """
+ # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry
+ mocked_manager = MagicMock()
+ importer = OPSProImport(mocked_manager, filenames=[])
+ importer.finish = MagicMock()
+ song, lyrics = self._build_test_data('amazing grace3.txt', True)
+
+ # WHEN: An importer object is created
+ importer.process_song(song, lyrics, [])
+
+ # THEN: The imported data should look like expected
+ result_file = open(os.path.join(TEST_PATH, 'Amazing Grace3.json'), 'rb')
+ result_data = json.loads(result_file.read().decode())
+ self.assertListEqual(importer.verses, self._get_data(result_data, 'verses'))
+ self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list'))
+
+ def _get_data(self, data, key):
+ if key in data:
+ return data[key]
+ return ''
+
+ def _build_test_data(self, test_file, dual_language):
+ song = MagicMock()
+ song.ID = 100
+ song.SongNumber = 123
+ song.SongBookName = 'The Song Book'
+ song.Title = 'Song Title'
+ song.CopyrightText = 'Music and text by me'
+ song.Version = '1'
+ song.Origin = '...'
+ lyrics = MagicMock()
+ test_file = open(os.path.join(TEST_PATH, test_file), 'rb')
+ lyrics.Lyrics = test_file.read().decode()
+ lyrics.Type = 1
+ lyrics.IsDualLanguage = dual_language
+ return song, lyrics
=== modified file 'tests/functional/openlp_plugins/songs/test_videopsalm.py'
--- tests/functional/openlp_plugins/songs/test_videopsalm.py 2016-01-08 19:52:24 +0000
+++ tests/functional/openlp_plugins/songs/test_videopsalm.py 2016-03-20 20:33:48 +0000
@@ -23,11 +23,8 @@
"""
import os
-from unittest import TestCase
from tests.helpers.songfileimport import SongImportTestHelper
-from openlp.core.common import Registry
-from tests.functional import patch, MagicMock
TEST_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
=== added directory 'tests/resources/opsprosongs'
=== added file 'tests/resources/opsprosongs/Amazing Grace.json'
--- tests/resources/opsprosongs/Amazing Grace.json 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/Amazing Grace.json 2016-03-20 20:33:48 +0000
@@ -0,0 +1,21 @@
+{
+ "title": "Amazing Grace",
+ "verse_order_list": ["v1", "v2", "v3"],
+ "verses": [
+ [
+ "v1",
+ "Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\nI once was lost, but now am found;\r\nWas blind, but now I see.\r\n\r\n'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\nHow precious did that grace appear,\r\nThe hour I first believed.",
+ null
+ ],
+ [
+ "v2",
+ "The Lord has promised good to me,\r\nHis Word my hope secures.\r\nHe will my shield and portion be\r\nAs long as life endures.",
+ null
+ ],
+ [
+ "v3",
+ "Thro' many dangers, toils and snares\r\nI have already come.\r\n'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.\r\n\r\n[---]\r\nWhen we've been there ten thousand years,\r\nBright shining as the sun,\r\nWe've no less days to sing God's praise,\r\nThan when we first begun.",
+ null
+ ]
+ ]
+}
=== added file 'tests/resources/opsprosongs/Amazing Grace3.json'
--- tests/resources/opsprosongs/Amazing Grace3.json 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/Amazing Grace3.json 2016-03-20 20:33:48 +0000
@@ -0,0 +1,31 @@
+{
+ "title": "Amazing Grace",
+ "verse_order_list": ["v1", "v2", "v3", "v4", "v5"],
+ "verses": [
+ [
+ "v1",
+ "Amazing grace! How sweet the sound!\r\n{translation}That saved a wretch like me!{/translation}\r\nI once was lost, but now am found;\r\n{translation}Was blind, but now I see.{/translation}",
+ null
+ ],
+ [
+ "v2",
+ "'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\n{translation}How precious did that grace appear,\r\nThe hour I first believed.\r\n{/translation}",
+ null
+ ],
+ [
+ "v3",
+ "The Lord has promised good to me,\r\nHis Word my hope secures.\r\nHe will my shield and portion be\r\n{translation}As long as life endures.{/translation}",
+ null
+ ],
+ [
+ "v4",
+ "Thro' many dangers, toils and snares\r\nI have already come.\r\n'Tis grace that brought me safe thus far,\r\n{translation}And grace will lead me home.{/translation}",
+ null
+ ],
+ [
+ "v5",
+ "[end]\r\n{translation}When we've been there ten thousand years,{/translation}\r\nBright shining as the sun,\r\n{translation}We've no less days to sing God's praise,{/translation}\r\nThan when we first begun.",
+ null
+ ]
+ ]
+}
=== added file 'tests/resources/opsprosongs/You are so faithful.json'
--- tests/resources/opsprosongs/You are so faithful.json 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/You are so faithful.json 2016-03-20 20:33:48 +0000
@@ -0,0 +1,31 @@
+{
+ "title": "You are so faithful",
+ "verse_order_list": ["v1", "c1", "v2", "c1", "v3", "c1", "v4"],
+ "verses": [
+ [
+ "v1",
+ "You are so faithful\r\nso faithful, so faithful.\r\nYou are so faithful\r\nso faithful, so faithful.",
+ null
+ ],
+ [
+ "c1",
+ "That's why I praise you\r\nin the morning\r\nThat's why I praise you\r\nin the noontime.\r\nThat's why I praise you\r\nin the evening\r\nThat's why I praise you\r\nall the time.",
+ null
+ ],
+ [
+ "v2",
+ "You are so loving\r\nso loving, so loving.\r\nYou are so loving\r\nso loving, so loving.",
+ null
+ ],
+ [
+ "v3",
+ "You are so caring\r\nso caring, so caring.\r\nYou are so caring\r\nso caring, so caring.",
+ null
+ ],
+ [
+ "v4",
+ "You are so mighty\r\nso mighty, so mighty.\r\nYou are so mighty\r\nso mighty, so mighty.",
+ null
+ ]
+ ]
+}
=== added file 'tests/resources/opsprosongs/amazing grace.txt'
--- tests/resources/opsprosongs/amazing grace.txt 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/amazing grace.txt 2016-03-20 20:33:48 +0000
@@ -0,0 +1,24 @@
+Amazing grace! How sweet the sound!
+That saved a wretch like me!
+I once was lost, but now am found;
+Was blind, but now I see.
+[join]
+'Twas grace that taught my heart to fear,
+And grace my fears relieved.
+How precious did that grace appear,
+The hour I first believed.
+
+The Lord has promised good to me,
+His Word my hope secures.
+He will my shield and portion be
+As long as life endures.
+
+Thro' many dangers, toils and snares
+I have already come.
+'Tis grace that brought me safe thus far,
+And grace will lead me home.
+[split]
+When we've been there ten thousand years,
+Bright shining as the sun,
+We've no less days to sing God's praise,
+Than when we first begun.
=== added file 'tests/resources/opsprosongs/amazing grace2.txt'
--- tests/resources/opsprosongs/amazing grace2.txt 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/amazing grace2.txt 2016-03-20 20:33:48 +0000
@@ -0,0 +1,29 @@
+[trans off]
+Amazing grace! How sweet the sound!
+That saved a wretch like me!
+I once was lost, but now am found;
+Was blind, but now I see.
+[join]
+[trans off]
+'Twas grace that taught my heart to fear,
+And grace my fears relieved.
+How precious did that grace appear,
+The hour I first believed.
+
+[trans off]
+The Lord has promised good to me,
+His Word my hope secures.
+He will my shield and portion be
+As long as life endures.
+
+[trans off]
+Thro' many dangers, toils and snares
+I have already come.
+'Tis grace that brought me safe thus far,
+And grace will lead me home.
+[trans off]
+[split]
+When we've been there ten thousand years,
+Bright shining as the sun,
+We've no less days to sing God's praise,
+Than when we first begun.
=== added file 'tests/resources/opsprosongs/amazing grace3.txt'
--- tests/resources/opsprosongs/amazing grace3.txt 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/amazing grace3.txt 2016-03-20 20:33:48 +0000
@@ -0,0 +1,31 @@
+Amazing grace! How sweet the sound!
+That saved a wretch like me!
+I once was lost, but now am found;
+Was blind, but now I see.
+
+[taal a]
+'Twas grace that taught my heart to fear,
+And grace my fears relieved.
+[taal b]
+How precious did that grace appear,
+The hour I first believed.
+
+[trans off]
+The Lord has promised good to me,
+His Word my hope secures.
+[trans on]
+He will my shield and portion be
+As long as life endures.
+
+[vertaal uit]
+Thro' many dangers, toils and snares
+I have already come.
+[vertaal aan]
+'Tis grace that brought me safe thus far,
+And grace will lead me home.
+
+[end]
+When we've been there ten thousand years,
+Bright shining as the sun,
+We've no less days to sing God's praise,
+Than when we first begun.
=== added file 'tests/resources/opsprosongs/you are so faithfull.txt'
--- tests/resources/opsprosongs/you are so faithfull.txt 1970-01-01 00:00:00 +0000
+++ tests/resources/opsprosongs/you are so faithfull.txt 2016-03-20 20:33:48 +0000
@@ -0,0 +1,37 @@
+1
+You are so faithful
+so faithful, so faithful.
+You are so faithful
+so faithful, so faithful.
+
+Refrein:
+That's why I praise you
+in the morning
+That's why I praise you
+in the noontime.
+That's why I praise you
+in the evening
+That's why I praise you
+all the time.
+
+2
+You are so loving
+so loving, so loving.
+You are so loving
+so loving, so loving.
+
+(refrein)
+
+3
+You are so caring
+so caring, so caring.
+You are so caring
+so caring, so caring.
+
+(refrein)
+
+4
+You are so mighty
+so mighty, so mighty.
+You are so mighty
+so mighty, so mighty.
References