From 1e8f72d49f87b2e004574d57839826935b4efcef Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 7 Oct 2019 17:12:38 +0000 Subject: [PATCH] LiveWorship importer --- openlp/core/common/__init__.py | 10 + openlp/plugins/songs/lib/importer.py | 49 ++- .../songs/lib/importers/liveworship.py | 287 ++++++++++++++ .../songs/test_liveworshipimport.py | 93 +++++ .../liveworship/A Child Of The King.json | 35 ++ .../valentina-db-simplified-dump.xml | 362 ++++++++++++++++++ 6 files changed, 817 insertions(+), 19 deletions(-) create mode 100644 openlp/plugins/songs/lib/importers/liveworship.py create mode 100644 tests/functional/openlp_plugins/songs/test_liveworshipimport.py create mode 100644 tests/resources/songs/liveworship/A Child Of The King.json create mode 100644 tests/resources/songs/liveworship/valentina-db-simplified-dump.xml diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 1f1ddb1d1..bbc9dfc65 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -222,6 +222,16 @@ def is_linux(): return sys.platform.startswith('linux') +def is_64bit_instance(): + """ + Returns true if the python/OpenLP instance running is 64 bit. If running a 32 bit instance on + a 64 bit system this will return false. + + :return: True if the python/OpenLP instance running is 64 bit, otherwise False. + """ + return (sys.maxsize > 2**32) + + def verify_ipv4(addr): """ Validate an IPv4 address diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 2603f2f23..962d17cba 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -34,6 +34,7 @@ from .importers.dreambeam import DreamBeamImport from .importers.easyslides import EasySlidesImport from .importers.easyworship import EasyWorshipSongImport from .importers.foilpresenter import FoilPresenterImport +from .importers.liveworship import LiveWorshipImport from .importers.lyrix import LyrixImport from .importers.openlp import OpenLPSongImport from .importers.openlyrics import OpenLyricsImport @@ -166,25 +167,26 @@ class SongFormat(object): EasyWorshipSqliteDB = 8 EasyWorshipService = 9 FoilPresenter = 10 - Lyrix = 11 - MediaShout = 12 - OpenSong = 13 - OPSPro = 14 - PowerPraise = 15 - PowerSong = 16 - PresentationManager = 17 - ProPresenter = 18 - SingingTheFaith = 19 - SongBeamer = 20 - SongPro = 21 - SongShowPlus = 22 - SongsOfFellowship = 23 - SundayPlus = 24 - VideoPsalm = 25 - WordsOfWorship = 26 - WorshipAssistant = 27 - WorshipCenterPro = 28 - ZionWorx = 29 + LiveWorship = 11 + Lyrix = 12 + MediaShout = 13 + OpenSong = 14 + OPSPro = 15 + PowerPraise = 16 + PowerSong = 17 + PresentationManager = 18 + ProPresenter = 19 + SingingTheFaith = 20 + SongBeamer = 21 + SongPro = 22 + SongShowPlus = 23 + SongsOfFellowship = 24 + SundayPlus = 25 + VideoPsalm = 26 + WordsOfWorship = 27 + WorshipAssistant = 28 + WorshipCenterPro = 29 + ZionWorx = 30 # Set optional attribute defaults __defaults__ = { @@ -282,6 +284,14 @@ class SongFormat(object): 'filter': '{text} (*.foil)'.format(text=translate('SongsPlugin.ImportWizardForm', 'Foilpresenter Song Files')) }, + LiveWorship: { + 'class': LiveWorshipImport, + 'name': 'LiveWorship Database', + 'prefix': 'liveWorship', + 'selectMode': SongFormatSelect.SingleFile, + 'filter': '{text} (*.vdb)'.format(text=translate('SongsPlugin.ImportWizardForm', + 'LiveWorship Database')) + }, Lyrix: { 'class': LyrixImport, 'name': 'LyriX', @@ -466,6 +476,7 @@ class SongFormat(object): SongFormat.EasyWorshipSqliteDB, SongFormat.EasyWorshipService, SongFormat.FoilPresenter, + SongFormat.LiveWorship, SongFormat.Lyrix, SongFormat.MediaShout, SongFormat.OpenSong, diff --git a/openlp/plugins/songs/lib/importers/liveworship.py b/openlp/plugins/songs/lib/importers/liveworship.py new file mode 100644 index 000000000..23c2dead6 --- /dev/null +++ b/openlp/plugins/songs/lib/importers/liveworship.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 . # +########################################################################## +""" +The :mod:`liveworship` module provides the functionality for importing +a LiveWorship database into the OpenLP database. +""" +import os +import logging +import ctypes + +from lxml import etree +from pathlib import Path +from tempfile import gettempdir + +from openlp.core.common import is_win, is_linux, is_macosx, is_64bit_instance, delete_file +from openlp.core.common.i18n import translate +from openlp.core.lib.ui import critical_error_message_box +from openlp.plugins.songs.lib.importers.songimport import SongImport +from openlp.plugins.songs.lib.ui import SongStrings + +# Copied from VCSDK_Enums.h +EVStorageType_kDisk = 1 +EVDumpType_kSQL = 1 +EVDumpType_kXML = 2 +EVDataKind_kStructureAndRecords = 2 +EVDataKind_kRecordsOnly = 3 + +pretty_print = 1 + +log = logging.getLogger(__name__) + + +class LiveWorshipImport(SongImport): + """ + The :class:`LiveWorshipImport` class provides the ability to import the + LiveWorship Valentina Database. + + The approach is to use the Valentina DB ADK for C via ctypes to dump the database content to a XML + file that is then analysed for data extraction. It would also be possible to skip the XML and + extract the data directly from the database using ctypes, but that approach requires a lot of ctypes + interaction and type conversion that is rather fragile across platforms and versions, so the XML approach + is used for now. + It was implemented using Valentina DB ADK for C version 9.6. + """ + def __init__(self, manager, **kwargs): + """ + Initialise the LiveWorship importer. + """ + self.root = None + super(LiveWorshipImport, self).__init__(manager, **kwargs) + + def do_import(self): + """ + Receive a path to a LiveWorship (valentina) DB. + """ + self.dump_file = Path(gettempdir()) / 'openlp-liveworship-dump.xml' + if not self.dump_valentina_to_xml(): + return + self.load_xml_dump() + if self.root is None: + return + self.extract_songs() + delete_file(self.dump_file) + + def dump_valentina_to_xml(self): + """ + Load the LiveWorship database using the Valentina DB ADK for C and dump the DB content to a XML file. + """ + self.import_wizard.increment_progress_bar(translate('SongsPlugin.LiveWorshipImport', + 'Extracting data from database'), 0) + # Based on OS and bitness, try to load the dll + libVCSDK = None + if is_win(): + # The DLL path must be set depending on the bitness of the OpenLP/Python instance + if is_64bit_instance(): + vcdk_install_folder = 'VCDK_x64_{adkver}' + dll_name = '/vcsdk_release_x64.dll' + else: + vcdk_install_folder = 'VCDK_{adkver}' + dll_name = '/vcsdk_release_x86.dll' + dll_path = '{pf}\\Paradigma Software\\{vcdk}'.format(pf=os.getenv('PROGRAMFILES'), vcdk=vcdk_install_folder) + dll_path2 = '{pf}\\Paradigma Software\\vcomponents_win_vc'.format(pf=os.getenv('PROGRAMFILES')) + os.environ['PATH'] = ';'.join([os.environ['PATH'], dll_path, dll_path2]) + libVCSDK_path = dll_path + dll_name + elif is_linux(): + libVCSDK_path = '/opt/VCSDK/libVCSDK.so' + elif is_macosx(): + # The DLL path must be set depending on the bitness of the OpenLP/Python instance + if is_64bit_instance(): + vcdk_install_folder = 'VCDK_x64_{adkver}' + dll_name = '/vcsdk_x64.dylib' + else: + vcdk_install_folder = 'VCDK_{adkver}' + dll_name = '/vcsdk_x86.dylib' + libVCSDK_path = '/Users/Shared/Paradigma Software/{folder}/{dll}'.format(folder=vcdk_install_folder, + dll=dll_name) + # Try to make this somewhat future proof by trying versions 9 to 15 + found_dll = False + if '{adkver}' in libVCSDK_path: + for i in range(9, 16): + if os.path.exists(libVCSDK_path.format(adkver=i)): + found_dll = True + libVCSDK_path = libVCSDK_path.format(adkver=i) + break + if not found_dll: + libVCSDK_path = libVCSDK_path.format(adkver=9) + elif os.path.exists(libVCSDK_path): + found_dll = True + if not found_dll: + adk_name = "Valentina DB ADK for C, {bitness} bit" + if is_64bit_instance(): + adk_name = adk_name.format(bitness=64) + else: + adk_name = adk_name.format(bitness=32) + critical_error_message_box(translate('SongsPlugin.LiveWorshipImport', + 'Could not find Valentina DB ADK libraries '), + translate('SongsPlugin.LiveWorshipImport', + 'Could not find "{dllpath}", please install "{adk}"' + .format(dllpath=libVCSDK_path, adk=adk_name))) + return False + libVCSDK = ctypes.CDLL(libVCSDK_path) + # cache size set to 1024, got no idea what this means... + # serial numbers set to None - only 10 minutes access, should be enough :) + libVCSDK.Valentina_Init(1024, None, None, None) + # Create a DB instance + Database_New = libVCSDK.Database_New + Database_New.argtypes = [ctypes.c_int] + Database_New.restype = ctypes.c_void_p + database = Database_New(EVStorageType_kDisk) + database_ptr = ctypes.c_void_p(database) + # Load the file into our instance + libVCSDK.Database_Open(database_ptr, ctypes.c_char_p(str(self.import_source).encode())) + # Dump the database to XML + libVCSDK.Database_Dump(database_ptr, ctypes.c_char_p(str(self.dump_file).encode()), EVDumpType_kXML, + EVDataKind_kStructureAndRecords, pretty_print, ctypes.c_char_p(b'utf-8')) + # Close the DB + libVCSDK.Database_Close(database_ptr) + # Shutdown Valentina + libVCSDK.Valentina_Shutdown() + return True + + def load_xml_dump(self): + self.import_wizard.increment_progress_bar(translate('SongsPlugin.LiveWorshipImport', + 'Loading the extracting data'), 0) + # The file can contain the EOT control character and certain invalid tags with missing attributewhich + # will make lxml fail, so it must be removed. + xml_file = open(self.dump_file, 'rt') + xml_content = xml_file.read() + xml_file.close() + xml_content = xml_content.replace('\4', '**EOT**').replace('CustomProperty =""', 'CustomProperty a=""', 1) + # Now load the XML + parser = etree.XMLParser(remove_blank_text=True, recover=True) + try: + self.root = etree.fromstring(xml_content, parser) + except etree.XMLSyntaxError: + self.log_error(self.dump_file, SongStrings.XMLSyntaxError) + log.exception('XML syntax error in file {path}'.format(path=str(self.dump_file))) + + def extract_songs(self): + """ + Extract all the songs from the XML object + """ + # Find song records + xpath = "//BaseObjectData[@Name='SlideCol']/Record/f[@n='Type' and normalize-space(text())='song']/.." + song_records = self.root.xpath(xpath) + # Song count for progress bar + song_count = len(song_records) + # set progress bar to songcount + self.import_wizard.progress_bar.setMaximum(song_count) + for record in song_records: + if self.stop_import_flag: + break + # reset to default values + self.set_defaults() + # Get song metadata + title = record.xpath("f[@n='Title']/text()") + song_rowid = record.xpath("f[@n='_rowid']/text()") + if title and song_rowid: + self.title = self.clean_string(title[0]) + song_rowid = self.clean_string(song_rowid[0]) + else: + # if no title or no rowid we skip the song + continue + authors_line = record.xpath("f[@n='Author']/text()") + if authors_line: + self.extract_authors(authors_line[0]) + cpr = record.xpath("f[@n='Copyright']/text()") + if cpr: + self.add_copyright(self.clean_string(cpr[0])) + ccli = record.xpath("f[@n='CCLI']/text()") + if ccli: + self.ccli = self.clean_string(ccli[0]) + # Get song tags + self.extract_tags(song_rowid) + # Get song verses + self.extract_verses(song_rowid) + if not self.finish(): + self.log_error(self.title) + + def extract_tags(self, song_id): + """ + Extract the tags for a particular song + """ + xpath = "//BaseObjectData[@Name='TagGroup']/Record/f[@n='SlideCol_rowid' "\ + "and normalize-space(text())='{rowid}']/.." + tag_group_records = self.root.xpath(xpath.format(rowid=song_id)) + for record in tag_group_records: + tag_rowid = record.xpath("f[@n='Tag_rowid']/text()") + if tag_rowid: + tag_rowid = self.clean_string(tag_rowid[0]) + xpath = "//BaseObjectData[@Name='Tag']/Record/f[@n='_rowid' and normalize-space(text())='{rowid}']/"\ + "../f[@n='Description']/text()" + tag = self.root.xpath(xpath.format(rowid=tag_rowid)) + if tag: + tag = self.clean_string(tag[0]) + self.topics.append(tag) + + def extract_verses(self, song_id): + """ + Extract the verses for a particular song + """ + xpath = "//BaseObjectData[@Name='SlideColSlides']/Record/f[@n='SlideCol_rowid' and "\ + "normalize-space(text())='{rowid}']/.." + slides_records = self.root.xpath(xpath.format(rowid=song_id)) + for record in slides_records: + verse_text = record.xpath("f[@n='kText']/text()") + if verse_text: + verse_text = self.clean_verse(verse_text[0]) + verse_tag = record.xpath("f[@n='Description']/text()") + if verse_tag: + verse_tag = self.convert_verse_name(verse_tag[0]) + else: + verse_tag = 'v' + self.add_verse(verse_text, verse_tag) + + def extract_authors(self, authors_line): + """ + Extract the authors as a list of str from the authors record + """ + if not authors_line: + return + for author_name in authors_line.split('/'): + name_parts = [self.clean_string(part) for part in author_name.split(',')][::-1] + self.parse_author(' '.join(name_parts)) + + def clean_string(self, string): + """ + Clean up the strings + """ + # is tab + return string.replace('^`^', '\'').replace('/', '-').replace(' ', ' ').strip() + + def clean_verse(self, verse_line): + """ + Extract the verse lines from the verse record + """ + # is carriage return + return self.clean_string(verse_line.replace(' ', '\n')) + + def convert_verse_name(self, verse_name): + """ + Convert the Verse # to v# + """ + name_parts = verse_name.lower().split() + if len(name_parts) > 1: + return name_parts[0][0] + name_parts[1] + else: + return name_parts[0][0] diff --git a/tests/functional/openlp_plugins/songs/test_liveworshipimport.py b/tests/functional/openlp_plugins/songs/test_liveworshipimport.py new file mode 100644 index 000000000..d5c2acf80 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_liveworshipimport.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 . # +########################################################################## +""" +This module contains tests for the LiveWorship song importer. +""" +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from tests.utils import load_external_result_data +from tests.utils.constants import RESOURCE_PATH + +from openlp.core.common.registry import Registry +from openlp.plugins.songs.lib.importers.liveworship import LiveWorshipImport + + +TEST_PATH = RESOURCE_PATH / 'songs' / 'liveworship' + + +def _get_item(data, key): + """ + Get an item or return a blank string + """ + if key in data: + return data[key] + return '' + + +class TestLiveWorshipSongImport(TestCase): + """ + Test the functions in the :mod:`liveworshipimport` module. + """ + def setUp(self): + """ + Create the registry + """ + Registry.create() + + @patch('openlp.plugins.songs.lib.importers.liveworship.SongImport') + def test_create_importer(self, mocked_songimport): + """ + Test creating an instance of the LiveWorship file importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = LiveWorshipImport(mocked_manager, file_paths=[]) + + # THEN: The importer object should not be None + assert importer is not None, 'Import should not be none' + + @patch('openlp.plugins.songs.lib.importers.liveworship.SongImport') + def test_parse_xml_dump(self, mocked_songimport): + """ + Test importing a simple XML dump in LiveWorshipImport + """ + # GIVEN: A mocked out SongImport class, a mocked out "manager" and a simple XML dump + mocked_manager = MagicMock() + importer = LiveWorshipImport(mocked_manager, file_paths=[]) + importer.finish = MagicMock() + importer.import_wizard = MagicMock() + importer.dump_file = TEST_PATH / 'valentina-db-simplified-dump.xml' + + # WHEN: The XML is loaded and processed + importer.load_xml_dump() + importer.extract_songs() + + # THEN: The imported data should look like expected + result_data = load_external_result_data(TEST_PATH / 'A Child Of The King.json') + assert importer.title == _get_item(result_data, 'title') + assert importer.verses == _get_item(result_data, 'verses') + assert importer.topics[0] == _get_item(result_data, 'topics')[0] + assert importer.authors[0][0] == _get_item(result_data, 'authors')[0] + assert importer.authors[1][0] == _get_item(result_data, 'authors')[1] diff --git a/tests/resources/songs/liveworship/A Child Of The King.json b/tests/resources/songs/liveworship/A Child Of The King.json new file mode 100644 index 000000000..e418a62c7 --- /dev/null +++ b/tests/resources/songs/liveworship/A Child Of The King.json @@ -0,0 +1,35 @@ +{ + "title": "A Child Of The King", + "authors": [ + "Harriet Eugenia Peck Buell", + "John Bunnell Sumner" + ], + "topics": [ "Worship" ], + "verses": [ + [ + "v1", + "My Father is rich in houses and lands,\nHe holdeth the wealth of the world in His hands!\nOf rubies and diamonds, of silver and gold,\nHis coffers are full, He has riches untold.", + null + ], + [ + "v2", + "My Father's own Son, the Savior of men,\nOnce wandered on earth as the poorest of them;\nBut now He is pleading our pardon on high,\nThat we may be His when He comes by and by.", + null + ], + [ + "v3", + "I once was an outcast stranger on earth,\nA sinner by choice, an alien by birth,\nBut I've been adopted, my name's written down,\nAn heir to a mansion, a robe and a crown.", + null + ], + [ + "v4", + "A tent or a cottage, why should I care?\nThey're building a palace for me over there;\nThough exiled from home, yet still may I sing:\nAll glory to God, I'm a child of the King.", + null + ], + [ + "c1", + "I'm a child of the King,\nA child of the King:\nWith Jesus my Savior,\nI'm a child of the King.", + null + ] + ] +} diff --git a/tests/resources/songs/liveworship/valentina-db-simplified-dump.xml b/tests/resources/songs/liveworship/valentina-db-simplified-dump.xml new file mode 100644 index 000000000..87a23cf91 --- /dev/null +++ b/tests/resources/songs/liveworship/valentina-db-simplified-dump.xml @@ -0,0 +1,362 @@ + + + + 1 + 32768 + 7 + 0 + 0 + / + : + . + + en_US_POSIX + + 16 + 21 + 16 + 16 + 16 + 2 + 16 + 16 + + UTF-16 + UTF-16 + + 0 + 0 + 0 + + + 0 + 16 + 0 + 0 + 0 + 0 + + + + + + + + 1 + 1.000000 + song + A Charge To Keep I Have + 00/00/0000 00:00:00:000 + Wesley, Charles / Mason, Lowell + Public Domain + + 0.000000 + + + 2 + 2.000000 + song + A Child Of The King + 00/00/0000 00:00:00:000 + Buell, Harriet Eugenia Peck / Sumner, John Bunnell + Public Domain + + 0.000000 + + + + 9478 + + 1 + 1.000000 + 1.000000 + Verse 1 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 2 + 2.000000 + 1.000000 + Verse 2 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 3 + 3.000000 + 1.000000 + Verse 3 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 4 + 4.000000 + 1.000000 + Verse 4 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 5 + 5.000000 + 2.000000 + Verse 1 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 6 + 6.000000 + 2.000000 + Verse 2 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 7 + 7.000000 + 2.000000 + Verse 3 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 8 + 8.000000 + 2.000000 + Verse 4 + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + 9 + 9.000000 + 2.000000 + Chorus + 0.000000 + 0 + 0.000000 + + + 0 + 0 + 0.000000 + + 0.000000 + 0 + 0.000000 + 0.000000 + + 0 + 0 + + 0.000000 + + 0.000000 + + + 0.000000 + 0.000000 + + + + 158 + + 1 + 854.000000 + 28.000000 + 0.000000 + 0.000000 + 2.000000 + 0.000000 + + + + 11 + + 1 + 28.000000 + 6.000000 + Worship + + + +