forked from openlp/openlp
437 lines
21 KiB
Python
437 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
##########################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# ---------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2022 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/>. #
|
|
##########################################################################
|
|
"""
|
|
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
|
|
from pathlib import Path
|
|
|
|
from openlp.core.common.i18n import translate
|
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
|
from openlp.core.common.applocation import AppLocation
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class SingingTheFaithImport(SongImport):
|
|
"""
|
|
Import songs exported from SingingTheFaith
|
|
"""
|
|
|
|
def __init__(self, manager, **kwargs):
|
|
"""
|
|
Initialise the class.
|
|
"""
|
|
super(SingingTheFaithImport, self).__init__(manager, **kwargs)
|
|
self.hints_available = False
|
|
self.checks_needed = True
|
|
self.hint_line = {}
|
|
self.hint_file_version = '0'
|
|
self.hint_verse_order = ''
|
|
self.hint_song_title = ''
|
|
self.hint_comments = ''
|
|
self.hint_ccli = ''
|
|
self.hint_ignore_indent = False
|
|
self.hint_songbook_number_in_title = False
|
|
|
|
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 file_path in self.import_source:
|
|
if self.stop_import_flag:
|
|
return
|
|
# If this is backported to version 2.4 then do_import is called with a filename
|
|
# rather than a path object if called from the development version.
|
|
# Check here to minimise differences between versions.
|
|
if isinstance(file_path, str):
|
|
song_file = open(file_path, 'rt', encoding='cp1251')
|
|
self.do_import_file(song_file)
|
|
song_file.close()
|
|
else:
|
|
with file_path.open('rt', encoding='cp1251') as song_file:
|
|
self.do_import_file(song_file)
|
|
|
|
def do_import_file(self, file):
|
|
"""
|
|
Process the SingingTheFaith file - pass in a file-like object, not a file path.
|
|
"""
|
|
hints_file_name = 'singingthefaith-hints.tag'
|
|
singing_the_faith_version = 1
|
|
self.set_defaults()
|
|
# Setup variables
|
|
line_number = 0
|
|
old_indent = 0
|
|
# The chorus indent is how many spaces the chorus is indented - it might be 6,
|
|
# but we test for >= and I do not know how consistent to formatting of the
|
|
# exported songs is.
|
|
chorus_indent = 5
|
|
# Initialise the song title - the format of the title finally produced can be affected
|
|
# by the SongbookNumberInTitle option in the hints file
|
|
song_title = 'STF000 -'
|
|
song_number = '0'
|
|
ccli = '0'
|
|
current_verse = ''
|
|
current_verse_type = 'v'
|
|
current_verse_number = 1
|
|
# Potentially we could try to track current chorus number to automatically handle
|
|
# more than 1 chorus, currently unused.
|
|
# current_chorus_number = 1
|
|
has_chorus = False
|
|
chorus_written = False
|
|
auto_verse_order_ok = False
|
|
copyright = ''
|
|
# the check_flag is prepended to the title, removed if the import should be OK
|
|
# all the songs which need manual editing should sort below all the OK songs
|
|
check_flag = 'z'
|
|
|
|
self.add_comment(
|
|
'Imported with Singing The Faith Importer v{no}'.format(no=singing_the_faith_version))
|
|
|
|
# Get the file_song_number - so we can use it for hints
|
|
filename = Path(file.name)
|
|
song_number_file = filename.stem
|
|
song_number_match = re.search(r'\d+', song_number_file)
|
|
if song_number_match:
|
|
song_number_file = song_number_match.group()
|
|
|
|
# See if there is a hints file in the same location as the file
|
|
dir_path = filename.parent
|
|
hints_file_path = dir_path / hints_file_name
|
|
try:
|
|
with hints_file_path.open('r') as hints_file:
|
|
hints_available = self.read_hints(hints_file, song_number_file)
|
|
except FileNotFoundError:
|
|
# Look for the hints file in the Plugins directory
|
|
hints_file_path = AppLocation.get_directory(AppLocation.PluginsDir) / 'songs' / 'lib' / \
|
|
'importers' / hints_file_name
|
|
try:
|
|
with hints_file_path.open('r') as hints_file:
|
|
hints_available = self.read_hints(hints_file, song_number_file)
|
|
except FileNotFoundError:
|
|
hints_available = False
|
|
|
|
try:
|
|
for line in file:
|
|
line_number += 1
|
|
# Strip out leftover formatting (\i and \b)
|
|
line = line.replace('\\i', '')
|
|
line = line.replace('\\b', '')
|
|
if hints_available and str(line_number) in self.hint_line:
|
|
hint = self.hint_line[str(line_number)]
|
|
# Set to false if this hint does not replace the line
|
|
line_replaced = True
|
|
if hint == 'Comment':
|
|
line.strip()
|
|
self.add_comment(line)
|
|
continue
|
|
elif hint == 'Ignore':
|
|
continue
|
|
elif hint == 'Author':
|
|
# add as a raw author - do not split
|
|
line.strip()
|
|
self.add_author(line)
|
|
line_number += 1
|
|
next(file)
|
|
continue
|
|
elif hint.startswith('VariantVerse'):
|
|
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{}'.format(str(current_verse_number)))
|
|
current_verse_number += 1
|
|
line_number += 1
|
|
next(file)
|
|
continue
|
|
elif hint == 'AddSpaceAfterSemi':
|
|
line = line.replace(';', '; ')
|
|
line_replaced = False
|
|
# note - do not use contine here as the line should now be processed as normal.
|
|
elif hint == 'AddSpaceAfterColon':
|
|
line = line.replace(':', ': ')
|
|
line_replaced = False
|
|
elif hint == 'BlankLine':
|
|
line = ' *Blank*'
|
|
line_replaced = False
|
|
elif hint == 'BoldLine':
|
|
# processing of the hint is deferred, but pick it up as a known hint here
|
|
line_replaced = False
|
|
else:
|
|
self.log_error(translate('SongsPlugin.SingingTheFaithImport',
|
|
'File {file})'.format(file=file.name)),
|
|
translate('SongsPlugin.SingingTheFaithImport',
|
|
'Unknown hint {hint}').format(hint=hint))
|
|
if line_replaced:
|
|
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 stripping all leading spaces.
|
|
indent = 0
|
|
if line.strip():
|
|
# One hymn has one line which starts '* 6' at the start of a verse
|
|
# Strip this out
|
|
if line.startswith('* 6'):
|
|
line = line.lstrip('* ')
|
|
verse_num_match = re.search(r'^\d+', line)
|
|
if verse_num_match:
|
|
# Could extract the verse number and check it against the calculated
|
|
# verse number - TODO
|
|
# verse_num = verse_num_match.group()
|
|
line = line.lstrip('0123456789')
|
|
indent_match = re.search(r'^\s+', line)
|
|
if indent_match:
|
|
indent = len(indent_match.group())
|
|
# Assuming we have sorted out what is verse and what is chorus, strip lines,
|
|
# unless ignoreIndent
|
|
if self.hint_ignore_indent:
|
|
line = line.rstrip()
|
|
else:
|
|
line = line.strip()
|
|
if line_number == 2:
|
|
# note that songs seem to start with a blank line so the title is line 2
|
|
# Also we strip blanks from the title, even if ignoring indent.
|
|
song_title = line.strip()
|
|
# Process possible line formatting hints after the verse number has been removed
|
|
if hints_available and str(line_number) in self.hint_line and hint == 'BoldLine':
|
|
line = '{{st}}{0}{{/st}}'.format(line)
|
|
# 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(r'\d+', line)
|
|
if song_number_match:
|
|
song_number = song_number_match.group()
|
|
continue
|
|
elif indent == 0:
|
|
# If the indent is 0 and it contains '(c)' then it is a Copyright line
|
|
if '(c)' in line:
|
|
copyright = line
|
|
continue
|
|
elif (line.startswith('Liturgical ') or line.startswith('From The ') or
|
|
line.startswith('From Common ') or line.startswith('Based on Psalm ')):
|
|
self.add_comment(line)
|
|
continue
|
|
# If indent is 0 it may be the author, unless it was one of the cases covered above
|
|
elif len(line) > 0:
|
|
# May have more than one author, separated by ' and '
|
|
authors = line.split(' and ')
|
|
for a in authors:
|
|
self.parse_author(a)
|
|
continue
|
|
# If a blank line has bee replaced by *Blank* then put it back to being
|
|
# a simple space since this is past stripping blanks
|
|
if '*Blank*' in line:
|
|
line = ' '
|
|
if line == '':
|
|
if 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:
|
|
current_verse_type = 'v'
|
|
else:
|
|
# If the line is indented more than or equal chorus_indent then assume it is a chorus
|
|
# If the indent has just changed then start a new verse just like hitting a blank line
|
|
if not self.hint_ignore_indent and ((indent >= chorus_indent) and (old_indent < indent)):
|
|
if 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
|
|
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 {file}').format(file=file.name),
|
|
translate('SongsPlugin.SingingTheFaithImport', 'Error: {error}').format(error=e))
|
|
return
|
|
|
|
if self.hint_song_title:
|
|
song_title = self.hint_song_title
|
|
self.title = '{}STF{} - {title}'.format(check_flag, song_number.zfill(3), title=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_verse_order and not self.checks_needed:
|
|
auto_verse_order_ok = False
|
|
# Popular case V1 C2 V2 ...
|
|
if self.verse_order_list: # 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:
|
|
auto_verse_order_ok = False
|
|
self.add_comment('Importer detected unexpected verse order entry {}'.format(
|
|
self.verse_order_list[i]))
|
|
i += 1
|
|
self.verse_order_list = new_verse_order_list
|
|
else:
|
|
if not auto_verse_order_ok:
|
|
self.verse_order_list = []
|
|
if self.hint_verse_order:
|
|
self.verse_order_list = self.hint_verse_order.split(',')
|
|
if self.hint_comments:
|
|
self.add_comment(self.hint_comments)
|
|
if self.hint_ccli:
|
|
self.ccli_number = self.hint_ccli
|
|
# 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 = ''
|
|
if self.hint_songbook_number_in_title:
|
|
self.title = '{}STF{} - {title}'.format(check_flag, song_number.zfill(3), title=song_title)
|
|
else:
|
|
self.title = '{}{title}'.format(check_flag, title=song_title)
|
|
if not self.finish():
|
|
self.log_error(file.name)
|
|
|
|
def read_hints(self, file, song_number):
|
|
"""
|
|
Read the hints used to transform a particular song into version which can be projected,
|
|
or improve the transformation process beyond the standard heuristics. Not every song will
|
|
have, or need, hints.
|
|
"""
|
|
hintfound = False
|
|
self.hint_verse_order = ''
|
|
self.hint_line.clear()
|
|
self.hint_comments = ''
|
|
self.hint_song_title = ''
|
|
self.hint_ignore_indent = False
|
|
self.hint_ccli = ''
|
|
for tl in file:
|
|
if not tl.strip():
|
|
return hintfound
|
|
tagval = tl.split(':')
|
|
tag = tagval[0].strip()
|
|
val = tagval[1].strip()
|
|
if tag == 'Version':
|
|
self.hint_file_version = val
|
|
continue
|
|
elif tag == 'SongbookNumberInTitle':
|
|
if val == 'False':
|
|
self.hint_songbook_number_in_title = False
|
|
else:
|
|
self.hint_songbook_number_in_title = True
|
|
continue
|
|
elif tag == 'Comment':
|
|
continue
|
|
if (tag == 'Hymn') and (val == song_number):
|
|
self.add_comment('Using hints version {}'.format(str(self.hint_file_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.hint_line[v] = 'Comment'
|
|
elif tag == 'IgnoreLine':
|
|
vals = val.split(',')
|
|
for v in vals:
|
|
self.hint_line[v] = 'Ignore'
|
|
elif tag == 'AuthorLine':
|
|
vals = val.split(',')
|
|
for v in vals:
|
|
self.hint_line[v] = 'Author'
|
|
elif tag == 'AddSpaceAfterSemi':
|
|
vals = val.split(',')
|
|
for v in vals:
|
|
self.hint_line[v] = 'AddSpaceAfterSemi'
|
|
elif tag == 'AddSpaceAfterColon':
|
|
vals = val.split(',')
|
|
for v in vals:
|
|
self.hint_line[v] = 'AddSpaceAfterColon'
|
|
elif tag == 'BlankLine':
|
|
vals = val.split(',')
|
|
for v in vals:
|
|
self.hint_line[v] = 'BlankLine'
|
|
elif tag == 'BoldLine':
|
|
vals = val.split(',')
|
|
for v in vals:
|
|
self.hint_line[v] = 'BoldLine'
|
|
elif tag == 'VerseOrder':
|
|
self.hint_verse_order = val
|
|
elif tag == 'ManualCheck':
|
|
self.checks_needed = True
|
|
elif tag == 'IgnoreIndent':
|
|
self.hint_ignore_indent = True
|
|
elif tag == 'VariantVerse':
|
|
vvline = val.split(' ', 1)
|
|
self.hint_line[vvline[0].strip()] = 'VariantVerse {}'.format(vvline[1].strip())
|
|
elif tag == 'SongTitle':
|
|
self.hint_song_title = val
|
|
elif tag == 'AddComment':
|
|
self.hint_comments += '\n' + val
|
|
elif tag == 'CCLI':
|
|
self.hint_ccli = val
|
|
elif tag == 'Hymn':
|
|
self.log_error(file.name, 'Missing End tag in hint for Hymn: {}'.format(song_number))
|
|
else:
|
|
self.log_error(file.name, 'Unknown tag {} value {}'.format(tag, val))
|
|
return hintfound
|