mirror of https://gitlab.com/openlp/openlp.git
551 lines
22 KiB
Python
551 lines
22 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/>. #
|
|
##########################################################################
|
|
"""
|
|
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
|
|
xml = self._remove_declaration(xml)
|
|
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 _remove_declaration(self, xml: str):
|
|
"""Remove the XML declaration"""
|
|
if xml[:5] == '<?xml':
|
|
return xml[38:]
|
|
return xml
|
|
|
|
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 ''
|