From fa05ee4b2decdebe3ee2c3a7717a63e67c328af6 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 9 Apr 2024 15:19:58 +0000 Subject: [PATCH] Add ewsx song importer --- openlp/plugins/songs/lib/importer.py | 54 +++++++++------ .../songs/lib/importers/easyworship.py | 63 ++++++++++++++++++ tests/openlp_plugins/songs/test_ewimport.py | 42 ++++++++++++ tests/resources/songs/easyworship/test1.ewsx | Bin 0 -> 19947 bytes 4 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 tests/resources/songs/easyworship/test1.ewsx diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index ef4acaad0..4dab8c86d 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -166,28 +166,29 @@ class SongFormat(object): EasyWorshipDB = 7 EasyWorshipSqliteDB = 8 EasyWorshipService = 9 - FoilPresenter = 10 - 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 - Datasoul = 31 + EasyWorshipServiceSqliteDB = 10 + FoilPresenter = 11 + LiveWorship = 12 + Lyrix = 13 + MediaShout = 14 + OpenSong = 15 + OPSPro = 16 + PowerPraise = 17 + PowerSong = 18 + PresentationManager = 19 + ProPresenter = 20 + SingingTheFaith = 21 + SongBeamer = 22 + SongPro = 23 + SongShowPlus = 24 + SongsOfFellowship = 25 + SundayPlus = 26 + VideoPsalm = 27 + WordsOfWorship = 28 + WorshipAssistant = 29 + WorshipCenterPro = 30 + ZionWorx = 31 + Datasoul = 32 # Set optional attribute defaults __defaults__ = { @@ -278,6 +279,14 @@ class SongFormat(object): 'filter': '{text} (*.ews)'.format(text=translate('SongsPlugin.ImportWizardForm', 'EasyWorship 2007/2009 Service File')) }, + EasyWorshipServiceSqliteDB: { + 'class': EasyWorshipSongImport, + 'name': 'EasyWorship 6/7 Service File', + 'prefix': 'ew', + 'selectMode': SongFormatSelect.SingleFile, + 'filter': '{text} (*.ewsx)'.format(text=translate('SongsPlugin.ImportWizardForm', + 'EasyWorship 6/7 Service File')) + }, FoilPresenter: { 'class': FoilPresenterImport, 'name': 'Foilpresenter', @@ -487,6 +496,7 @@ class SongFormat(object): SongFormat.EasyWorshipDB, SongFormat.EasyWorshipSqliteDB, SongFormat.EasyWorshipService, + SongFormat.EasyWorshipServiceSqliteDB, SongFormat.FoilPresenter, SongFormat.LiveWorship, SongFormat.Lyrix, diff --git a/openlp/plugins/songs/lib/importers/easyworship.py b/openlp/plugins/songs/lib/importers/easyworship.py index 0d0d2ff03..55e83fd12 100644 --- a/openlp/plugins/songs/lib/importers/easyworship.py +++ b/openlp/plugins/songs/lib/importers/easyworship.py @@ -28,6 +28,8 @@ import sqlite3 import struct import zlib from pathlib import Path +from tempfile import NamedTemporaryFile +from zipfile import ZipFile from openlp.core.common.i18n import translate from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding, strip_rtf @@ -83,6 +85,8 @@ class EasyWorshipSongImport(SongImport): self.import_ews() elif ext == '.db': self.import_db() + elif ext == '.ewsx': + self.import_ewsx() else: self.import_sqlite_db() except Exception: @@ -346,6 +350,65 @@ class EasyWorshipSongImport(SongImport): db_file.close() self.memo_file.close() + def import_ewsx(self): + """ + Imports songs from an EasyWorship 6/7 service file, which is just a zip file with an Sqlite DB with text + resources. Non-text recources is also in the zip file, but is ignored. + """ + invalid_ewsx_msg = translate('SongsPlugin.EasyWorshipSongImport', + 'This is not a valid Easy Worship 6/7 service file.') + # Open ewsx file if it exists + if not self.import_source.is_file(): + log.debug('Given ewsx file does not exists.') + return + tmp_db_file = NamedTemporaryFile(delete=False) + with ZipFile(self.import_source, 'r') as eswx_file: + db_zfile = eswx_file.open('main.db') + # eswx has bad CRC for the database for some reason (custom CRC?), so skip the CRC + db_zfile._expected_crc = None + db_data = db_zfile.read() + tmp_db_file.write(db_data) + tmp_db_file.close() + ewsx_conn = sqlite3.connect(tmp_db_file.file.name) + if ewsx_conn is None: + self.log_error(self.import_source, invalid_ewsx_msg) + return + ewsx_db = ewsx_conn.cursor() + # Take a stab at how text is encoded + self.encoding = 'cp1252' + self.encoding = retrieve_windows_encoding(self.encoding) + if not self.encoding: + log.debug('No encoding set.') + return + # get list of songs in service file, presentation_type=6 means songs + songs_exec = ewsx_db.execute('SELECT rowid, title, author, copyright, reference_number ' + 'FROM presentation WHERE presentation_type=6;') + songs = songs_exec.fetchall() + for song in songs: + self.title = title = song[1] + self.author = song[2] + self.copyright = song[3] + self.ccli_number = song[4] + # get slides for the song, element_type=6 means songs, element_style_type=4 means song text + slides = ewsx_db.execute('SELECT rt.rtf ' + 'FROM element as e ' + 'JOIN slide as s ON e.slide_id = s.rowid ' + 'JOIN resource_text as rt ON rt.resource_id = e.foreground_resource_id ' + 'WHERE e.element_type=6 AND e.element_style_type=4 AND s.presentation_id = ? ' + 'ORDER BY s.order_index;', (song[0],)) + for slide in slides: + if slide: + self.set_song_import_object(self.author, slide[0].encode()) + # save song + if not self.finish(): + self.log_error(self.import_source, + translate('SongsPlugin.EasyWorshipSongImport', + '"{title}" could not be imported. {entry}'). + format(title=title, entry=self.entry_error_log)) + # close database handles + ewsx_conn.close() + Path(tmp_db_file.file.name).unlink() + def import_sqlite_db(self): """ Import the songs from an EasyWorship 6 SQLite database diff --git a/tests/openlp_plugins/songs/test_ewimport.py b/tests/openlp_plugins/songs/test_ewimport.py index 30aaf2533..c7b41365f 100644 --- a/tests/openlp_plugins/songs/test_ewimport.py +++ b/tests/openlp_plugins/songs/test_ewimport.py @@ -498,6 +498,48 @@ def test_ews_file_import(mocked_retrieve_windows_encoding: MagicMock, MockSongIm mocked_finish.assert_called_with() +@patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') +@patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') +def test_ewsx_file_import(mocked_retrieve_windows_encoding: MagicMock, MockSongImport: MagicMock, + registry: Registry, settings: Settings): + """ + Test the actual import of song from ewsx file and check that the imported data is correct. + """ + + # GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard", + # and mocked out "author", "add_copyright", "add_verse", "finish" methods. + mocked_retrieve_windows_encoding.return_value = 'cp1252' + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + mocked_add_author = MagicMock() + mocked_add_verse = MagicMock() + mocked_finish = MagicMock() + mocked_title = MagicMock() + mocked_finish.return_value = True + importer = EasyWorshipSongImportLogger(mocked_manager) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = False + importer.add_author = mocked_add_author + importer.add_verse = mocked_add_verse + importer.title = mocked_title + importer.finish = mocked_finish + importer.topics = [] + + # WHEN: Importing ews file + importer.import_source = str(TEST_PATH / 'test1.ewsx') + import_result = importer.do_import() + + # THEN: do_import should return none, the song data should be as expected, and finish should have been + # called. + title = EWS_SONG_TEST_DATA['title'] + assert import_result is None, 'do_import should return None when it has completed' + assert title in importer._title_assignment_list, 'title for should be "%s"' % title + mocked_add_author.assert_any_call(EWS_SONG_TEST_DATA['authors'][0]) + for verse_text, verse_tag in EWS_SONG_TEST_DATA['verses']: + mocked_add_verse.assert_any_call(verse_text, verse_tag) + mocked_finish.assert_called_with() + + @patch('openlp.plugins.songs.lib.importers.easyworship.SongImport') def test_import_rtf_unescaped_unicode(MockSongImport: MagicMock, registry: Registry, settings: Settings): """ diff --git a/tests/resources/songs/easyworship/test1.ewsx b/tests/resources/songs/easyworship/test1.ewsx new file mode 100644 index 0000000000000000000000000000000000000000..c8833eb00161d0677ea66095f0fddfd6cbc344be GIT binary patch literal 19947 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 GP2%5B-<1^r literal 0 HcmV?d00001