← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~john+ubuntu-g/openlp/singingthefaith into lp:openlp

 

John Lines has proposed merging lp:~john+ubuntu-g/openlp/singingthefaith into lp:openlp.

Commit message:
Initial merge of SingingTheFaithImport, including update to importer.py

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~john+ubuntu-g/openlp/singingthefaith/+merge/369398

Singing The Faith is the new Authorized Hymn book for the Methodist Church of Great Britain.
There is an electronic version of the Hymn book, for Windows only, which can export Hymns as text files.

This import module smooths the process of converting these text files into OpenLP. The input format is messy and not intended for automatic processing so the importer uses a combination of heuristics and a hints file. This version has not been tested on all the hymns in Singing The Faith, but deals with most of the, more than 100, hymns it has been tested with.

Note that it includes a test module, but that test module fails, and I am not sure why.
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~john+ubuntu-g/openlp/singingthefaith into lp:openlp.
=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py	2019-04-13 13:00:22 +0000
+++ openlp/plugins/songs/lib/importer.py	2019-06-27 12:35:49 +0000
@@ -42,6 +42,7 @@
 from .importers.powersong import PowerSongImport
 from .importers.presentationmanager import PresentationManagerImport
 from .importers.propresenter import ProPresenterImport
+from .importers.singingthefaith import SingingTheFaithImport
 from .importers.songbeamer import SongBeamerImport
 from .importers.songpro import SongProImport
 from .importers.songshowplus import SongShowPlusImport
@@ -173,16 +174,17 @@
     PowerSong = 16
     PresentationManager = 17
     ProPresenter = 18
-    SongBeamer = 19
-    SongPro = 20
-    SongShowPlus = 21
-    SongsOfFellowship = 22
-    SundayPlus = 23
-    VideoPsalm = 24
-    WordsOfWorship = 25
-    WorshipAssistant = 26
-    WorshipCenterPro = 27
-    ZionWorx = 28
+    SingingTheFaith = 19
+    SongBeamer = 20
+    SongPro = 21
+    SongShowPlus = 22
+    SongsOfFellowship = 23
+    SundayPlus = 24
+    VideoPsalm = 25
+    WordsOfWorship = 26
+    WorshipAssistant = 27
+    WorshipCenterPro = 28
+    ZionWorx = 29
 
     # Set optional attribute defaults
     __defaults__ = {
@@ -343,6 +345,15 @@
             'filter': '{text} (*.pro4 *.pro5 *.pro6)'.format(text=translate('SongsPlugin.ImportWizardForm',
                                                                             'ProPresenter Song Files'))
         },
+       SingingTheFaith: {
+            'class': SingingTheFaithImport,
+            'name': 'SingingTheFaith',
+            'prefix': 'singingTheFaith',
+            'filter': '%s (*.txt)' % translate('SongsPlugin.ImportWizardForm', 'Singing The Faith Exported Files'),
+            'descriptionText': translate('SongsPlugin.ImportWizardForm',
+                                         'First use Singing The Faith Electonic edition to export '
+                                         'the song(s) in Text format.')
+        },
         SongBeamer: {
             'class': SongBeamerImport,
             'name': 'SongBeamer',
@@ -462,6 +473,7 @@
             SongFormat.PowerSong,
             SongFormat.PresentationManager,
             SongFormat.ProPresenter,
+            SongFormat.SingingTheFaith,
             SongFormat.SongBeamer,
             SongFormat.SongPro,
             SongFormat.SongShowPlus,

=== added file 'openlp/plugins/songs/lib/importers/singingthefaith.py'
--- openlp/plugins/songs/lib/importers/singingthefaith.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/lib/importers/singingthefaith.py	2019-06-27 12:35:49 +0000
@@ -0,0 +1,389 @@
+# -*- 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; 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:`singingthefaith` module provides the functionality for importing songs which are
+exported from Singing The Faith - an Authorised songbook for the Methodist Church of
+Great Britain."""
+
+
+import logging
+import re
+
+import os
+
+from openlp.core.common.i18n import translate
+from openlp.plugins.songs.lib.importers.songimport import SongImport
+
+log = logging.getLogger(__name__)
+
+
+class SingingTheFaithImport(SongImport):
+    """
+    Import songs exported from SingingTheFaith
+    """
+
+    hints_available = False
+    checks_needed = True
+    hintline = {}
+    hintfile_version = '0'
+    hint_verseOrder = ''
+    hint_songtitle = ''
+    hint_comments = ''
+    hint_ignoreIndent = False
+
+    def __init__(self, manager, **kwargs):
+        """
+        Initialise the class.
+        """
+        super(SingingTheFaithImport, self).__init__(manager, **kwargs)
+
+    def do_import(self):
+        """
+        Receive a single file or a list of files to import.
+        """
+        if not isinstance(self.import_source, list):
+            return
+        self.import_wizard.progress_bar.setMaximum(len(self.import_source))
+        for filename in self.import_source:
+            if self.stop_import_flag:
+                return
+            song_file = open(filename, 'rt', encoding='cp1251')
+            self.do_import_file(song_file)
+            song_file.close()
+
+    def do_import_file(self, file):
+        """
+        Process the SingingTheFaith file - pass in a file-like object, not a file path.
+        """
+        singingTheFaithVersion = 1
+        self.set_defaults()
+        # Setup variables
+        line_number = 0
+        old_indent = 0
+        chorus_indent = 5           # It might be 6, but we test for >=
+        song_title = 'STF000 -'
+        song_number = '0'
+        ccli = '0'
+        current_verse = ''
+        current_verse_type = 'v'
+        current_verse_number = 1
+        has_chorus = False
+        chorus_written = False
+        verses = []
+        author = ''
+        copyright = ''
+        check_flag = 'z'            # Prepended to title, remove if we think import should be OK
+
+
+        self.add_comment("Imported with Singing The Faith Importer v "+str(singingTheFaithVersion))
+
+# Get the file_song_number - so we can use it for hints
+        filename = file.name
+        song_number_file = os.path.splitext(os.path.basename(filename))[0]
+        song_number_match = re.search('\d+',song_number_file)
+        if song_number_match:
+            song_number_file=song_number_match.group()
+
+# See if there are hints available at all
+            # See if there is a hints file in the same location as the file
+        dir_path = os.path.dirname(os.path.realpath(filename))
+##            print("Pathname is ",dir_path)
+        hints_file_name = os.path.join(dir_path,"hints.tag")
+        try:
+            hints_file=open(hints_file_name,"r")
+            hints_available = self.read_hints(hints_file,song_number_file)
+        except FileNotFoundError:
+            hints_available = False
+
+
+        try:
+            # Read the file
+            for line in file:
+                line_number += 1
+
+##                print("Read line",line_number,"-",line)
+
+                if (hints_available and (str(line_number) in self.hintline)):
+##                    print("Found hint for line ",line_number)
+                    hint = self.hintline[str(line_number)]
+##                    print("Hint is ",hint)
+                    if (hint == "Comment"):
+                        line.strip()
+##                        print("Comment hint for line ",line_number," line is ",line)
+                        self.add_comment(line)
+                        line_number += 1
+                        next(file)
+                        continue
+                    elif (hint == "Ignore"):
+                        line_number += 1
+                        next(file)
+                        continue
+                    elif (hint == "Author"):
+                        # add as a raw author - do not split and make them a words author
+                        line.strip()
+                        self.add_author(line,'words')
+                        line_number += 1
+                        next(file)
+                        continue
+                    elif (hint.startswith("VariantVerse")):
+ ##                       print("VariantVerse found - hint is ",hint)
+                        (vv,hintverse,replace)=hint.split(" ",2)
+                        this_verse = self.verses[int(hintverse)-1]
+                        this_verse_str = this_verse[1]
+                        new_verse = this_verse_str
+                        # There might be multiple replace pairs separated by |
+                        replaces=replace.split("|")
+                        for rep in replaces:
+                            (source_str,dest_str)=rep.split("/")
+                            new_verse = new_verse.replace(source_str,dest_str)
+                        self.add_verse(new_verse,'v')
+                        self.verse_order_list.append('v'+str(current_verse_number))
+                        current_verse_number += 1
+                        line_number += 1
+                        next(file)
+                        continue
+                    else:
+                        self.log_error(translate('SongsPlugin.SingingTheFaithImport', 'File %s' % file.name),
+                            translate('SongsPlugin.SingingTheFaithImport', 'Unknown hint %s' % hint))
+                    return
+                        
+
+                # STF exported lines have a leading verse number at the start of each verse.
+                #  remove them - note that we want to track the indent as that shows a chorus
+                # so will deal with that before stipping all leading spaces.                
+                indent = 0
+                if line.strip():
+##                    print("Dealing non empty line ",line)
+                    verse_num_match = re.search('^\d+',line)
+                    if verse_num_match:
+                        verse_num = verse_num_match.group()
+##                        print("Verse num is ",verse_num)
+                        line = line.lstrip("0123456789")
+                    indent_match = re.search('^\s+',line)
+                    if indent_match:
+                       indent=len(indent_match.group())
+##                       print("indent is ",indent)
+
+                # Assuming we have sorted out what is verse and what is chorus, strip lines, unless ignoreIndent
+                if not self.hint_ignoreIndent:
+                    line = line.strip()
+                else:
+                    line = line.rstrip()
+##                print("Read line",line_number,"(",indent,")",line)
+                if line_number == 2:
+                    # note that songs seem to start with a blank line
+                    song_title = line
+##                    print("Set song title to "+song_title)
+                # Detect the 'Reproduced from Singing the Faith Electronic Words Edition' line
+                if line.startswith('Reproduced from Singing the Faith Electronic Words Edition'):
+                    song_number_match = re.search('\d+',line)
+                    if song_number_match:
+                        song_number=song_number_match.group()
+##                        print("Found Reproduced - song is ",song_number)
+                        continue
+                # If the indent is 0 and it contains '(c)' then it is a Copyright line
+                elif (indent == 0) and ( "(c)" in line):
+                    copyright = line
+                    continue
+                elif (indent == 0) and (line.startswith('Liturgical ')):
+                    self.add_comment(line)
+                    continue
+                elif (indent == 0) and (line.startswith('From The ')):
+                    self.add_comment(line)
+                    continue
+                elif (indent == 0) and (line.startswith('From Common ')):
+                    self.add_comment(line)
+                    continue
+                # If indent is 0 it may be the author, unless it was one of the cases covered above
+                elif (indent == 0) and len(line)>0 :
+##                    print ("Possible author ",line)
+#                   May have more than one author, separated by ' and '
+                    authors = line.split(' and ')
+                    for a in authors:
+                        self.parse_author(a)
+                    continue
+                if line == '':
+##                    print("Starting a new verse")
+                    if current_verse != '':
+##                        print("About to add a verse - type ",current_verse_type," ** ",current_verse)
+                        self.add_verse(current_verse, current_verse_type)
+                        self.verse_order_list.append(current_verse_type+str(current_verse_number))
+                        if (current_verse_type == 'c'):
+                            chorus_written = True
+                        else:
+                            current_verse_number += 1
+                    current_verse = ''
+                    if chorus_written:
+##                        print("Setting current_verse_type to v")
+                        current_verse_type = 'v'
+                else:
+                    # If the line is indented more than or equal chorus_indent then assume it is a chorus
+                    # If then indent has just changed then start a new verse just like hitting a blank line
+
+                    if not self.hint_ignoreIndent and ((indent >= chorus_indent) and (old_indent < indent)):
+##                        print("Change of indent - close off old verse")
+                        if current_verse != '':
+##                            print("About to add a verse (indent change) - type ",current_verse_type," ** ",current_verse)
+                            self.add_verse(current_verse, current_verse_type)
+                            self.verse_order_list.append(current_verse_type+str(current_verse_number))
+                            if current_verse_type == 'v':
+                                current_verse_number += 1
+                        current_verse = line
+##                        print("Setting current_verse_type to c");
+                        current_verse_type = 'c'
+                        old_indent=indent
+                        chorus_written = False
+                        has_chorus = True
+                        continue
+                    if current_verse == '':
+                        current_verse += line
+                    else:
+                        current_verse += '\n' + line
+                old_indent = indent
+        except Exception as e:
+            self.log_error(translate('SongsPlugin.SingingTheFaithImport', 'File %s' % file.name),
+                           translate('SongsPlugin.SingingTheFaithImport', 'Error: %s') % e)
+            return
+
+        if self.hint_songtitle:
+            song_title = self.hint_songtitle
+        self.title = check_flag+"STF"+song_number.zfill(3)+" - "+song_title
+        self.song_book_name="Singing The Faith"
+        self.song_number = song_number
+        self.ccli_number = ccli
+        self.add_copyright(copyright)
+# If we have a chorus then the generated Verse order can not be used directly, but we can generate
+#  one for two special cases - Verse followed by one chorus (to be repeated after every verse)
+#  of Chorus, followed by verses. If hints for ManualCheck or VerseOrder are supplied ignore this
+        if has_chorus and not self.hint_verseOrder and not self.checks_needed:
+##            print ("Has chorus - verse order list is ",self.verse_order_list)
+            auto_verse_order_ok = False
+            # Popular case V1 C2 V2 ...
+            if len(self.verse_order_list) >= 1:         # protect against odd cases
+                if (self.verse_order_list[0] == "v1") and (self.verse_order_list[1] == "c2"):
+                    new_verse_order_list = ['v1','c1']
+                    i = 2
+                    auto_verse_order_ok = True
+                elif (self.verse_order_list[0] == "c1") and (self.verse_order_list[1] == "v1"):
+                    new_verse_order_list = ['c1','v1','c1']
+                    i = 2
+                    auto_verse_order_ok = True
+                # if we are in a case we can deal with
+                if auto_verse_order_ok:
+                   while i < len(self.verse_order_list):
+                        if self.verse_order_list[i].startswith('v'):
+                            new_verse_order_list.append(self.verse_order_list[i])
+                            new_verse_order_list.append("c1")
+                        else:
+                            self.log_error(translate('SongsPlugin.SingingTheFaithImport', 'File %s' % file.name),
+                                'Error: Strange verse order entry '+self.verse_order_list[i])
+##                            print("Found strange verseorder entry ",self.verse_order_list[i]," in ",file.name)
+                            auto_verse_order_ok = False
+                        i += 1
+##                    print(" new verse_order_list (Chorus first is ",new_verse_order_list)
+                   self.verse_order_list = new_verse_order_list 
+            else:
+                if not auto_verse_order_ok:
+                    print ("setting verse_order_list to empty")
+                    self.verse_order_list = []
+            # If it is a simple case, 
+        if self.hint_verseOrder:
+            self.verse_order_list = self.hint_verseOrder.split(',')
+        if self.hint_comments:
+            self.add_comment(self.hint_comments)
+# Write the title last as by now we will know if we need checks
+        if ( hints_available and not self.checks_needed):
+            check_flag=''
+        elif ( not hints_available and not has_chorus):
+            check_flag=''
+        elif ( not hints_available and has_chorus and auto_verse_order_ok):
+            check_flag=''
+        self.title = check_flag+"STF"+song_number.zfill(3)+" - "+song_title
+        if not self.finish():
+            self.log_error(file.name)
+
+
+    def read_hints(self, file, song_number ):
+        hintfound = False
+#   clear hints
+        self.hint_verseOrder = ''
+        self.hintline.clear()
+        self.hint_comments = ''
+        self.hint_songtitle = ''
+        self.hint_ignoreIndent = False
+
+##        print("Reading the hints file for ",song_number)
+        for tl in file:
+#   if the line is empty then return
+            if not tl.strip():
+                return hintfound
+            tagval = tl.split(':')
+            tag = tagval[0].strip()
+            val = tagval[1].strip()
+            if (tag == "Version") :
+                self.hintfile_version = val
+                continue
+            if (tag == "Hymn") and (val == song_number):
+##                print ("Found song ",song_number," in hints")
+                self.add_comment("Using hints version "+str(self.hintfile_version))
+                hintfound = True
+#   Assume, unless the hints has ManualCheck that if hinted all will be OK
+                self.checks_needed = False
+                for tl in file:
+                    tagval = tl.split(':')
+                    tag = tagval[0].strip()
+                    val = tagval[1].strip()
+                    if (tag == "End"):
+                        return hintfound
+                    elif (tag == "CommentsLine"):
+                        vals = val.split(',')
+                        for v in vals:
+                            self.hintline[v] = "Comment"
+                    elif (tag == "IgnoreLine"):
+                        vals = val.split(',')
+                        for v in vals:
+                            self.hintline[v] = "Ignore"
+                    elif (tag == "AuthorLine"):
+                        vals = val.split(',')
+                        for v in vals:
+                            self.hintline[v] = "Author"  
+                    elif (tag == "VerseOrder"):
+                        self.hint_verseOrder = val
+                    elif (tag == "ManualCheck"):
+                        self.checks_needed = True
+                    elif (tag == "IgnoreIndent"):
+                        self.hint_ignoreIndent = True
+                    elif (tag == "VariantVerse"):
+                        vvline = val.split(' ',1)
+                        self.hintline[vvline[0].strip()] = "VariantVerse "+vvline[1].strip()
+                    elif (tag == "SongTitle"):
+                        self.hint_songtitle = val
+                    elif (tag == "AddComment"):
+                        self.hint_comments += '\n' + val
+                    else:
+                        print("Unknown tag ",tag," value ",val)
+
+
+
+        return hintfound        
+ 
+      

=== added file 'tests/functional/openlp_plugins/songs/test_singingthefaithimport.py'
--- tests/functional/openlp_plugins/songs/test_singingthefaithimport.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/songs/test_singingthefaithimport.py	2019-06-27 12:35:49 +0000
@@ -0,0 +1,45 @@
+# -*- 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 <https://www.gnu.org/licenses/>. #
+##########################################################################
+"""
+This module contains tests for the SingingTheFaith song importer.
+"""
+from tests.helpers.songfileimport import SongImportTestHelper
+from tests.utils.constants import RESOURCE_PATH
+
+
+TEST_PATH = RESOURCE_PATH / 'songs' / 'singingthefaith'
+
+
+class TestSingingTheFaithFileImport(SongImportTestHelper):
+
+    def __init__(self, *args, **kwargs):
+        self.importer_class_name = 'SingingTheFaithImport'
+        self.importer_module_name = 'singingthefaith'
+        super(TestSingingTheFaithFileImport, self).__init__(*args, **kwargs)
+
+    def test_song_import(self):
+        """
+        Test that loading a Singing The Faith file works correctly on various files
+        """
+        self.file_import([TEST_PATH / 'H1.txt'],
+                         self.load_external_result_data(TEST_PATH / 'STF001.json'))
+ 

=== modified file 'tests/helpers/songfileimport.py'
--- tests/helpers/songfileimport.py	2019-05-22 06:47:00 +0000
+++ tests/helpers/songfileimport.py	2019-06-27 12:35:49 +0000
@@ -123,7 +123,7 @@
         log.debug("Song copyright imported: %s" % importer.song_number)
         log.debug("Topics imported: %s" % importer.topics)
 
-        assert importer.title == title, 'title for %s should be "%s"' % (source_file_name, title)
+        assert importer.title == title, 'title for %s should be "%s" and is "%s"' % (source_file_name, title, importer.title)
         for author in author_calls:
             if isinstance(author, str):
                 self.mocked_add_author.assert_any_call(author)

=== added directory 'tests/resources/songs/singingthefaith'
=== added file 'tests/resources/songs/singingthefaith/H1.txt'
--- tests/resources/songs/singingthefaith/H1.txt	1970-01-01 00:00:00 +0000
+++ tests/resources/songs/singingthefaith/H1.txt	2019-06-27 12:35:49 +0000
@@ -0,0 +1,30 @@
+
+1   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.
+
+2   'Twas grace that taught my heart to fear,
+    And grace my fears relieved.
+    How precious did that grace appear,
+    The hour I first believed.
+
+3   The Lord has promised good to me,
+    His Word my hope secures.
+    He will my shield and portion be
+    As long as life endures.
+
+4   Thro' many dangers, toils and snares
+    I have already come.
+    'Tis grace that brought me safe thus far,
+    And grace will lead me home.
+
+5   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.
+
+
+John Newton (d. 1807)
+
+Reproduced from Singing the Faith Electronic Words Edition, number 1 - or not as this is a hand made test file

=== added file 'tests/resources/songs/singingthefaith/STF001.json'
--- tests/resources/songs/singingthefaith/STF001.json	1970-01-01 00:00:00 +0000
+++ tests/resources/songs/singingthefaith/STF001.json	2019-06-27 12:35:49 +0000
@@ -0,0 +1,29 @@
+{
+    "title": "STF001 - Amazing Grace! how sweet the sound",
+    "authors": [
+        "John Newton (d. 1807)"
+    ],
+    "verse_order_list": ["v1", "v2", "v3", "v4", "v5"],
+    "verses": [
+        [
+            "Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
+            "v1"
+        ],
+        [
+            "'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.",
+            "v2"
+        ],
+        [
+            "The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.",
+            "v3"
+        ],
+        [
+            "Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
+            "v4"
+        ],
+        [
+            "When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.",
+            "v5"
+        ]
+    ]
+}


Follow ups