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

546 lines
22 KiB
Python

# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 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 XML of `Foilpresenter <http://foilpresenter.de/>`_ songs is of the format::
<?xml version="1.0" encoding="UTF-8"?>
<foilpresenterfolie version="00300.000092">
<id>2004.6.18.18.44.37.0767</id>
<lastchanged>2012.1.21.8.53.5</lastchanged>
<titel>
<titelstring>Above all</titelstring>
</titel>
<sprache>1</sprache>
<ccliid></ccliid>
<tonart></tonart>
<valign>0</valign>
<notiz>Notiz</notiz>
<versionsinfo>1.0</versionsinfo>
<farben>
<cback>0,0,0</cback>
<ctext>255,255,255</ctext>
</farben>
<reihenfolge>
<name>Standard</name>
<strophennummer>0</strophennummer>
</reihenfolge>
<strophen>
<strophe>
<align>0</align>
<font>Verdana</font>
<textsize>14</textsize>
<bold>0</bold>
<italic>0</italic>
<underline>0</underline>
<key>1</key>
<text>Above all powers, above all kings,
above all nature an all created things;
above all wisdom and all the ways of man,
You were here before the world began.</text>
<sortnr>1</sortnr>
</strophe>
</strophen>
<verkn>
<filename>Herr du bist maechtig.foil</filename>
</verkn>
<copyright>
<font>Arial</font>
<textsize>7</textsize>
<anzeigedauer>3</anzeigedauer>
<bold>0</bold>
<italic>1</italic>
<underline>0</underline>
<text>Text und Musik: Lenny LeBlanc/Paul Baloche</text>
</copyright>
<buch>
<bucheintrag>
<name>Feiert Jesus 3</name>
<nummer>10</nummer>
</bucheintrag>
</buch>
<kategorien>
<name>Worship</name>
</kategorien>
</foilpresenterfolie>
"""
import logging
import re
from lxml import etree, objectify
from openlp.core.common.i18n import translate
from openlp.core.widgets.wizard import WizardStrings
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, SongBook, Song, Topic
from openlp.plugins.songs.lib.importers.songimport import SongImport
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.plugins.songs.lib.ui import SongStrings
log = logging.getLogger(__name__)
class FoilPresenterImport(SongImport):
"""
This provides the Foilpresenter import.
"""
def __init__(self, manager, **kwargs):
"""
Initialise the import.
"""
log.debug('initialise FoilPresenterImport')
SongImport.__init__(self, manager, **kwargs)
self.foil_presenter = FoilPresenter(self.manager, self)
def do_import(self):
"""
Imports the songs.
"""
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
parser = etree.XMLParser(remove_blank_text=True)
for file_path in self.import_source:
if self.stop_import_flag:
return
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name))
try:
parsed_file = etree.parse(str(file_path), parser)
xml = etree.tostring(parsed_file).decode()
self.foil_presenter.xml_to_song(xml)
except etree.XMLSyntaxError:
self.log_error(file_path, SongStrings.XMLSyntaxError)
log.exception('XML syntax error in file {path}'.format(path=file_path))
except AttributeError:
self.log_error(file_path, translate('SongsPlugin.FoilPresenterSongImport',
'Invalid Foilpresenter song file. Missing expected tags'))
log.exception('Missing content in file {path}'.format(path=file_path))
class FoilPresenter(object):
"""
This class represents the converter for Foilpresenter XML from a song.
As Foilpresenter has a rich set of different features, we cannot support
them all. The following features are supported by the :class:`Foilpresenter`
OpenPL does not support styletype and font attributes like "align, font,
textsize, bold, italic, underline"
*<lastchanged>*
This property is currently not supported.
*<title>*
As OpenLP does only support one title, the first titlestring becomes
title, all other titlestrings will be alternate titles
*<sprache>*
This property is not supported.
*<ccliid>*
The *<ccliid>* property is fully supported.
*<tonart>*
This property is currently not supported.
*<valign>*
This property is not supported.
*<notiz>*
The *<notiz>* property is fully supported.
*<versionsinfo>*
This property is not supported.
*<farben>*
This property is not supported.
*<reihenfolge>* = verseOrder
OpenLP supports this property.
*<strophen>*
Only the attributes *key* and *text* are supported.
*<verkn>*
This property is not supported.
*<verkn>*
This property is not supported.
*<copyright>*
Only the attribute *text* is supported. => Done
*<buch>* = songbooks
As OpenLP does only support one songbook, we cannot consider more than
one songbook.
*<kategorien>*
This property is not supported.
The tag *<author>* is not support by foilpresenter, mostly the author is
named in the <copyright> tag. We try to extract the authors from the
<copyright> tag.
"""
def __init__(self, manager, importer):
self.manager = manager
self.importer = importer
def xml_to_song(self, xml):
"""
Create and save a song from Foilpresenter format xml to the database.
:param xml: The XML to parse (unicode).
"""
# No xml get out of here.
if not xml:
return
if xml[:5] == '<?xml':
xml = xml[38:]
song = Song()
# Values will be set when cleaning the song.
song.search_lyrics = ''
song.verse_order = ''
song.search_title = ''
self.save_song = True
# Because "text" seems to be an reserved word, we have to recompile it.
xml = re.compile('<text>').sub('<text_>', xml)
xml = re.compile('</text>').sub('</text_>', xml)
song_xml = objectify.fromstring(xml)
self._process_copyright(song_xml, song)
self._process_cclinumber(song_xml, song)
self._process_titles(song_xml, song)
# The verse order is processed with the lyrics!
self._process_lyrics(song_xml, song)
self._process_comments(song_xml, song)
self._process_authors(song_xml, song)
self._process_songbooks(song_xml, song)
self._process_topics(song_xml, song)
if self.save_song:
clean_song(self.manager, song)
self.manager.save_object(song)
def _process_authors(self, foilpresenterfolie, song):
"""
Adds the authors specified in the XML to the song.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
authors = []
try:
copyright = to_str(foilpresenterfolie.copyright.text_)
except AttributeError:
copyright = None
if copyright:
strings = []
if copyright.find('Copyright') != -1:
temp = copyright.partition('Copyright')
copyright = temp[0]
elif copyright.find('copyright') != -1:
temp = copyright.partition('copyright')
copyright = temp[0]
elif copyright.find('©') != -1:
temp = copyright.partition('©')
copyright = temp[0]
elif copyright.find('(c)') != -1:
temp = copyright.partition('(c)')
copyright = temp[0]
elif copyright.find('(C)') != -1:
temp = copyright.partition('(C)')
copyright = temp[0]
elif copyright.find('c)') != -1:
temp = copyright.partition('c)')
copyright = temp[0]
elif copyright.find('C)') != -1:
temp = copyright.partition('C)')
copyright = temp[0]
elif copyright.find('C:') != -1:
temp = copyright.partition('C:')
copyright = temp[0]
elif copyright.find('C,)') != -1:
temp = copyright.partition('C,)')
copyright = temp[0]
copyright = re.compile(r'\\n').sub(' ', copyright)
copyright = re.compile(r'\(.*\)').sub('', copyright)
if copyright.find('Rechte') != -1:
temp = copyright.partition('Rechte')
copyright = temp[0]
markers = [r'Text +u\.?n?d? +Melodie[\w\,\. ]*:',
r'Text +u\.?n?d? +Musik', 'T & M', 'Melodie und Satz',
r'Text[\w\,\. ]*:', 'Melodie', 'Musik', 'Satz',
'Weise', '[dD]eutsch', r'[dD]t[\.\:]', 'Englisch',
'[oO]riginal', 'Bearbeitung', '[R|r]efrain']
for marker in markers:
copyright = re.compile(marker).sub('<marker>', copyright, re.U)
copyright = re.compile('(?<=<marker>) *:').sub('', copyright)
x = 0
while True:
if copyright.find('<marker>') != -1:
temp = copyright.partition('<marker>')
if temp[0].strip() and x > 0:
strings.append(temp[0])
copyright = temp[2]
x += 1
elif x > 0:
strings.append(copyright)
break
else:
break
author_temp = []
for author in strings:
temp = re.split(r',(?=\D{2})|(?<=\D),|/(?=\D{3,})|(?<=\D);', author)
for tempx in temp:
author_temp.append(tempx)
for author in author_temp:
regex = r'^[\/,;\-\s\.]+|[\/,;\-\s\.]+$|\s*[0-9]{4}\s*[\-\/]?\s*([0-9]{4})?[\/,;\-\s\.]*$'
author = re.compile(regex).sub('', author)
author = re.compile(r'[0-9]{1,2}\.\s?J(ahr)?h\.|um\s*$|vor\s*$').sub('', author)
author = re.compile(r'[N|n]ach.*$').sub('', author)
author = author.strip()
if re.search(r'\w+\.?\s+\w{3,}\s+[a|u]nd\s|\w+\.?\s+\w{3,}\s+&\s', author, re.U):
temp = re.split(r'\s[a|u]nd\s|\s&\s', author)
for tempx in temp:
tempx = tempx.strip()
authors.append(tempx)
elif len(author) > 2:
authors.append(author)
for display_name in authors:
author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
if author is None:
# We need to create a new author, as the author does not exist.
author = Author(display_name=display_name, last_name=display_name.split(' ')[-1],
first_name=' '.join(display_name.split(' ')[:-1]))
self.manager.save_object(author)
song.add_author(author)
def _process_cclinumber(self, foilpresenterfolie, song):
"""
Adds the CCLI number to the song.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
try:
song.ccli_number = to_str(foilpresenterfolie.ccliid)
except AttributeError:
song.ccli_number = ''
def _process_comments(self, foilpresenterfolie, song):
"""
Joins the comments specified in the XML and add it to the song.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
try:
song.comments = to_str(foilpresenterfolie.notiz)
except AttributeError:
song.comments = ''
def _process_copyright(self, foilpresenterfolie, song):
"""
Adds the copyright to the song.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
try:
song.copyright = to_str(foilpresenterfolie.copyright.text_)
except AttributeError:
song.copyright = ''
def _process_lyrics(self, foilpresenterfolie, song):
"""
Processes the verses and search_lyrics for the song.
:param foilpresenterfolie: The foilpresenterfolie object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
sxml = SongXML()
temp_verse_order = {}
temp_verse_order_backup = []
temp_sortnr_backup = 1
temp_sortnr_liste = []
verse_count = {
VerseType.tags[VerseType.Verse]: 1,
VerseType.tags[VerseType.Chorus]: 1,
VerseType.tags[VerseType.Bridge]: 1,
VerseType.tags[VerseType.Ending]: 1,
VerseType.tags[VerseType.Other]: 1,
VerseType.tags[VerseType.Intro]: 1,
VerseType.tags[VerseType.PreChorus]: 1
}
if not hasattr(foilpresenterfolie.strophen, 'strophe'):
self.importer.log_error(to_str(foilpresenterfolie.titel),
str(translate('SongsPlugin.FoilPresenterSongImport',
'Invalid Foilpresenter song file. No verses found.')))
self.save_song = False
return
for strophe in foilpresenterfolie.strophen.strophe:
text = to_str(strophe.text_) if hasattr(strophe, 'text_') else ''
verse_name = to_str(strophe.key)
children = strophe.getchildren()
sortnr = False
for child in children:
if child.tag == 'sortnr':
verse_sortnr = to_str(strophe.sortnr)
sortnr = True
# In older Version there is no sortnr, but we need one
if not sortnr:
verse_sortnr = str(temp_sortnr_backup)
temp_sortnr_backup += 1
# Foilpresenter allows e. g. "Ref" or "1", but we need "C1" or "V1".
temp_sortnr_liste.append(verse_sortnr)
temp_verse_name = re.compile('[0-9].*').sub('', verse_name)
temp_verse_name = temp_verse_name[:3].lower()
if temp_verse_name == 'ref':
verse_type = VerseType.tags[VerseType.Chorus]
elif temp_verse_name == 'r':
verse_type = VerseType.tags[VerseType.Chorus]
elif temp_verse_name == '':
verse_type = VerseType.tags[VerseType.Verse]
elif temp_verse_name == 'v':
verse_type = VerseType.tags[VerseType.Verse]
elif temp_verse_name == 'bri':
verse_type = VerseType.tags[VerseType.Bridge]
elif temp_verse_name == 'cod':
verse_type = VerseType.tags[VerseType.Ending]
elif temp_verse_name == 'sch':
verse_type = VerseType.tags[VerseType.Ending]
elif temp_verse_name == 'pre':
verse_type = VerseType.tags[VerseType.PreChorus]
elif temp_verse_name == 'int':
verse_type = VerseType.tags[VerseType.Intro]
else:
verse_type = VerseType.tags[VerseType.Other]
verse_number = re.compile('[a-zA-Z.+-_ ]*').sub('', verse_name)
# Foilpresenter allows e. g. "C", but we need "C1".
if not verse_number:
verse_number = str(verse_count[verse_type])
verse_count[verse_type] += 1
else:
# test if foilpresenter have the same versenumber two times with
# different parts raise the verse number
for value in temp_verse_order_backup:
if value == ''.join((verse_type, verse_number)):
verse_number = str(int(verse_number) + 1)
verse_type_index = VerseType.from_tag(verse_type[0])
verse_type = VerseType.tags[verse_type_index]
temp_verse_order[verse_sortnr] = ''.join((verse_type[0], verse_number))
temp_verse_order_backup.append(''.join((verse_type[0], verse_number)))
sxml.add_verse_to_lyrics(verse_type, verse_number, text)
song.lyrics = str(sxml.extract_xml(), 'utf-8')
# Process verse order
verse_order = []
verse_strophenr = []
try:
for strophennummer in foilpresenterfolie.reihenfolge.strophennummer:
verse_strophenr.append(strophennummer)
except AttributeError:
pass
# Currently we do not support different "parts"!
if '0' in temp_verse_order:
for vers in temp_verse_order_backup:
verse_order.append(vers)
else:
for number in verse_strophenr:
numberx = temp_sortnr_liste[int(number)]
verse_order.append(temp_verse_order[str(numberx)])
song.verse_order = ' '.join(verse_order)
def _process_songbooks(self, foilpresenterfolie, song):
"""
Adds the song book and song number specified in the XML to the song.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
song.song_book_id = 0
song.song_number = ''
try:
for bucheintrag in foilpresenterfolie.buch.bucheintrag:
book_name = to_str(bucheintrag.name)
if book_name:
book = self.manager.get_object_filtered(SongBook, SongBook.name == book_name)
if book is None:
# We need to create a book, because it does not exist.
book = SongBook(name=book_name, publisher='')
self.manager.save_object(book)
song.song_book_id = book.id
try:
if to_str(bucheintrag.nummer):
song.song_number = to_str(bucheintrag.nummer)
except AttributeError:
pass
# We only support one song book, so take the first one.
break
except AttributeError:
pass
def _process_titles(self, foilpresenterfolie, song):
"""
Processes the titles specified in the song's XML.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
try:
for title_string in foilpresenterfolie.titel.titelstring:
if not song.title:
song.title = to_str(title_string)
song.alternate_title = ''
else:
song.alternate_title = to_str(title_string)
except AttributeError:
# Use first line of first verse
first_line = to_str(foilpresenterfolie.strophen.strophe.text_)
song.title = first_line.split('\n')[0]
def _process_topics(self, foilpresenterfolie, song):
"""
Adds the topics to the song.
:param foilpresenterfolie: The property object (lxml.objectify.ObjectifiedElement).
:param song: The song object.
"""
try:
for name in foilpresenterfolie.kategorien.name:
topic_text = to_str(name)
if topic_text:
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
if topic is None:
# We need to create a topic, because it does not exist.
topic = Topic(name=topic_text)
self.manager.save_object(topic)
song.topics.append(topic)
except AttributeError:
pass
def to_str(element):
"""
This returns the text of an element as unicode string.
:param element: The element
"""
if element is not None:
return str(element)
return ''