Songs cleanned up

This commit is contained in:
Tim Bentley 2014-03-08 19:58:58 +00:00
parent a0a2e5efbc
commit f86c6bc77d
12 changed files with 274 additions and 285 deletions

View File

@ -67,7 +67,7 @@ class EasySlidesImport(SongImport):
self._success = True self._success = True
self._add_unicode_attribute('title', song.Title1, True) self._add_unicode_attribute('title', song.Title1, True)
if hasattr(song, 'Title2'): if hasattr(song, 'Title2'):
self._add_unicode_attribute('alternateTitle', song.Title2) self._add_unicode_attribute('alternate_title', song.Title2)
if hasattr(song, 'SongNumber'): if hasattr(song, 'SongNumber'):
self._add_unicode_attribute('song_number', song.SongNumber) self._add_unicode_attribute('song_number', song.SongNumber)
if self.song_number == '0': if self.song_number == '0':

View File

@ -69,8 +69,8 @@ class SofImport(OooImport):
Use OpenOffice.org Writer for processing the rtf file Use OpenOffice.org Writer for processing the rtf file
The three books are not only inconsistant with each other, they are The three books are not only inconsistent with each other, they are
inconsistant in themselves too with their formatting. Not only this, but inconsistent in themselves too with their formatting. Not only this, but
the 1+2 book does not space out verses correctly. This script attempts the 1+2 book does not space out verses correctly. This script attempts
to sort it out, but doesn't get it 100% right. But better than having to to sort it out, but doesn't get it 100% right. But better than having to
type them all out! type them all out!
@ -90,14 +90,14 @@ class SofImport(OooImport):
""" """
Handle the import process for SoF files. Handle the import process for SoF files.
""" """
self.processSofFile() self.process_sof_file()
def processSofFile(self): def process_sof_file(self):
""" """
Process the RTF file, a paragraph at a time Process the RTF file, a paragraph at a time
""" """
self.blankLines = 0 self.blank_lines = 0
self.newSong() self.new_song()
try: try:
paragraphs = self.document.getText().createEnumeration() paragraphs = self.document.getText().createEnumeration()
while paragraphs.hasMoreElements(): while paragraphs.hasMoreElements():
@ -105,65 +105,68 @@ class SofImport(OooImport):
return return
paragraph = paragraphs.nextElement() paragraph = paragraphs.nextElement()
if paragraph.supportsService("com.sun.star.text.Paragraph"): if paragraph.supportsService("com.sun.star.text.Paragraph"):
self.processParagraph(paragraph) self.process_paragraph(paragraph)
except RuntimeException as exc: except RuntimeException as exc:
log.exception('Error processing file: %s', exc) log.exception('Error processing file: %s', exc)
if not self.finish(): if not self.finish():
self.log_error(self.file_path) self.log_error(self.file_path)
def processParagraph(self, paragraph): def process_paragraph(self, paragraph):
""" """
Process a paragraph. Process a paragraph.
In the first book, a paragraph is a single line. In the latter ones In the first book, a paragraph is a single line. In the latter ones they may contain multiple lines.
they may contain multiple lines. Each paragraph contains textportions. Each textportion has it's own styling, e.g. italics, bold etc.
Each paragraph contains textportions. Each textportion has it's own
styling, e.g. italics, bold etc.
Also check for page breaks, which indicates a new song in books 1+2. Also check for page breaks, which indicates a new song in books 1+2.
In later books, there may not be line breaks, so check for 3 or more In later books, there may not be line breaks, so check for 3 or more newlines
newlines
:param paragraph: The paragraph text
""" """
text = '' text = ''
textportions = paragraph.createEnumeration() text_portions = paragraph.createEnumeration()
while textportions.hasMoreElements(): while text_portions.hasMoreElements():
textportion = textportions.nextElement() text_portion = text_portions.nextElement()
if textportion.BreakType in (PAGE_BEFORE, PAGE_BOTH): if text_portion.BreakType in (PAGE_BEFORE, PAGE_BOTH):
self.processParagraphText(text) self.process_paragraph_text(text)
self.newSong() self.new_song()
text = '' text = ''
text += self.processTextPortion(textportion) text += self.process_text_portion(text_portion)
if textportion.BreakType in (PAGE_AFTER, PAGE_BOTH): if text_portion.BreakType in (PAGE_AFTER, PAGE_BOTH):
self.processParagraphText(text) self.process_paragraph_text(text)
self.newSong() self.new_song()
text = '' text = ''
self.processParagraphText(text) self.process_paragraph_text(text)
def processParagraphText(self, text): def process_paragraph_text(self, text):
""" """
Split the paragraph text into multiple lines and process Split the paragraph text into multiple lines and process
:param text: The text
""" """
for line in text.split('\n'): for line in text.split('\n'):
self.processParagraphLine(line) self.process_paragraph_line(line)
if self.blankLines > 2: if self.blank_lines > 2:
self.newSong() self.new_song()
def processParagraphLine(self, text): def process_paragraph_line(self, text):
""" """
Process a single line. Throw away that text which isn't relevant, i.e. Process a single line. Throw away that text which isn't relevant, i.e.
stuff that appears at the end of the song. stuff that appears at the end of the song.
Anything that is OK, append to the current verse Anything that is OK, append to the current verse
:param text: The text
""" """
text = text.strip() text = text.strip()
if text == '': if text == '':
self.blankLines += 1 self.blank_lines += 1
if self.blankLines > 1: if self.blank_lines > 1:
return return
if self.title != '': if self.title != '':
self.finishVerse() self.finish_verse()
return return
self.blankLines = 0 self.blank_lines = 0
if self.skipToCloseBracket: if self.skip_to_close_bracket:
if text.endswith(')'): if text.endswith(')'):
self.skipToCloseBracket = False self.skip_to_close_bracket = False
return return
if text.startswith('CCL Licence'): if text.startswith('CCL Licence'):
self.italics = False self.italics = False
@ -171,90 +174,93 @@ class SofImport(OooImport):
if text == 'A Songs of Fellowship Worship Resource': if text == 'A Songs of Fellowship Worship Resource':
return return
if text.startswith('(NB.') or text.startswith('(Regrettably') or text.startswith('(From'): if text.startswith('(NB.') or text.startswith('(Regrettably') or text.startswith('(From'):
self.skipToCloseBracket = True self.skip_to_close_bracket = True
return return
if text.startswith('Copyright'): if text.startswith('Copyright'):
self.add_copyright(text) self.add_copyright(text)
return return
if text == '(Repeat)': if text == '(Repeat)':
self.finishVerse() self.finish_verse()
self.repeat_verse() self.repeat_verse()
return return
if self.title == '': if self.title == '':
if self.copyright == '': if self.copyright == '':
self.addSofAuthor(text) self.add_sof_author(text)
else: else:
self.add_copyright(text) self.add_copyright(text)
return return
self.addVerseLine(text) self.add_verse_line(text)
def processTextPortion(self, textportion): def process_text_portion(self, text_portion):
""" """
Process a text portion. Here we just get the text and detect if Process a text portion. Here we just get the text and detect if it's bold or italics. If it's bold then its a
it's bold or italics. If it's bold then its a song number or song title. song number or song title.
Song titles are in all capitals, so we must bring the capitalization Song titles are in all capitals, so we must bring the capitalization into line
into line
:param text_portion: A Piece of text
""" """
text = textportion.getString() text = text_portion.getString()
text = self.tidy_text(text) text = self.tidy_text(text)
if text.strip() == '': if text.strip() == '':
return text return text
if textportion.CharWeight == BOLD: if text_portion.CharWeight == BOLD:
boldtext = text.strip() bold_text = text.strip()
if boldtext.isdigit() and self.song_number == '': if bold_text.isdigit() and self.song_number == '':
self.addSongNumber(boldtext) self.add_song_number(bold_text)
return '' return ''
text = self.uncapText(text) text = self.uncap_text(text)
if self.title == '': if self.title == '':
self.addTitle(text) self.add_title(text)
return text return text
if text.strip().startswith('('): if text.strip().startswith('('):
return text return text
self.italics = (textportion.CharPosture == ITALIC) self.italics = (text_portion.CharPosture == ITALIC)
return text return text
def newSong(self): def new_song(self):
""" """
A change of song. Store the old, create a new A change of song. Store the old, create a new
... but only if the last song was complete. If not, stick with it ... but only if the last song was complete. If not, stick with it
""" """
if self.song: if self.song:
self.finishVerse() self.finish_verse()
if not self.check_complete(): if not self.check_complete():
return return
self.finish() self.finish()
self.song = True self.song = True
self.set_defaults() self.set_defaults()
self.skipToCloseBracket = False self.skip_to_close_bracket = False
self.isChorus = False self.is_chorus = False
self.italics = False self.italics = False
self.currentVerse = '' self.current__verse = ''
def addSongNumber(self, song_no): def add_song_number(self, song_no):
""" """
Add a song number, store as alternate title. Also use the song Add a song number, store as alternate title. Also use the song number to work out which songbook we're in
number to work out which songbook we're in
:param song_no: The Song number
""" """
self.song_number = song_no self.song_number = song_no
self.alternateTitle = song_no + '.' self.alternate_title = song_no + '.'
self.songBook_pub = 'Kingsway Publications' self.song_book_pub = 'Kingsway Publications'
if int(song_no) <= 640: if int(song_no) <= 640:
self.songBook = 'Songs of Fellowship 1' self.song_book = 'Songs of Fellowship 1'
elif int(song_no) <= 1150: elif int(song_no) <= 1150:
self.songBook = 'Songs of Fellowship 2' self.song_book = 'Songs of Fellowship 2'
elif int(song_no) <= 1690: elif int(song_no) <= 1690:
self.songBook = 'Songs of Fellowship 3' self.song_book = 'Songs of Fellowship 3'
elif int(song_no) <= 2200: elif int(song_no) <= 2200:
self.songBook = 'Songs of Fellowship 4' self.song_book = 'Songs of Fellowship 4'
elif int(song_no) <= 2710: elif int(song_no) <= 2710:
self.songBook = 'Songs of Fellowship 5' self.song_book = 'Songs of Fellowship 5'
else: else:
self.songBook = 'Songs of Fellowship Other' self.song_book = 'Songs of Fellowship Other'
def addTitle(self, text): def add_title(self, text):
""" """
Add the title to the song. Strip some leading/trailing punctuation that Add the title to the song. Strip some leading/trailing punctuation that we don't want in a title
we don't want in a title
:param text: Title text
""" """
title = text.strip() title = text.strip()
if title.startswith('\''): if title.startswith('\''):
@ -264,50 +270,50 @@ class SofImport(OooImport):
self.title = title self.title = title
self.import_wizard.increment_progress_bar('Processing song ' + title, 0) self.import_wizard.increment_progress_bar('Processing song ' + title, 0)
def addSofAuthor(self, text): def add_sof_author(self, text):
""" """
Add the author. OpenLP stores them individually so split by 'and', '&' Add the author. OpenLP stores them individually so split by 'and', '&' and comma.
and comma. However need to check for "Mr and Mrs Smith" and turn it to "Mr Smith" and "Mrs Smith".
However need to check for "Mr and Mrs Smith" and turn it to
"Mr Smith" and "Mrs Smith". :param text: Author text
""" """
text = text.replace(' and ', ' & ') text = text.replace(' and ', ' & ')
self.parse_author(text) self.parse_author(text)
def addVerseLine(self, text): def add_verse_line(self, text):
""" """
Add a line to the current verse. If the formatting has changed and Add a line to the current verse. If the formatting has changed and we're beyond the second line of first verse,
we're beyond the second line of first verse, then this indicates then this indicates a change of verse. Italics are a chorus
a change of verse. Italics are a chorus
"""
if self.italics != self.isChorus and ((len(self.verses) > 0) or
(self.currentVerse.count('\n') > 1)):
self.finishVerse()
if self.italics:
self.isChorus = True
self.currentVerse += text + '\n'
def finishVerse(self): :param text: The verse text
""" """
Verse is finished, store it. Note in book 1+2, some songs are formatted if self.italics != self.is_chorus and ((len(self.verses) > 0) or
incorrectly. Here we try and split songs with missing line breaks into (self.current__verse.count('\n') > 1)):
the correct number of verses. self.finish_verse()
if self.italics:
self.is_chorus = True
self.current__verse += text + '\n'
def finish_verse(self):
""" """
if self.currentVerse.strip() == '': Verse is finished, store it. Note in book 1+2, some songs are formatted incorrectly. Here we try and split
songs with missing line breaks into the correct number of verses.
"""
if self.current__verse.strip() == '':
return return
if self.isChorus: if self.is_chorus:
versetag = 'C' versetag = 'C'
splitat = None splitat = None
else: else:
versetag = 'V' versetag = 'V'
splitat = self.verseSplits(self.song_number) splitat = self.verse_splits(self.song_number)
if splitat: if splitat:
ln = 0 ln = 0
verse = '' verse = ''
for line in self.currentVerse.split('\n'): for line in self.current__verse.split('\n'):
ln += 1 ln += 1
if line == '' or ln > splitat: if line == '' or ln > splitat:
self.addSofVerse(verse, versetag) self.add_sof_verse(verse, versetag)
ln = 0 ln = 0
if line: if line:
verse = line + '\n' verse = line + '\n'
@ -316,19 +322,19 @@ class SofImport(OooImport):
else: else:
verse += line + '\n' verse += line + '\n'
if verse: if verse:
self.addSofVerse(verse, versetag) self.add_sof_verse(verse, versetag)
else: else:
self.addSofVerse(self.currentVerse, versetag) self.add_sof_verse(self.current__verse, versetag)
self.currentVerse = '' self.current__verse = ''
self.isChorus = False self.is_chorus = False
def addSofVerse(self, lyrics, tag): def add_sof_verse(self, lyrics, tag):
self.add_verse(lyrics, tag) self.add_verse(lyrics, tag)
if not self.isChorus and 'C1' in self.verse_order_list_generated: if not self.is_chorus and 'C1' in self.verse_order_list_generated:
self.verse_order_list_generated.append('C1') self.verse_order_list_generated.append('C1')
self.verseOrderListGenerated_useful = True self.verse_order_list_generated_useful = True
def uncapText(self, text): def uncap_text(self, text):
""" """
Words in the title are in all capitals, so we lowercase them. Words in the title are in all capitals, so we lowercase them.
However some of these words, e.g. referring to God need a leading However some of these words, e.g. referring to God need a leading
@ -337,28 +343,26 @@ class SofImport(OooImport):
There is a complicated word "One", which is sometimes lower and There is a complicated word "One", which is sometimes lower and
sometimes upper depending on context. Never mind, keep it lower. sometimes upper depending on context. Never mind, keep it lower.
""" """
textarr = re.split('(\W+)', text) text_arr = re.split('(\W+)', text)
textarr[0] = textarr[0].capitalize() text_arr[0] = text_arr[0].capitalize()
for i in range(1, len(textarr)): for i in range(1, len(text_arr)):
# Do not translate these. Fixed strings in SOF song file # Do not translate these. Fixed strings in SOF song file
if textarr[i] in ('JESUS', 'CHRIST', 'KING', 'ALMIGHTY', if text_arr[i] in ('JESUS', 'CHRIST', 'KING', 'ALMIGHTY', 'REDEEMER', 'SHEPHERD', 'SON', 'GOD', 'LORD',
'REDEEMER', 'SHEPHERD', 'SON', 'GOD', 'LORD', 'FATHER', 'FATHER', 'HOLY', 'SPIRIT', 'LAMB', 'YOU', 'YOUR', 'I', 'I\'VE', 'I\'M', 'I\'LL',
'HOLY', 'SPIRIT', 'LAMB', 'YOU', 'YOUR', 'I', 'I\'VE', 'SAVIOUR', 'O', 'YOU\'RE', 'HE', 'HIS', 'HIM', 'ZION', 'EMMANUEL', 'MAJESTY', 'JESUS\'',
'I\'M', 'I\'LL', 'SAVIOUR', 'O', 'YOU\'RE', 'HE', 'HIS', 'JIREH', 'JUDAH', 'LION', 'LORD\'S', 'ABRAHAM', 'GOD\'S', 'FATHER\'S', 'ELIJAH' 'MARTHA',
'HIM', 'ZION', 'EMMANUEL', 'MAJESTY', 'JESUS\'', 'JIREH', 'CHRISTMAS', 'ALPHA', 'OMEGA'):
'JUDAH', 'LION', 'LORD\'S', 'ABRAHAM', 'GOD\'S', text_arr[i] = text_arr[i].capitalize()
'FATHER\'S', 'ELIJAH' 'MARTHA', 'CHRISTMAS', 'ALPHA',
'OMEGA'):
textarr[i] = textarr[i].capitalize()
else: else:
textarr[i] = textarr[i].lower() text_arr[i] = text_arr[i].lower()
text = ''.join(textarr) text = ''.join(text_arr)
return text return text
def verseSplits(self, song_number): def verse_splits(self, song_number):
""" """
Because someone at Kingsway forgot to check the 1+2 RTF file, Because someone at Kingsway forgot to check the 1+2 RTF file, some verses were not formatted correctly.
some verses were not formatted correctly.
:param song_number: The Song number
""" """
if song_number == 11: if song_number == 11:
return 8 return 8

View File

@ -40,6 +40,7 @@ from openlp.plugins.songs.lib.songimport import SongImport
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SongBeamerTypes(object): class SongBeamerTypes(object):
MarkTypes = { MarkTypes = {
'Refrain': VerseType.tags[VerseType.Chorus], 'Refrain': VerseType.tags[VerseType.Chorus],
@ -110,8 +111,8 @@ class SongBeamerImport(SongImport):
if self.stop_import_flag: if self.stop_import_flag:
return return
self.set_defaults() self.set_defaults()
self.currentVerse = '' self.current_verse = ''
self.currentVerseType = VerseType.tags[VerseType.Verse] self.current_verse_type = VerseType.tags[VerseType.Verse]
read_verses = False read_verses = False
file_name = os.path.split(import_file)[1] file_name = os.path.split(import_file)[1]
if os.path.isfile(import_file): if os.path.isfile(import_file):
@ -132,39 +133,38 @@ class SongBeamerImport(SongImport):
if line.startswith('#') and not read_verses: if line.startswith('#') and not read_verses:
self.parseTags(line) self.parseTags(line)
elif line.startswith('---'): elif line.startswith('---'):
if self.currentVerse: if self.current_verse:
self.replaceHtmlTags() self.replace_html_tags()
self.add_verse(self.currentVerse, self.currentVerseType) self.add_verse(self.current_verse, self.current_verse_type)
self.currentVerse = '' self.current_verse = ''
self.currentVerseType = VerseType.tags[VerseType.Verse] self.current_verse_type = VerseType.tags[VerseType.Verse]
read_verses = True read_verses = True
verse_start = True verse_start = True
elif read_verses: elif read_verses:
if verse_start: if verse_start:
verse_start = False verse_start = False
if not self.checkVerseMarks(line): if not self.check_verse_marks(line):
self.currentVerse = line + '\n' self.current_verse = line + '\n'
else: else:
self.currentVerse += line + '\n' self.current_verse += line + '\n'
if self.currentVerse: if self.current_verse:
self.replaceHtmlTags() self.replace_html_tags()
self.add_verse(self.currentVerse, self.currentVerseType) self.add_verse(self.current_verse, self.current_verse_type)
if not self.finish(): if not self.finish():
self.log_error(import_file) self.log_error(import_file)
def replaceHtmlTags(self): def replace_html_tags(self):
""" """
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags. This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
""" """
for pair in SongBeamerImport.HTML_TAG_PAIRS: for pair in SongBeamerImport.HTML_TAG_PAIRS:
self.currentVerse = pair[0].sub(pair[1], self.currentVerse) self.current_verse = pair[0].sub(pair[1], self.current_verse)
def parseTags(self, line): def parseTags(self, line):
""" """
Parses a meta data line. Parses a meta data line.
``line`` :param line: The line in the file. It should consist of a tag and a value for this tag (unicode)::
The line in the file. It should consist of a tag and a value for this tag (unicode)::
u'#Title=Nearer my God to Thee' u'#Title=Nearer my God to Thee'
""" """
@ -267,20 +267,19 @@ class SongBeamerImport(SongImport):
# TODO: add the verse order. # TODO: add the verse order.
pass pass
def checkVerseMarks(self, line): def check_verse_marks(self, line):
""" """
Check and add the verse's MarkType. Returns ``True`` if the given linE contains a correct verse mark otherwise Check and add the verse's MarkType. Returns ``True`` if the given linE contains a correct verse mark otherwise
``False``. ``False``.
``line`` :param line: The line to check for marks (unicode).
The line to check for marks (unicode).
""" """
marks = line.split(' ') marks = line.split(' ')
if len(marks) <= 2 and marks[0] in SongBeamerTypes.MarkTypes: if len(marks) <= 2 and marks[0] in SongBeamerTypes.MarkTypes:
self.currentVerseType = SongBeamerTypes.MarkTypes[marks[0]] self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0]]
if len(marks) == 2: if len(marks) == 2:
# If we have a digit, we append it to current_verse_type. # If we have a digit, we append it to current_verse_type.
if marks[1].isdigit(): if marks[1].isdigit():
self.currentVerseType += marks[1] self.current_verse_type += marks[1]
return True return True
return False return False

View File

@ -56,11 +56,8 @@ def songs_probably_equal(song1, song2):
""" """
Calculate and return whether two songs are probably equal. Calculate and return whether two songs are probably equal.
``song1`` :param song1: The first song to compare.
The first song to compare. :param song2: The second song to compare.
``song2``
The second song to compare.
""" """
if len(song1.search_lyrics) < len(song2.search_lyrics): if len(song1.search_lyrics) < len(song2.search_lyrics):
small = song1.search_lyrics small = song1.search_lyrics
@ -96,8 +93,7 @@ def _op_length(opcode):
""" """
Return the length of a given difference. Return the length of a given difference.
``opcode`` :param opcode: The difference.
The difference.
""" """
return max(opcode[2] - opcode[1], opcode[4] - opcode[3]) return max(opcode[2] - opcode[1], opcode[4] - opcode[3])
@ -107,33 +103,28 @@ def _remove_typos(diff):
Remove typos from a diff set. A typo is a small difference (<max_typo_size) Remove typos from a diff set. A typo is a small difference (<max_typo_size)
surrounded by larger equal passages (>min_fragment_size). surrounded by larger equal passages (>min_fragment_size).
``diff`` :param diff: The diff set to remove the typos from.
The diff set to remove the typos from.
""" """
# Remove typo at beginning of the string. # Remove typo at beginning of the string.
if len(diff) >= 2: if len(diff) >= 2:
if diff[0][0] != "equal" and _op_length(diff[0]) <= MAX_TYPO_SIZE and \ if diff[0][0] != "equal" and _op_length(diff[0]) <= MAX_TYPO_SIZE and _op_length(diff[1]) >= MIN_FRAGMENT_SIZE:
_op_length(diff[1]) >= MIN_FRAGMENT_SIZE: del diff[0]
del diff[0]
# Remove typos in the middle of the string. # Remove typos in the middle of the string.
if len(diff) >= 3: if len(diff) >= 3:
for index in range(len(diff) - 3, -1, -1): for index in range(len(diff) - 3, -1, -1):
if _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \ if _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and diff[index + 1][0] != "equal" and \
diff[index + 1][0] != "equal" and _op_length(diff[index + 1]) <= MAX_TYPO_SIZE and \ _op_length(diff[index + 1]) <= MAX_TYPO_SIZE and _op_length(diff[index + 2]) >= MIN_FRAGMENT_SIZE:
_op_length(diff[index + 2]) >= MIN_FRAGMENT_SIZE: del diff[index + 1]
del diff[index + 1]
# Remove typo at the end of the string. # Remove typo at the end of the string.
if len(diff) >= 2: if len(diff) >= 2:
if _op_length(diff[-2]) >= MIN_FRAGMENT_SIZE and \ if _op_length(diff[-2]) >= MIN_FRAGMENT_SIZE and diff[-1][0] != "equal" \
diff[-1][0] != "equal" and _op_length(diff[-1]) <= MAX_TYPO_SIZE: and _op_length(diff[-1]) <= MAX_TYPO_SIZE:
del diff[-1] del diff[-1]
# Merge the bordering equal passages that occured by removing differences. # Merge the bordering equal passages that occured by removing differences.
for index in range(len(diff) - 2, -1, -1): for index in range(len(diff) - 2, -1, -1):
if diff[index][0] == "equal" and _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \ if diff[index][0] == "equal" and _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \
diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= MIN_FRAGMENT_SIZE: diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= MIN_FRAGMENT_SIZE:
diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], diff[index + 1][4])
diff[index + 1][4]) del diff[index + 1]
del diff[index + 1]
return diff return diff

View File

@ -271,8 +271,7 @@ class SongImport(QtCore.QObject):
def check_complete(self): def check_complete(self):
""" """
Check the mandatory fields are entered (i.e. title and a verse) Check the mandatory fields are entered (i.e. title and a verse)
Author not checked here, if no author then "Author unknown" is Author not checked here, if no author then "Author unknown" is automatically added
automatically added
""" """
if not self.title or not self.verses: if not self.title or not self.verses:
return False return False

View File

@ -89,13 +89,13 @@ class SongProImport(SongImport):
file_line = str(file_line, 'cp1252') file_line = str(file_line, 'cp1252')
file_text = file_line.rstrip() file_text = file_line.rstrip()
if file_text and file_text[0] == '#': if file_text and file_text[0] == '#':
self.processSection(tag, text.rstrip()) self.process_section(tag, text.rstrip())
tag = file_text[1:] tag = file_text[1:]
text = '' text = ''
else: else:
text += file_line text += file_line
def processSection(self, tag, text): def process_section(self, tag, text):
""" """
Process a section of the song, i.e. title, verse etc. Process a section of the song, i.e. title, verse etc.
""" """

View File

@ -141,8 +141,8 @@ class SongShowPlusImport(SongImport):
authors = self.decode(data).split(" / ") authors = self.decode(data).split(" / ")
for author in authors: for author in authors:
if author.find(",") !=-1: if author.find(",") !=-1:
authorParts = author.split(", ") author_parts = author.split(", ")
author = authorParts[1] + " " + authorParts[0] author = author_parts[1] + " " + author_parts[0]
self.parse_author(author) self.parse_author(author)
elif block_key == COPYRIGHT: elif block_key == COPYRIGHT:
self.add_copyright(self.decode(data)) self.add_copyright(self.decode(data))
@ -180,6 +180,13 @@ class SongShowPlusImport(SongImport):
self.log_error(file) self.log_error(file)
def to_openlp_verse_tag(self, verse_name, ignore_unique=False): def to_openlp_verse_tag(self, verse_name, ignore_unique=False):
"""
Handle OpenLP verse tags
:param verse_name: The verse name
:param ignore_unique: Ignore if unique
:return: The verse tags and verse number concatenated
"""
# Have we got any digits? If so, verse number is everything from the digits to the end (OpenLP does not have # Have we got any digits? If so, verse number is everything from the digits to the end (OpenLP does not have
# concept of part verses, so just ignore any non integers on the end (including floats)) # concept of part verses, so just ignore any non integers on the end (including floats))
match = re.match(r'(\D*)(\d+)', verse_name) match = re.match(r'(\D*)(\d+)', verse_name)

View File

@ -71,10 +71,10 @@ class SongsTab(SettingsTab):
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Songs Mode')) self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Songs Mode'))
self.search_as_type_check_box.setText(translate('SongsPlugin.SongsTab', 'Enable search as you type')) self.search_as_type_check_box.setText(translate('SongsPlugin.SongsTab', 'Enable search as you type'))
self.tool_bar_active_check_box.setText(translate('SongsPlugin.SongsTab', self.tool_bar_active_check_box.setText(translate('SongsPlugin.SongsTab',
'Display verses on live tool bar')) 'Display verses on live tool bar'))
self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit')) self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit'))
self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab', self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab',
'Import missing songs from service files')) 'Import missing songs from service files'))
def on_search_as_type_check_box_changed(self, check_state): def on_search_as_type_check_box_changed(self, check_state):
self.song_search = (check_state == QtCore.Qt.Checked) self.song_search = (check_state == QtCore.Qt.Checked)

View File

@ -48,6 +48,7 @@ HOTKEY_TO_VERSE_TYPE = {
'+': 'b', '+': 'b',
'Z': 'o'} 'Z': 'o'}
class SundayPlusImport(SongImport): class SundayPlusImport(SongImport):
""" """
Import Sunday Plus songs Import Sunday Plus songs
@ -68,10 +69,10 @@ class SundayPlusImport(SongImport):
if self.stop_import_flag: if self.stop_import_flag:
return return
song_file = open(filename) song_file = open(filename)
self.doImportFile(song_file) self.do_import_file(song_file)
song_file.close() song_file.close()
def doImportFile(self, file): def do_import_file(self, file):
""" """
Process the Sunday Plus file object. Process the Sunday Plus file object.
""" """
@ -80,11 +81,18 @@ class SundayPlusImport(SongImport):
self.log_error(file.name) self.log_error(file.name)
return return
if not self.title: if not self.title:
self.title = self.titleFromFilename(file.name) self.title = self.title_from_filename(file.name)
if not self.finish(): if not self.finish():
self.log_error(file.name) self.log_error(file.name)
def parse(self, data, cell=False): def parse(self, data, cell=False):
"""
Process the records
:param data: The data to be processed
:param cell: ?
:return:
"""
if len(data) == 0 or data[0:1] != '[' or data[-1] != ']': if len(data) == 0 or data[0:1] != '[' or data[-1] != ']':
self.log_error('File is malformed') self.log_error('File is malformed')
return False return False
@ -173,7 +181,13 @@ class SundayPlusImport(SongImport):
i += 1 i += 1
return True return True
def titleFromFilename(self, filename): def title_from_filename(self, filename):
"""
Extract the title from the filename
:param filename: File name
:return:
"""
title = os.path.split(filename)[1] title = os.path.split(filename)[1]
if title.endswith('.ptf'): if title.endswith('.ptf'):
title = title[:-4] title = title[:-4]

View File

@ -40,6 +40,7 @@ BLOCK_TYPES = ('V', 'C', 'B')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class WowImport(SongImport): class WowImport(SongImport):
""" """
The :class:`WowImport` class provides the ability to import song files from The :class:`WowImport` class provides the ability to import song files from
@ -113,29 +114,33 @@ class WowImport(SongImport):
self.set_defaults() self.set_defaults()
song_data = open(source, 'rb') song_data = open(source, 'rb')
if song_data.read(19) != 'WoW File\nSong Words': if song_data.read(19) != 'WoW File\nSong Words':
self.log_error(source, str(translate('SongsPlugin.WordsofWorshipSongImport', self.log_error(source,
('Invalid Words of Worship song file. Missing "Wow File\\nSong Words" header.')))) str(translate('SongsPlugin.WordsofWorshipSongImport',
'Invalid Words of Worship song file. Missing "Wow File\\nSong '
'Words" header.')))
continue continue
# Seek to byte which stores number of blocks in the song # Seek to byte which stores number of blocks in the song
song_data.seek(56) song_data.seek(56)
no_of_blocks = ord(song_data.read(1)) no_of_blocks = ord(song_data.read(1))
song_data.seek(66) song_data.seek(66)
if song_data.read(16) != 'CSongDoc::CBlock': if song_data.read(16) != 'CSongDoc::CBlock':
self.log_error(source, str(translate('SongsPlugin.WordsofWorshipSongImport', self.log_error(source,
('Invalid Words of Worship song file. Missing "CSongDoc::CBlock" string.')))) str(translate('SongsPlugin.WordsofWorshipSongImport',
'Invalid Words of Worship song file. Missing "CSongDoc::CBlock" '
'string.')))
continue continue
# Seek to the beginning of the first block # Seek to the beginning of the first block
song_data.seek(82) song_data.seek(82)
for block in range(no_of_blocks): for block in range(no_of_blocks):
self.linesToRead = ord(song_data.read(4)[:1]) self.lines_to_read = ord(song_data.read(4)[:1])
block_text = '' block_text = ''
while self.linesToRead: while self.lines_to_read:
self.lineText = str(song_data.read(ord(song_data.read(1))), 'cp1252') self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252')
song_data.seek(1, os.SEEK_CUR) song_data.seek(1, os.SEEK_CUR)
if block_text: if block_text:
block_text += '\n' block_text += '\n'
block_text += self.lineText block_text += self.line_text
self.linesToRead -= 1 self.lines_to_read -= 1
block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])] block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
# Blocks are separated by 2 bytes, skip them, but not if # Blocks are separated by 2 bytes, skip them, but not if
# this is the last block! # this is the last block!

View File

@ -97,21 +97,13 @@ class SongXML(object):
""" """
Add a verse to the ``<lyrics>`` tag. Add a verse to the ``<lyrics>`` tag.
``type`` :param type: A string denoting the type of verse. Possible values are *v*, *c*, *b*, *p*, *i*, *e* and *o*.
A string denoting the type of verse. Possible values are *v*, *c*, *b*, *p*, *i*, *e* and *o*. Any other Any other type is **not** allowed, this also includes translated types.
type is **not** allowed, this also includes translated types. :param number: An integer denoting the number of the item, for example: verse 1.
:param content: The actual text of the verse to be stored.
``number`` :param lang: The verse's language code (ISO-639). This is not required, but should be added if available.
An integer denoting the number of the item, for example: verse 1.
``content``
The actual text of the verse to be stored.
``lang``
The verse's language code (ISO-639). This is not required, but should be added if available.
""" """
verse = etree.Element('verse', type=str(type), verse = etree.Element('verse', type=str(type), label=str(number))
label=str(number))
if lang: if lang:
verse.set('lang', lang) verse.set('lang', lang)
verse.text = etree.CDATA(content) verse.text = etree.CDATA(content)
@ -121,16 +113,13 @@ class SongXML(object):
""" """
Extract our newly created XML song. Extract our newly created XML song.
""" """
return etree.tostring(self.song_xml, encoding='UTF-8', return etree.tostring(self.song_xml, encoding='UTF-8', xml_declaration=True)
xml_declaration=True)
def get_verses(self, xml): def get_verses(self, xml):
""" """
Iterates through the verses in the XML and returns a list of verses and their attributes. Iterates through the verses in the XML and returns a list of verses and their attributes.
``xml`` :param xml: The XML of the song to be parsed.
The XML of the song to be parsed.
The returned list has the following format:: The returned list has the following format::
[[{'type': 'v', 'label': '1'}, u"optional slide split 1[---]optional slide split 2"], [[{'type': 'v', 'label': '1'}, u"optional slide split 1[---]optional slide split 2"],
@ -351,8 +340,7 @@ class OpenLyrics(object):
The first unicode string are the start tags (for the next slide). The second unicode string are the end tags. The first unicode string are the start tags (for the next slide). The second unicode string are the end tags.
``text`` :param text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags are allowed::
The text to test. The text must **not** contain html tags, only OpenLP formatting tags are allowed::
{st}{r}Text text text {st}{r}Text text text
""" """
@ -378,12 +366,9 @@ class OpenLyrics(object):
Create and save a song from OpenLyrics format xml to the database. Since we also export XML from external Create and save a song from OpenLyrics format xml to the database. Since we also export XML from external
sources (e. g. OpenLyrics import), we cannot ensure, that it completely conforms to the OpenLyrics standard. sources (e. g. OpenLyrics import), we cannot ensure, that it completely conforms to the OpenLyrics standard.
``xml`` :param xml: The XML to parse (unicode).
The XML to parse (unicode). :param parse_and_temporary_save: Switch to skip processing the whole song and storing the songs in the database
with a temporary flag. Defaults to ``False``.
``parse_and_temporary_save``
Switch to skip processing the whole song and storing the songs in the database with a temporary flag.
Defaults to ``False``.
""" """
# No xml get out of here. # No xml get out of here.
if not xml: if not xml:
@ -418,6 +403,15 @@ class OpenLyrics(object):
return song return song
def _add_text_to_element(self, tag, parent, text=None, label=None): def _add_text_to_element(self, tag, parent, text=None, label=None):
"""
Build an element
:param tag: A Tag
:param parent: Its parent
:param text: Some text to be added
:param label: And a label
:return:
"""
if label: if label:
element = etree.Element(tag, name=str(label)) element = etree.Element(tag, name=str(label))
else: else:
@ -430,6 +424,9 @@ class OpenLyrics(object):
def _add_tag_to_formatting(self, tag_name, tags_element): def _add_tag_to_formatting(self, tag_name, tags_element):
""" """
Add new formatting tag to the element ``<format>`` if the tag is not present yet. Add new formatting tag to the element ``<format>`` if the tag is not present yet.
:param tag_name: The tag_name
:param tags_element: Some tag elements
""" """
available_tags = FormattingTags.get_html_tags() available_tags = FormattingTags.get_html_tags()
start_tag = '{%s}' % tag_name start_tag = '{%s}' % tag_name
@ -477,16 +474,16 @@ class OpenLyrics(object):
def _extract_xml(self, xml): def _extract_xml(self, xml):
""" """
Extract our newly created XML song. Extract our newly created XML song.
:param xml: The XML
""" """
return etree.tostring(xml, encoding='UTF-8', return etree.tostring(xml, encoding='UTF-8', xml_declaration=True)
xml_declaration=True)
def _text(self, element): def _text(self, element):
""" """
This returns the text of an element as unicode string. This returns the text of an element as unicode string.
``element`` :param element: The element.
The element.
""" """
if element.text is not None: if element.text is not None:
return str(element.text) return str(element.text)
@ -496,11 +493,8 @@ class OpenLyrics(object):
""" """
Adds the authors specified in the XML to the song. Adds the authors specified in the XML to the song.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object
``song``
The song object.
""" """
authors = [] authors = []
if hasattr(properties, 'authors'): if hasattr(properties, 'authors'):
@ -509,24 +503,20 @@ class OpenLyrics(object):
if display_name: if display_name:
authors.append(display_name) authors.append(display_name)
for display_name in authors: for display_name in authors:
author = self.manager.get_object_filtered(Author, author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
Author.display_name == display_name)
if author is None: if author is None:
# We need to create a new author, as the author does not exist. # We need to create a new author, as the author does not exist.
author = Author.populate(display_name=display_name, author = Author.populate(display_name=display_name,
last_name=display_name.split(' ')[-1], last_name=display_name.split(' ')[-1],
first_name=' '.join(display_name.split(' ')[:-1])) first_name=' '.join(display_name.split(' ')[:-1]))
song.authors.append(author) song.authors.append(author)
def _process_cclinumber(self, properties, song): def _process_cclinumber(self, properties, song):
""" """
Adds the CCLI number to the song. Adds the CCLI number to the song.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object.
``song``
The song object.
""" """
if hasattr(properties, 'ccliNo'): if hasattr(properties, 'ccliNo'):
song.ccli_number = self._text(properties.ccliNo) song.ccli_number = self._text(properties.ccliNo)
@ -535,11 +525,8 @@ class OpenLyrics(object):
""" """
Joins the comments specified in the XML and add it to the song. Joins the comments specified in the XML and add it to the song.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object.
``song``
The song object.
""" """
if hasattr(properties, 'comments'): if hasattr(properties, 'comments'):
comments_list = [] comments_list = []
@ -553,11 +540,8 @@ class OpenLyrics(object):
""" """
Adds the copyright to the song. Adds the copyright to the song.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object.
``song``
The song object.
""" """
if hasattr(properties, 'copyright'): if hasattr(properties, 'copyright'):
song.copyright = self._text(properties.copyright) song.copyright = self._text(properties.copyright)
@ -566,6 +550,9 @@ class OpenLyrics(object):
""" """
Process the formatting tags from the song and either add missing tags temporary or permanently to the Process the formatting tags from the song and either add missing tags temporary or permanently to the
formatting tag list. formatting tag list.
:param song_xml: The song XML
:param temporary: Is the song temporary?
""" """
if not hasattr(song_xml, 'format'): if not hasattr(song_xml, 'format'):
return return
@ -603,11 +590,9 @@ class OpenLyrics(object):
Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are
converted. converted.
``element`` :param element: The property object (lxml.etree.Element).
The property object (lxml.etree.Element). :param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
OpenLyrics 0.8.
``newlines``
The switch to enable/disable processing of line breaks <br/>. The <br/> is used since OpenLyrics 0.8.
""" """
text = '' text = ''
use_endtag = True use_endtag = True
@ -655,8 +640,8 @@ class OpenLyrics(object):
""" """
Converts lyrics lines to OpenLP representation. Converts lyrics lines to OpenLP representation.
``lines`` :param lines: The lines object (lxml.objectify.ObjectifiedElement).
The lines object (lxml.objectify.ObjectifiedElement). :param version:
""" """
text = '' text = ''
# Convert lxml.objectify to lxml.etree representation. # Convert lxml.objectify to lxml.etree representation.
@ -682,14 +667,9 @@ class OpenLyrics(object):
""" """
Processes the verses and search_lyrics for the song. Processes the verses and search_lyrics for the song.
``properties`` :param properties: The properties object (lxml.objectify.ObjectifiedElement).
The properties object (lxml.objectify.ObjectifiedElement). :param song_xml: The objectified song (lxml.objectify.ObjectifiedElement).
:param song_obj: The song object.
``song_xml``
The objectified song (lxml.objectify.ObjectifiedElement).
``song_obj``
The song object.
""" """
sxml = SongXML() sxml = SongXML()
verses = {} verses = {}
@ -698,12 +678,12 @@ class OpenLyrics(object):
lyrics = song_xml.lyrics lyrics = song_xml.lyrics
except AttributeError: except AttributeError:
raise OpenLyricsError(OpenLyricsError.LyricsError, '<lyrics> tag is missing.', raise OpenLyricsError(OpenLyricsError.LyricsError, '<lyrics> tag is missing.',
translate('OpenLP.OpenLyricsImportError', '<lyrics> tag is missing.')) translate('OpenLP.OpenLyricsImportError', '<lyrics> tag is missing.'))
try: try:
verse_list = lyrics.verse verse_list = lyrics.verse
except AttributeError: except AttributeError:
raise OpenLyricsError(OpenLyricsError.VerseError, '<verse> tag is missing.', raise OpenLyricsError(OpenLyricsError.VerseError, '<verse> tag is missing.',
translate('OpenLP.OpenLyricsImportError', '<verse> tag is missing.')) translate('OpenLP.OpenLyricsImportError', '<verse> tag is missing.'))
# Loop over the "verse" elements. # Loop over the "verse" elements.
for verse in verse_list: for verse in verse_list:
text = '' text = ''
@ -712,8 +692,7 @@ class OpenLyrics(object):
if text: if text:
text += '\n' text += '\n'
# Append text from "lines" element to verse text. # Append text from "lines" element to verse text.
text += self._process_verse_lines(lines, text += self._process_verse_lines(lines, version=song_xml.get('version'))
version=song_xml.get('version'))
# Add an optional split to the verse text. # Add an optional split to the verse text.
if lines.get('break') is not None: if lines.get('break') is not None:
text += '\n[---]' text += '\n[---]'
@ -749,11 +728,8 @@ class OpenLyrics(object):
""" """
Adds the song book and song number specified in the XML to the song. Adds the song book and song number specified in the XML to the song.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object.
``song``
The song object.
""" """
song.song_book_id = None song.song_book_id = None
song.song_number = '' song.song_number = ''
@ -775,11 +751,8 @@ class OpenLyrics(object):
""" """
Processes the titles specified in the song's XML. Processes the titles specified in the song's XML.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object.
``song``
The song object.
""" """
for title in properties.titles.title: for title in properties.titles.title:
if not song.title: if not song.title:
@ -792,11 +765,8 @@ class OpenLyrics(object):
""" """
Adds the topics to the song. Adds the topics to the song.
``properties`` :param properties: The property object (lxml.objectify.ObjectifiedElement).
The property object (lxml.objectify.ObjectifiedElement). :param song: The song object.
``song``
The song object.
""" """
if hasattr(properties, 'themes'): if hasattr(properties, 'themes'):
for topic_text in properties.themes.theme: for topic_text in properties.themes.theme:

View File

@ -41,6 +41,7 @@ log = logging.getLogger(__name__)
# Used to strip control chars (except 10=LF, 13=CR) # Used to strip control chars (except 10=LF, 13=CR)
CONTROL_CHARS_MAP = dict.fromkeys(list(range(10)) + [11, 12] + list(range(14,32)) + [127]) CONTROL_CHARS_MAP = dict.fromkeys(list(range(10)) + [11, 12] + list(range(14,32)) + [127])
class ZionWorxImport(SongImport): class ZionWorxImport(SongImport):
""" """
The :class:`ZionWorxImport` class provides the ability to import songs The :class:`ZionWorxImport` class provides the ability to import songs
@ -84,13 +85,13 @@ class ZionWorxImport(SongImport):
""" """
with open(self.import_source, 'rb') as songs_file: with open(self.import_source, 'rb') as songs_file:
field_names = ['SongNum', 'Title1', 'Title2', 'Lyrics', 'Writer', 'Copyright', 'Keywords', field_names = ['SongNum', 'Title1', 'Title2', 'Lyrics', 'Writer', 'Copyright', 'Keywords',
'DefaultStyle'] 'DefaultStyle']
songs_reader = csv.DictReader(songs_file, field_names) songs_reader = csv.DictReader(songs_file, field_names)
try: try:
records = list(songs_reader) records = list(songs_reader)
except csv.Error as e: except csv.Error as e:
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Error reading CSV file.'), self.log_error(translate('SongsPlugin.ZionWorxImport', 'Error reading CSV file.'),
translate('SongsPlugin.ZionWorxImport', 'Line %d: %s') % (songs_reader.line_num, e)) translate('SongsPlugin.ZionWorxImport', 'Line %d: %s') % (songs_reader.line_num, e))
return return
num_records = len(records) num_records = len(records)
log.info('%s records found in CSV file' % num_records) log.info('%s records found in CSV file' % num_records)
@ -108,7 +109,7 @@ class ZionWorxImport(SongImport):
lyrics = self._decode(record['Lyrics']) lyrics = self._decode(record['Lyrics'])
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d' % index), self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d' % index),
translate('SongsPlugin.ZionWorxImport', 'Decoding error: %s') % e) translate('SongsPlugin.ZionWorxImport', 'Decoding error: %s') % e)
continue continue
except TypeError as e: except TypeError as e:
self.log_error(translate( self.log_error(translate(
@ -126,12 +127,11 @@ class ZionWorxImport(SongImport):
title = self.title title = self.title
if not self.finish(): if not self.finish():
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index
+ (': "' + title + '"' if title else '')) + (': "' + title + '"' if title else ''))
def _decode(self, str): def _decode(self, str):
""" """
Decodes CSV input to unicode, stripping all control characters (except Decodes CSV input to unicode, stripping all control characters (except new lines).
new lines).
""" """
# This encoding choice seems OK. ZionWorx has no option for setting the # This encoding choice seems OK. ZionWorx has no option for setting the
# encoding for its songs, so we assume encoding is always the same. # encoding for its songs, so we assume encoding is always the same.