# -*- 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 . # ########################################################################## """ The XML of `Foilpresenter `_ songs is of the format:: 2004.6.18.18.44.37.0767 2012.1.21.8.53.5 Above all 1 0 Notiz 1.0 0,0,0 255,255,255 Standard 0 0 Verdana 14 0 0 0 1 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. 1 Herr du bist maechtig.foil Arial 7 3 0 1 0 Text und Musik: Lenny LeBlanc/Paul Baloche Feiert Jesus 3 10 Worship """ 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" ** This property is currently not supported. ** 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 ''