openlp/openlp/plugins/songs/lib/importers/easyslides.py

322 lines
13 KiB
Python

# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2024 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/>. #
##########################################################################
import logging
import re
from lxml import etree, objectify
from openlp.core.common import normalize_str
from openlp.core.common.i18n import translate
from openlp.plugins.songs.lib import VerseType
from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__)
class EasySlidesImport(SongImport):
"""
Import songs exported from EasySlides
The format example is here:
http://wiki.openlp.org/Development:EasySlides_-_Song_Data_Format
"""
def __init__(self, manager, **kwargs):
"""
Initialise the class.
"""
super(EasySlidesImport, self).__init__(manager, **kwargs)
def do_import(self):
log.info('Importing EasySlides XML file {source}'.format(source=self.import_source))
parser = etree.XMLParser(remove_blank_text=True, recover=True)
try:
with self.import_source.open('r', encoding='utf-8-sig') as xml_file:
parsed_file = etree.parse(xml_file, parser)
except etree.XMLSyntaxError:
log.exception('XML syntax error in file {name}'.format(name=self.import_source))
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
return
except UnicodeDecodeError:
log.exception('Unreadable characters in {name}'.format(name=self.import_source))
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
return
file_str = etree.tostring(parsed_file)
if not file_str:
log.exception('Could not find XML in file {name}'.format(name=self.import_source))
self.log_error(self.import_source, SongStrings().XMLSyntaxError)
return
xml = file_str.decode()
song_xml = objectify.fromstring(xml)
try:
item_count = len(song_xml.Item)
except AttributeError:
log.exception('No root attribute "Item"')
self.log_error(self.import_source, translate('SongsPlugin.EasySlidesImport',
'Invalid EasySlides song file. Missing Item tag.'))
return
self.import_wizard.progress_bar.setMaximum(item_count)
for song in song_xml.Item:
if self.stop_import_flag:
return
self._parse_song(song)
def _parse_song(self, song):
self._success = True
self._add_unicode_attribute('title', song.Title1, True)
if hasattr(song, 'Title2'):
self._add_unicode_attribute('alternate_title', song.Title2)
if hasattr(song, 'SongNumber'):
self._add_unicode_attribute('song_number', song.SongNumber)
if self.song_number == '0':
self.song_number = ''
if hasattr(song, 'Writer'):
self._add_authors(song.Writer)
if hasattr(song, 'Copyright'):
self._add_copyright(song.Copyright)
if hasattr(song, 'LicenceAdmin1'):
self._add_copyright(song.LicenceAdmin1)
if hasattr(song, 'LicenceAdmin2'):
self._add_copyright(song.LicenceAdmin2)
if hasattr(song, 'BookReference'):
self._add_unicode_attribute('song_book_name', song.BookReference)
self._parse_and_add_lyrics(song)
if self._success:
if not self.finish():
self.log_error(song.Title1 if song.Title1 else '')
else:
self.set_defaults()
def _add_unicode_attribute(self, self_attribute, import_attribute, mandatory=False):
"""
Add imported values to the song model converting them to unicode at the
same time. If the unicode decode fails or a mandatory attribute is not
present _success is set to False so the importer can react
appropriately.
:param self_attribute: The attribute in the song model to populate.
:param import_attribute: The imported value to convert to unicode and save to the song.
:param mandatory: Signals that this attribute must exist in a valid song.
"""
try:
setattr(self, self_attribute, str(import_attribute).strip())
except UnicodeDecodeError:
log.exception('UnicodeDecodeError decoding {attribute}'.format(attribute=import_attribute))
self._success = False
except AttributeError:
log.exception('No attribute {attribute}'.format(attribute=import_attribute))
if mandatory:
self._success = False
def _add_authors(self, writer):
try:
self.parse_author(str(writer))
except UnicodeDecodeError:
log.exception('Unicode decode error while decoding Writer')
self._success = False
def _add_copyright(self, element):
"""
Add a piece of copyright to the total copyright information for the song.
:param element: The imported variable to get the data from.
"""
try:
self.add_copyright(str(element).strip())
except UnicodeDecodeError:
log.exception('Unicode error on decoding copyright: {element}'.format(element=element))
self._success = False
except AttributeError:
pass
def _parse_and_add_lyrics(self, song):
"""
Process the song lyrics
:param song: The song details
"""
try:
lyrics = str(song.Contents).strip()
except UnicodeDecodeError:
log.exception('Unicode decode error while decoding Contents')
self._success = False
return
except AttributeError:
log.exception('no Contents')
self._success = False
return
lines = lyrics.split('\n')
# we go over all lines first, to determine information,
# which tells us how to parse verses later
region_lines = {}
separator_lines = 0
for line in lines:
line = line.strip()
if not line:
continue
elif line[1:7] == 'region':
# this is region separator, probably [region 2]
region = self._extract_region(line)
region_lines[region] = 1 + region_lines.get(region, 0)
elif line[0] == '[':
separator_lines += 1
# if the song has separators
separators = (separator_lines > 0)
# the number of different regions in song - 1
if len(region_lines) > 1:
log.info('EasySlidesImport: the file contained a song named "{title}"'
'with more than two regions, but only two regions are tested, '
'encountered regions were: {keys}'.format(title=self.title,
keys=','.join(list(region_lines.keys()))))
# if the song has regions
regions = (len(region_lines) > 0)
# if the regions are inside verses
regions_in_verses = (regions and region_lines[list(region_lines.keys())[0]] > 1)
MarkTypes = {
'CHORUS': VerseType.tags[VerseType.Chorus],
'VERSE': VerseType.tags[VerseType.Verse],
'INTRO': VerseType.tags[VerseType.Intro],
'ENDING': VerseType.tags[VerseType.Ending],
'BRIDGE': VerseType.tags[VerseType.Bridge],
'PRECHORUS': VerseType.tags[VerseType.PreChorus]
}
verses = {}
# list as [region, versetype, versenum, instance]
our_verse_order = []
default_region = '1'
reg = default_region
verses[reg] = {}
# instance differentiates occurrences of same verse tag
vt = 'v'
vn = '1'
inst = 1
for line in lines:
line = line.strip()
if not line:
if separators:
# separators are used, so empty line means slide break
# inside verse
if self._list_has(verses, [reg, vt, vn, inst]):
inst += 1
else:
# separators are not used, so empty line starts a new verse
vt = 'v'
vn = len(verses[reg].get(vt, {})) + 1
inst = 1
elif line[0:7] == '[region':
reg = self._extract_region(line)
verses.setdefault(reg, {})
if not regions_in_verses:
vt = 'v'
vn = '1'
inst = 1
elif line[0] == '[':
# this is a normal section marker
marker = line[1:line.find(']')].upper()
vn = '1'
# have we got any digits?
# If so, versenumber is everything from the digits to the end
match = re.match(r'(.*)(\d+.*)', marker)
if match:
marker = match.group(1).strip()
vn = match.group(2)
vt = MarkTypes.get(marker, 'o') if marker else 'v'
if regions_in_verses:
region = default_region
inst = 1
if self._list_has(verses, [reg, vt, vn, inst]):
inst = len(verses[reg][vt][vn]) + 1
else:
if not [reg, vt, vn, inst] in our_verse_order:
our_verse_order.append([reg, vt, vn, inst])
verses[reg].setdefault(vt, {})
verses[reg][vt].setdefault(vn, {})
verses[reg][vt][vn].setdefault(inst, [])
verses[reg][vt][vn][inst].append(normalize_str(line))
# done parsing
versetags = []
# we use our_verse_order to ensure, we insert lyrics in the same order
# as these appeared originally in the file
for [reg, vt, vn, inst] in our_verse_order:
if self._list_has(verses, [reg, vt, vn, inst]):
# this is false, but needs user input
versetag = '{tag}{number}'.format(tag=vt, number=vn)
versetags.append(versetag)
lines = '\n'.join(verses[reg][vt][vn][inst])
self.add_verse(lines, versetag)
SeqTypes = {
'p': 'p1',
'q': 'p2',
'c': 'c1',
't': 'c2',
'b': 'b1',
'w': 'b2',
'e': 'e1'}
# Make use of Sequence data, determining the order of verses
try:
order = str(song.Sequence).strip().split(',')
for tag in order:
if not tag:
continue
elif tag[0].isdigit():
tag = 'v' + tag
elif tag.lower() in SeqTypes:
tag = SeqTypes[tag.lower()]
else:
continue
if tag in versetags:
self.verse_order_list.append(tag)
else:
log.info('Got order item {tag}, which is not in versetags, dropping item from presentation '
'order'.format(tag=tag))
except UnicodeDecodeError:
log.exception('Unicode decode error while decoding Sequence')
self._success = False
except AttributeError:
pass
def _list_has(self, lst, sub_items):
"""
See if the list has sub items
:param lst: The list to check
:param sub_items: sub item list
:return:
"""
for sub_item in sub_items:
if sub_item in lst:
lst = lst[sub_item]
else:
return False
return True
def _extract_region(self, line):
# this was true already: line[0:7] == '[region':
"""
Extract the region from text
:param line: The line of text
:return:
"""
right_bracket = line.find(']')
return line[7:right_bracket].strip()