Hi Tomas,
I have wrote a parser for my service eliakim.net to parse and generate
songs with chords on different formats.
If it can help you, here as attachment.
You can test importing french songs on OpenLP from http://shir.fr/
Cheers.
Jeremie.
Le 26/07/2016 à 09:27, Tomas Groth a écrit :
Hi all,
More than a month ago I announced here that I'm working on adding
support for chords in OpenLP (read that email here:
https://lists.openlp.io/pipermail/openlp-dev/2016-June/000093.html).
Now I've had some time to progress and I'd like to get some feedback
from you guys.
The current features are:
* Support for chords in ChordPro format (such as: "A[D]mazing
[D7]grace!").
* Display chords on new stageview (available on
http://<ipaddress>:<port>/chords).
* Display chords on mainview (if enabled in song settings).
* Import from the following formats: OpenLyrics, OpenSong, VideoPsalm.
* Export to OpenLyrics format.
There are still some features missing, such as printing song
with/without chords.
The code is available from this branch: lp:~tomasgroth/openlp/chords
Or if you're on windows you can get a build from here:
https://ci.openlp.io/view/Branch/job/Branch-06-Windows_Installer/44/
As mentioned above I'd appreciate some feedback as I'm not a musician
myself.
Cheers,
Tomas
_______________________________________________
openlp-dev mailing list
[email protected]
https://lists.openlp.io/mailman/listinfo/openlp-dev
# -*- coding: utf-8 -*-
# coding: utf8
#####
# slc.py is part of eliakim.net project
# Jeremie Nau <[email protected]>
#
# Example of usage: python slc.py -s "Nous avons soif de plus.chordpro" -f chordpro -o ascii
#####
import re
import codecs
import chardet, string
from StringIO import StringIO
class Chord(object):
LOCALE_CHORDS = dict(
international = dict(
notes=[ # starting by A
('A',),
('B',),
('C',),
('D',),
('E',),
('F',),
('G',),
],
accidentals=[ # in lower case with sharp for the first and flat for the second
('#', 'sharp', ' sharp'), # sharp
('b', 'flat', ' flat'), # flat
],
diatonic=[ # Notes to use for display starting by A, the prefered note in first
('A',),
('Bb','A#'),
('B', 'Cb'),
('C', 'B#'),
('C#', 'Db'),
('D',),
('Eb', 'D#'),
('E', 'Fb'),
('F', 'E#'),
('F#', 'Gb'),
('G',),
('G#', 'Ab'),
],
),
fr = dict(
notes=[ # starting by A
(u'La',),
(u'Si',),
(u'Do',),
(u'Ré', u'Re'),
(u'Mi',),
(u'Fa',),
(u'Sol',),
],
accidentals=[
(u'#',), # sharp
(u'b', u'bemol', u'bémol', u' bemol', u' bémol'), # flat
],
diatonic=[ # Notes to use for display starting by A, the prefered note in first
(u'La',),
(u'Sib',u'La#'),
(u'Si', u'Dob'),
(u'Do', u'Si#'),
(u'Do#', u'Réb'),
(u'Ré',),
(u'Mib', u'Ré#'),
(u'Mi', u'Fab'),
(u'Fa', u'Mi#'),
(u'Fa#', u'Solb'),
(u'Sol',),
(u'Sol#', u'Lab'),
],
),
)
def __init__(self, name=u'', offset=None, locale_input='international', locale_output=None):
self.original_input = name
self.locale_input = locale_input
self.locale_output = locale_output
self.offset = offset
self.spaces_before = ''
self.spaces_after = ''
self.is_empty = False
self.parsed_chord = [] # list with bass/root chord or root only chord
self.transposed_parsed_chord = [] # list with bass/root chord or root only chord
self.parse()
@property
def text(self):
return self.spaces_before + self.name + self.spaces_after
@property
def name(self):
parsed_chord = self.parsed_chord
if self.transposed_parsed_chord:
parsed_chord = self.transposed_parsed_chord
return u'/'.join([self.get_chord(chord) for chord in parsed_chord])
def get_chord(self, parsed_chord):
if self.locale_output:
note, accidental, mode = parsed_chord
note_index = self.get_element_index('notes', note.lower())
accidental_index = self.get_element_index('accidentals', accidental.lower())
chord = self.LOCALE_CHORDS[self.locale_output]['notes'][note_index][0]
if accidental_index:
chord += self.LOCALE_CHORDS[self.locale_output]['accidentals'][accidental_index][0]
chord += mode
return chord
return u''.join(parsed_chord)
def get_element_index(self, element_name, value):
if not value:
return None
for index, element in enumerate(self.LOCALE_CHORDS[self.locale_input][element_name]):
if value in [elm.lower() for elm in element]:
return index
def parse(self):
if not self.original_input:
self.is_empty = True
return
for chord in self.original_input.split('/'):
self.parsed_chord.append(self.get_parsed_chord(chord))
def get_parsed_chord(self, chord):
chord = chord.strip()
lower_chord = chord.lower()
parsed_note, chord, lower_chord = self.get_chord_element('notes', chord, lower_chord)
parsed_accidental, chord, lower_chord = self.get_chord_element('accidentals', chord, lower_chord)
return (parsed_note, parsed_accidental, chord)
def get_chord_element(self, element_name, chord, lower_chord):
if not chord:
return '', chord, lower_chord
for element in self.LOCALE_CHORDS[self.locale_input][element_name]:
for elm in element:
elm = elm.lower()
if lower_chord.startswith(elm):
elm_len = len(elm)
parsed_element = chord[:elm_len]
chord = chord[elm_len:]
lower_chord = lower_chord[elm_len:]
return parsed_element, chord, lower_chord
return '', chord, lower_chord
def transpose(self, semitone):
if self.is_empty:
return
for chord in self.parsed_chord:
self.transposed_parsed_chord.append(self.get_transposed_chord(chord, semitone))
def get_transposed_chord(self, chord, semitone):
note, accidental, mode = chord
note = note.lower() + accidental.lower()
note_index = [index for index, n in enumerate(self.LOCALE_CHORDS[self.locale_input]['diatonic']) if note in [n2.lower() for n2 in n]][0]
transposed_note_index = (note_index + semitone) % 12
return self.get_parsed_chord(self.LOCALE_CHORDS[self.locale_input]['diatonic'][transposed_note_index][0]+mode)
def __unicode__(self):
return self.text or ''
def __str__(self):
return unicode(self).encode('utf-8')
class SongLine(object):
INTERNAL_START_BLOC = 1
INTERNAL_END_BLOC = 2
STRUCTURE = 3
LYRICS_CHORDS = 4
LYRICS = 5
STRUCTURE_WORDS = dict(
intro=(r'intro[\s:]*$',),
verse=(r'verse?[\d\s:]*$', r'.{0,6}couplet[\d\s:]*$'),
pre_chorus=(r'pr..?chorus[\s:]*$', r'pr..?refrain[\s:]*$'),
chorus=(r'chorus[\d\s:]*$', r'refrain[\d\s:]*$', r'refr?.{0,2}(\s+$|$)'),
bridge=(r'bridge[\s:]*$', r'pont[\s:]*$'),
solo=(r'solo[\s:]*$',),
music=(r'music[\s:]*$', r'musical[\s:]*$', r'instrumental[\s:]*$'),
end=(r'end[\s:]*$', r'ending[\s:]*$', r'outro[\s:]*$', r'fin[\s:]*$', r'final[\s:]*$')
)
def __init__(self, line_type, text=None, lyrics=None, chords=None, bloc_name=None):
self.line_type = line_type
self.text = text
self.lyrics = lyrics
self.chords = chords
self.bloc_name = bloc_name
@property
def is_bloc_start(self):
return self.line_type == self.INTERNAL_START_BLOC
@property
def is_bloc_end(self):
return self.line_type == self.INTERNAL_END_BLOC
@property
def is_structure(self):
return self.line_type == self.STRUCTURE
@property
def is_lyrics_chords(self):
return self.line_type == self.LYRICS_CHORDS
@property
def is_lyrics(self):
return self.line_type == self.LYRICS
@property
def is_blank(self):
if self.text is None and self.lyrics is None and self.chords is None:
return True
return True if self.text is not None and re.match(r'^\s*$', self.text) else False
@property
def is_lyrics_empty(self):
return True if not ''.join(self.lyrics).strip() else False
def is_end_line_empty(self, index):
try:
return False if ''.join(self.lyrics[index+1:]).strip() else True
except IndexError:
return True
def __unicode__(self):
return self.line_type
def __str__(self):
return unicode(self).encode('utf-8')
class Song(object):
def __init__(self, **kwargs):
self.lines = []
self.title = ''
self.author = ''
self.songbook = ''
self.aka = ''
self._chord_key = ''
self.bpm = ''
self.meter = ''
self.time_length = ''
self.arrangement = ''
self.copyright = ''
self.note = ''
self.themes = []
self.transpose = 0
def add_metadata(self, key, value):
if not value:
return
setattr(self, key, value)
def get_metadata(self):
return {attr: getattr(self, attr) for attr in dir(self) if not attr.startswith('_')}
@property
def chord_key(self):
return self._chord_key
@chord_key.setter
def chord_key(self, value):
self._chord_key = unicode(Chord(value))
def __unicode__(self):
return self.title
def __str__(self):
return unicode(self).encode('utf-8')
class BaseFormat(object):
CHORD_PATTERN = [
r'(', # start capture
# Root chord
# r'(?!(?:Fb|Cb|E#|B#))', # incorrect chords (to eliminate) # Seems to be valid chords
r'[A-G]', # note
r'(?:#|b)?', # accidentals
r'(?:m|min|\-|M|maj|Î|sus)?', # mode
r'(?:2|4|5|6|7|7sus|7sus4|9|11|13)?', # intervals
r'(?:',
r'(?:#|b|\-|\+|aug|add|dim|°|o|Ã|ø|dom)', # accidentals
r'(?:2|4|5|6|7|9|11|13)?', # intervals
r')?',
# Bass chord
r'(?:',
r'\/', # / between root chord and bass chord
# r'(?!(?:Fb|Cb|E#|B#))', # incorrect chords (to eliminate) # Seems to be valid chords
r'[A-G]', # note
r'(?:#|b)?', # accidentals
r')?',
r')', # end of capture
r'(?:\s|$)'
]
NON_PRINTABLE_CHARS = [u'\x80', u'\x81', u'\x82', u'\x83', u'\x84', u'\x85', u'\x86', u'\x87', u'\x88', u'\x89', u'\x8A', u'\x8B', u'\x8C', u'\x8D', u'\x8E', u'\x8F', u'\x90', u'\x91', u'\x92', u'\x93', u'\x94', u'\x95', u'\x96', u'\x97', u'\x99', u'\x99', u'\x9A', u'\x9B', u'\x9C', u'\x9D', u'\x9E', u'\x9F', u'\xA0']
def __init__(self, songs=None):
self.song = Song()
self.output_songs = songs
def parse_file(self, filename, *args, **kwargs):
with codecs.open(filename, 'r', encoding='utf-8') as f:
return self.parse_input(f.read(), *args, **kwargs)
def parse_input(self, content, metadata=None):
if metadata:
for key, value in metadata.iteritems():
if isinstance(value, list):
value = [v.decode('utf-8') if isinstance(v, str) else v for v in value]
else:
value = value.decode('utf-8').replace('\r', '') if isinstance(value, str) else value
self.song.add_metadata(key, value)
if not content:
return self.song
guess = chardet.detect(content)
content = content.decode(guess.get('encoding', 'utf-8') or 'utf-8')
content = content.strip(unichr(65279)) # Remove BOM marker, sometimes present at the first char
content = content.replace(u'â', "'")
#content = filter(lambda x: x in string.printable, content) # Remove all non printable char # Warning, all accented chars are also deleted
self.parse(content)
if not self.song.title:
raise Exception('Bad song format for %s' % self.__class__.__name__)
return self.song
def parse(self, content):
raise NotImplementedError()
def get_output(self, *args, **kwargs):
return self.generate(*args, **kwargs)
def get_output_file(self, *args, **kwargs):
return StringIO(self.generate(*args, **kwargs))
def generate(self):
raise NotImplementedError()
def cleanup_content(self, content):
return filter(lambda x: x not in self.NON_PRINTABLE_CHARS, content)
def get_structure_words_pattern(self, key=None):
structure_words_values = []
if not key:
for words in SongLine.STRUCTURE_WORDS.values():
structure_words_values += list(words)
else:
structure_words_values += list(SongLine.STRUCTURE_WORDS.get(key))
return r'(?=(%s))' % '|'.join(structure_words_values)
def get_structure_name(self, line):
for key in SongLine.STRUCTURE_WORDS.iterkeys():
if re.match(self.get_structure_words_pattern(key), line, re.IGNORECASE):
return key
def get_text_without_chords(self, line):
return re.sub(''.join(self.CHORD_PATTERN[:-1]), '', line)
class EliakimFormat(BaseFormat):
def parse(self, content):
chords_waiting_for_lyrics = None
current_bloc_name = None
for line in content.split('\n'):
chords = []
line = line.strip('\r\n').replace('\t', ' ') # Replace all tabs by spaces
for m in re.finditer(''.join(self.CHORD_PATTERN), line):
chords.append(Chord(m.group(1).strip(), offset=m.start()))
if chords and not chords_waiting_for_lyrics:
if re.match(self.get_structure_words_pattern(), line, re.IGNORECASE):
# structure and chords line
if current_bloc_name is not None:
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = self.get_structure_name(line)
self.song.lines.append(SongLine(SongLine.INTERNAL_START_BLOC, bloc_name=current_bloc_name))
self.song.lines.append(SongLine(SongLine.STRUCTURE, text=self.get_text_without_chords(line)))
# chords line
chords_waiting_for_lyrics = chords
continue
if re.match(self.get_structure_words_pattern(), line, re.IGNORECASE):
# structure line
if current_bloc_name is not None:
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = self.get_structure_name(line)
self.song.lines.append(SongLine(SongLine.INTERNAL_START_BLOC, bloc_name=current_bloc_name))
self.song.lines.append(SongLine(SongLine.STRUCTURE, text=line))
continue
if chords_waiting_for_lyrics:
# lyrics and chords
chord_indexes = [c.offset for c in chords_waiting_for_lyrics]
lyrics = [line[i:j] for i, j in zip([0]+chord_indexes, chord_indexes+[None])]
chords = chords_waiting_for_lyrics
# First lyric blank means that the line starts with a chord, so remove this blank or insert a blank chord
if lyrics[0] == '':
lyrics.pop(0)
else:
chords.insert(0, Chord())
self.song.lines.append(SongLine(SongLine.LYRICS_CHORDS, lyrics=lyrics, chords=chords))
if not ''.join(lyrics).strip():
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = None
self.song.lines.append(SongLine(SongLine.LYRICS, text=''))
chords_waiting_for_lyrics = None
continue
if not line:
# blank line
if len(self.song.lines) > 1 and not self.song.lines[-1].is_blank: # Add only one INTERNAL_END_BLOC
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = None
self.song.lines.append(SongLine(SongLine.LYRICS, text=line))
continue
# lyrics
self.song.lines.append(SongLine(SongLine.LYRICS, text=line))
if chords_waiting_for_lyrics:
self.song.lines.append(SongLine(SongLine.LYRICS_CHORDS, lyrics=[], chords=chords_waiting_for_lyrics))
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = None
def generate(self, **kwargs):
output = []
for output_song in self.output_songs:
current_bloc_name = None
for song_line in output_song.lines:
if song_line.is_bloc_start:
current_bloc_name = song_line.bloc_name
if current_bloc_name == 'chorus':
output.append(u'Refrain')
continue
if song_line.is_bloc_end:
current_bloc_name = None
continue
if song_line.is_lyrics_chords:
chords_line = u''
lyrics_line = u''
i = 1
for chord, lyric in zip(song_line.chords, song_line.lyrics):
# Transposition
if output_song.transpose:
chord.transpose(output_song.transpose)
if chords_line and not chords_line.endswith(' '): # If the last chord has not spaces after it
chords_line += u' '
chords_line += chord.name + u' ' * (len(lyric) - len(chord.name))
lyrics_line += lyric
# Add auto spaces between chords and lyrics when chords are too closed
shift_value = (len(chord.name) - len(lyric)) + 1
if shift_value > 0 and i != len(song_line.chords): # do not apply if we are at the end of the line
if lyrics_line: # do not insert dash if we are no lyrics
lyrics_line += u' ' * shift_value
chords_line += u' ' * (shift_value - len(chord.name) + 2)
i += 1
output.append(chords_line)
output.append(lyrics_line)
continue
if song_line.is_structure:
if current_bloc_name == 'chorus' and output[-1].lower().startswith(current_bloc_name):
output.pop() # Remove the last item when it is the structure name (chorus:) because we will add it again below
output.append(song_line.text)
continue
if song_line.is_blank:
output.append(song_line.text)
continue
output.append(song_line.text)
return u'\n'.join(output).encode('utf-8')
class AsciiFormat(BaseFormat):
def generate(self, **kwargs):
padding = u' ' * 4
chorus_padding = u' | '
output = []
for output_song in self.output_songs:
current_bloc_name = None
for song_line in output_song.lines:
if song_line.is_bloc_start:
current_bloc_name = song_line.bloc_name
continue
if song_line.is_bloc_end:
current_bloc_name = None
continue
extra_padding = chorus_padding if current_bloc_name == 'chorus' else ''
if song_line.is_lyrics_chords:
chords_line = u''
lyrics_line = u''
i = 1
for chord, lyric in zip(song_line.chords, song_line.lyrics):
# Transposition
if output_song.transpose:
chord.transpose(output_song.transpose)
if chords_line and not chords_line.endswith(' '): # If the last chord has not spaces after it
chords_line += u' '
chords_line += chord.name + u' ' * (len(lyric) - len(chord.name))
lyrics_line += lyric
# Add auto spaces between chords and lyrics when chords are too closed
shift_value = (len(chord.name) - len(lyric)) + 1
if shift_value > 0 and i != len(song_line.chords): # do not apply if we are at the end of the line
if lyrics_line: # do not insert dash if we are no lyrics
lyrics_line += u'-' * shift_value
chords_line += u' ' * (shift_value - len(chord.name) + 2)
i += 1
output.append(padding + extra_padding + chords_line)
output.append(padding + extra_padding + lyrics_line)
continue
if song_line.is_structure:
output.append(song_line.text)
continue
if song_line.is_blank:
output.append(song_line.text)
continue
output.append(padding + extra_padding + song_line.text)
line_size_max = max(map(len, output))
output.insert(0, u'') # Add a blank line
output.insert(0, output_song.note) # Add note
output.insert(0, u'') # Add a blank line
output.insert(0, output_song.chord_key.center(line_size_max)) if output_song.chord_key else None # Add chord_key
output.insert(0, output_song.author.center(line_size_max)) if output_song.author else None # Add author
output.insert(0, output_song.title.upper().center(line_size_max)) # Add title
output.append(u'') # Add a blank line
output.append(u'') # Add a blank line
output.append(output_song.copyright.center(line_size_max)) if output_song.copyright else None # Add copyright
return u'\n'.join(output).encode('utf-8')
class ChordproFormat(BaseFormat):
def parse(self, content):
current_bloc_name = None
content = self.cleanup_content(content)
for line in content.split('\n'):
line = line.strip('\r\n')
if line.startswith('!'):
# OnSong line starts with ! for structure line
line = line[1:] # Remove ! char
m = re.match(r'\{(?:c|comment):(.*)\}', line)
if m:
if re.match(self.get_structure_words_pattern(), m.group(1), re.IGNORECASE):
# structure line
if current_bloc_name is not None:
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = self.get_structure_name(m.group(1))
self.song.lines.append(SongLine(SongLine.INTERNAL_START_BLOC, bloc_name=current_bloc_name))
self.song.lines.append(SongLine(SongLine.STRUCTURE, text=m.group(1)))
continue
else:
# comments
if u'©' in m.group(1):
if m.group(1).startswith(u'©'):
self.song.copyright = m.group(1).replace(u'©', u'').strip()
else:
aka, copyright = m.group(1).split(u'©', 1)
self.song.aka = aka.strip()
self.song.copyright = copyright.strip()
elif u'shir.fr' in m.group(1):
if u' â ' in m.group(1):
note, songbook = m.group(1).split(u' â ', 1)
self.song.note = note.strip()
self.song.songbook = songbook.strip()
else:
self.song.note = self.song.note + m.group(1)
else:
self.song.note = self.song.note + m.group(1)
continue
m = re.match(r'\{(?:soc|start_of_chorus)\}', line)
if m:
# structure line
self.song.lines.append(SongLine(SongLine.INTERNAL_START_BLOC, bloc_name='chorus'))
current_bloc_name = 'chorus'
continue
m = re.match(r'\{(?:eoc|end_of_chorus)\}', line)
if m:
# structure line
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name='chorus'))
current_bloc_name = None
continue
m = re.match(r'\{(.+)\:(.+)\}', line)
if m:
# chordpro directive
command_name = m.group(1)
command_value = m.group(2)
if command_name == 'title' or command_name == 't':
self.song.title = command_value
if command_name == 'subtitle' or command_name == 'st':
self.song.author = command_value
if command_name == 'key': # Not chordpro standard directive
self.song.chord_key = command_value
continue
# lyrics and chords
parts = re.split(r'\[' + ''.join(self.CHORD_PATTERN[:-1]) + '\]', line)
if len(parts) == 1:
# lyrics
self.song.lines.append(SongLine(SongLine.LYRICS, text=line))
continue
# lyrics and chords
# First part blank means that the line starts with a chord
if parts[0] == '':
# do not get the first blank item
chords = [Chord(c) for c in parts[1::2]]
lyrics = parts[2::2]
else:
# add a blank chord
chords = [Chord()] + [Chord(c) for c in parts[1::2]]
lyrics = parts[0::2]
self.song.lines.append(SongLine(SongLine.LYRICS_CHORDS, lyrics=lyrics, chords=chords))
def generate(self, **kwargs):
output = []
for output_song in self.output_songs:
current_bloc_name = None
inside_chorus = False
output.append('{t:%s}' % output_song.title) # Add title
output.append('{st:%s}' % output_song.author) if output_song.author else None # Add author
[output.append('{c:%s}' % note) for note in output_song.note.split('\n') if note] # Add note
for song_line in output_song.lines:
if song_line.is_bloc_start:
current_bloc_name = song_line.bloc_name
if song_line.bloc_name == 'chorus':
output.append('{soc}')
continue
if song_line.is_bloc_end:
current_bloc_name = None
if song_line.bloc_name == 'chorus':
if output[-1] == '': # Remove blank line
output.pop()
output.append('{eoc}')
continue
if song_line.is_lyrics_chords:
lyrics_chords_line = ''
for chord, lyric in zip(song_line.chords, song_line.lyrics):
# Transposition
if output_song.transpose:
chord.transpose(output_song.transpose)
lyrics_chords_line += (u'[%s]' % chord.name if chord.name else '') + lyric
output.append(' '.join(lyrics_chords_line.split())) # Remove multiples spaces
continue
if song_line.is_structure:
if current_bloc_name == 'chorus':
continue # Skip return 'chorus' structure name
if output[-1] != '': # Add blank line before new bloc
output.append('')
output.append('{c:%s}' % song_line.text)
continue
if song_line.is_blank:
if output[-1] != '': # Remove multiple blank lines
output.append(song_line.text)
continue
output.append(song_line.text)
if current_bloc_name == 'chorus': # when song ends with the chorus and without a blank link
output.append('{eoc}')
output.append('') # Add a blank line
return '\n'.join(output).encode('utf-8')
class OnSongFormat(BaseFormat):
def generate(self, **kwargs):
output = []
for output_song in self.output_songs:
current_bloc_name = None
inside_chorus = False
output.append(output_song.title) # Add title
output.append(output_song.author) # Add author
output.append('Key: %s' % output_song.chord_key) if output_song.chord_key else None # Add chord_key
output.append('Tempo: %s' % output_song.bpm) if output_song.bpm else None # Add bpm
output.append('Time: %s' % output_song.meter) if output_song.meter else None # Add meter
output.append('Duration: %s' % output_song.time_length) if output_song.time_length else None # Add time_length
output.append('Keywords: %s' % ', '.join(output_song.themes)) if output_song.themes else None # Add themes
output.append('Copyright: %s' % output_song.copyright) if output_song.copyright else None # Add copyright
output.append(output_song.note) if output_song.note else None # Add note
output.append('') # Add a blank line
for song_line in output_song.lines:
if song_line.is_bloc_start:
current_bloc_name = song_line.bloc_name
continue
if song_line.is_bloc_end:
current_bloc_name = None
continue
if song_line.is_lyrics_chords:
lyrics_chords_line = ''
for chord, lyric in zip(song_line.chords, song_line.lyrics):
# Transposition
if output_song.transpose:
chord.transpose(output_song.transpose)
lyrics_chords_line += (u'[%s]' % chord.name if chord.name else '') + lyric
output.append(' '.join(lyrics_chords_line.split())) # Remove multiples spaces
continue
if song_line.is_structure:
if output[-1] != '': # Add blank line before new bloc
output.append('')
output.append(song_line.text if song_line.text.endswith(':') else '%s:' % song_line.text.replace(':', ''))
continue
if song_line.is_blank:
if output[-1] != '': # Remove multiple blank lines
output.append(song_line.text)
continue
output.append(song_line.text)
return '\n'.join(output).encode('utf-8')
class OpenSongFormat(BaseFormat):
STRUCTURE_MAPPING = dict(
intro='T',
verse='V',
pre_chorus='P',
chorus='C',
bridge='B',
solo='T',
music='T',
end='T'
)
def parse(self, content):
import xml.etree.cElementTree as ET
current_bloc_name = None
chords_waiting_for_lyrics = None
xml = ET.fromstring(content.encode('utf-8'))
self.song.title = xml.find('title').text if xml.find('title') is not None else ''
self.song.author = xml.find('author').text if xml.find('author') is not None else ''
self.song.songbook = xml.find('hymn_number').text if xml.find('hymn_number') is not None else ''
self.song.aka = xml.find('aka').text if xml.find('aka') is not None else ''
self.song.chord_key = xml.find('key').text if xml.find('key') is not None else ''
self.song.bpm = xml.find('tempo').text if xml.find('tempo') is not None else ''
self.song.meter = xml.find('timesig').text if xml.find('timesig') is not None else ''
self.song.copyright = xml.find('copyright').text if xml.find('copyright') is not None else ''
self.song.note = xml.find('user1').text if xml.find('user1') is not None else ''
self.song.themes = xml.find('theme').text.split(',') if xml.find('theme') is not None and xml.find('theme').text else []
for line in xml.find('lyrics').text.split('\n'):
m = re.match(r'\[([%s])(\d*)' % ''.join(self.STRUCTURE_MAPPING.values()), line, re.UNICODE)
if m:
# structure line
bloc_name = [key for key, value in self.STRUCTURE_MAPPING.iteritems() if value==m.group(1)][0]
if current_bloc_name is not None:
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
current_bloc_name = bloc_name
self.song.lines.append(SongLine(SongLine.INTERNAL_START_BLOC, bloc_name=bloc_name))
self.song.lines.append(SongLine(SongLine.STRUCTURE, text=bloc_name.capitalize() + (u' %s' % m.group(2) if m.group(2) else '') + u':'))
continue
if line.startswith('.'):
# chords
if chords_waiting_for_lyrics:
chords = chords_waiting_for_lyrics
self.song.lines.append(SongLine(SongLine.LYRICS_CHORDS, lyrics='', chords=chords))
chords_waiting_for_lyrics = None
chords = []
line = line[1:].strip('\r\n').replace('\t', ' ') # Replace all tabs by spaces
for m in re.finditer(''.join(self.CHORD_PATTERN), line):
chords.append(Chord(m.group(1).strip(), offset=m.start()))
if chords and not chords_waiting_for_lyrics:
chords_waiting_for_lyrics = chords
continue
if line.startswith(' '):
line = line[1:]
if chords_waiting_for_lyrics:
# lyrics and chords
chord_indexes = [c.offset for c in chords_waiting_for_lyrics]
lyrics = [line[i:j] for i, j in zip([0]+chord_indexes, chord_indexes+[None])]
chords = chords_waiting_for_lyrics
# First lyric blank means that the line starts with a chord, so remove this blank or insert a blank chord
if lyrics[0] == '':
lyrics.pop(0)
else:
chords.insert(0, Chord())
self.song.lines.append(SongLine(SongLine.LYRICS_CHORDS, lyrics=lyrics, chords=chords))
chords_waiting_for_lyrics = None
else:
# lyrics
self.song.lines.append(SongLine(SongLine.LYRICS, text=line))
continue
if not line:
# blank line
if len(self.song.lines) > 1 and not self.song.lines[-1].is_blank: # Add only one INTERNAL_END_BLOC
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name=current_bloc_name))
def generate(self, **kwargs):
padding = u' ' * 4
output = StringIO()
for output_song in self.output_songs:
current_bloc_name = None
inside_chorus = False
import xml.etree.cElementTree as ET
song = ET.Element('song')
ET.SubElement(song, 'title').text = output_song.title # Add title
ET.SubElement(song, 'author').text = output_song.author # Add author
ET.SubElement(song, 'hymn_number').text = output_song.songbook # Add songbook
ET.SubElement(song, 'aka').text = output_song.aka # Add aka
ET.SubElement(song, 'key').text = output_song.chord_key # Add chord_key
ET.SubElement(song, 'tempo').text = str(output_song.bpm) if output_song.bpm else '' # Add bpm
ET.SubElement(song, 'timesig').text = output_song.meter # Add meter
ET.SubElement(song, 'user2').text = output_song.arrangement # Add arrangement
ET.SubElement(song, 'copyright').text = output_song.copyright # Add copyright
ET.SubElement(song, 'user1').text = output_song.note # Add note
ET.SubElement(song, 'theme').text = ', '.join(output_song.themes) # Add themes
verse_num = 1
lyrics = []
for song_line in output_song.lines:
if song_line.is_bloc_start:
current_bloc_name = song_line.bloc_name
if current_bloc_name == 'verse':
lyrics.append(u'[%s%s]' % (self.STRUCTURE_MAPPING.get(song_line.bloc_name), verse_num))
verse_num += 1
else:
lyrics.append(u'[%s]' % self.STRUCTURE_MAPPING.get(song_line.bloc_name, ''))
continue
if song_line.is_bloc_end:
current_bloc_name = None
continue
if song_line.is_lyrics_chords:
chords_line = u''
lyrics_line = u''
i = 1
for chord, lyric in zip(song_line.chords, song_line.lyrics):
# Transposition
if output_song.transpose:
chord.transpose(output_song.transpose)
chords_line += chord.name + u' ' * (len(lyric) - len(chord.name))
lyrics_line += lyric
# Add auto spaces between chords and lyrics when chords are too closed
shift_value = (len(chord.name) - len(lyric)) + 1
if shift_value > 0 and i != len(song_line.chords): # do not apply if we are at the end of the line
if lyrics_line: # do not insert dash if we are no lyrics
lyrics_line += u'-' * shift_value
chords_line += u' ' * (shift_value - len(chord.name) + 1)
i += 1
lyrics.append(u'.%s' % padding + chords_line)
lyrics.append(u' %s' % padding + lyrics_line)
continue
if song_line.is_structure:
continue
if song_line.is_blank:
lyrics.append(song_line.text)
lyrics.append(u' %s' % song_line.text)
ET.SubElement(song, 'lyrics').text = '\n'.join(lyrics) # Add lyrics
tree = ET.ElementTree(song)
tree.write(output, encoding='utf-8')
output.seek(0)
return output.read()
class HTMLFormat(BaseFormat):
CSS = """
@page {
size: A4 portrait;
margin: 1cm 1cm 2cm 1cm;
counter-increment: page;
@bottom-right {
content: "Page " counter(page) " sur " counter(pages);
}
}
h1 {
font-size: 28px;
font-weight: bold;
text-transform: uppercase;
margin-top: 0;
margin-bottom: 0;
}
h2 {
margin-top: 0;
}
hr {
margin-top: 0;
border-top: solid 1px #aaa;
}
.logo {
position: fixed;
bottom: -45px;
left: 0;
width: 80px;
}
.songbook {
font-size: 18px;
}
.key {
font-size: 20px;
font-weight: bold;
border: solid 2px #333;
border-radius: 5px;
padding: 2px 6px 2px 6px;
}
.bpm {
font-size: 18px;
position: relative;
top: 5px;
}
table {
font-size: 22px;
line-height: 22px;
margin-bottom: 14px;
}
tr {
white-space: pre-wrap;
}
p {
font-size: 22px;
line-height: 16px;
margin-bottom: 16px;
}
.bloc {
page-break-inside: avoid;
padding: 5px 5px 1px 5px;
}
.spacer {
height: 20px;
}
.structure {
font-size: 16px;
letter-spacing: 3px;
margin-bottom: 6px;
border-bottom: solid 1px #aaa;
}
.pre_chorus {
border-left: double 4px #333;
border-right: double 4px #333;
padding-left: 20px;
padding-right: 20px;
}
.chorus {
border-left: solid 8px #333;
border-right: solid 8px #333;
padding-left: 20px;
padding-right: 20px;
background-color: #ddd !important;
}
.bridge {
border-left: dashed 2px #333;
border-right: dashed 2px #333;
padding-left: 20px;
padding-right: 20px;
}
.intro, .solo, .music, .end {
background-color: #ddd !important;
}
"""
TEMPLATE_LAYOUT = u"""
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
{headers}
<style type="text/css">
@page {{
@bottom-center {{
content: "{pages_footer}";
}}
}}
@page :first {{
@bottom-center {{
content: "{first_page_footer}";
}}
}}
{css}
</style>
</head>
<body>
{content}
<footer style="font-size:10px">
<div class="logo">
<img src="{logo}" />
</div>
</footer>
</body>
</html>
"""
CSS_BLOC = u"""
<link href="{css}" rel="stylesheet" type="text/css" />
"""
TEMPLATE_CONTENT = u"""
<h1 class="text-center">{title}</h1>
<h2 class="text-center">{author}</h2>
<div class="row">
<div class="col-xs-5">{note}</div>
<div class="col-xs-4 text-right"><span class="songbook">{songbook}</span></div>
<div class="col-xs-1 text-right"><span class="key">{chord_key}</span></div>
<div class="col-xs-2 text-right"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAbCAYAAABFuB6DAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AEHDScJ0eY5uQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAACuSURBVDjL7dIxagJBFAbgb4aFCFaxsPYAEey9QCCNjSews0ll6UlyAM8QLKxEu5wghwgkgqvLppkNiwR3Gzv/ZmDmm/eYxwTNGWASW8BHjNvAEmUbCO7wDm8LA2QXmw/o4wU9bJH/6ZQpXvGEIv3sDF/YVxWXWOAbh9rlHF2EiCHmCf2XAj8xtQsND1pFdK6ADB/YRLzVRhXSmuGMNZ5xCulwhFkazRE7vOOzKv0Lgasbg/TgUVcAAAAASUVORK5CYII=" /> <span class="bpm">{bpm} {meter}</span></div>
</div>
<hr/>
{content}
"""
LYRICS_CHORDS_BLOC = u"""
<table>
<tbody>
{content}
</tbody>
</table>
"""
PAGE_BREAK = '<div style="page-break-after:always"></div>'
SPLIT_WORD_STR = ' - '
SPACE_CHORD_STR = ' '
def generate(self, css_files=None, logo_url=None, local_base_href=None, first_page_footer=None, pages_footer=None, **kwargs):
first_page_footer = first_page_footer and first_page_footer.decode('utf-8') or None
pages_footer = pages_footer and pages_footer.decode('utf-8') or None
output = []
for output_song in self.output_songs:
current_bloc_name = None
content = []
for song_line in output_song.lines:
if song_line.is_bloc_start:
current_bloc_name = song_line.bloc_name
content.append('<div class="bloc %s">' % song_line.bloc_name)
continue
if song_line.is_bloc_end:
current_bloc_name = None
content.append('</div>')
continue
if song_line.is_lyrics_chords:
index = 0
for chord, lyric in zip(song_line.chords, song_line.lyrics):
# Transposition
if output_song.transpose:
chord.transpose(output_song.transpose)
# Add auto spaces between chords and lyrics when chords are too closed
if chord.name and (len(chord.name) + 4 > len(lyric)) and index < len(song_line.lyrics) - 1:
song_line.chords[index].spaces_after = ' '
if not song_line.is_end_line_empty(index): # Splitting only when we are not at the end of lyrics line
if song_line.lyrics[index]:
song_line.lyrics[index] += self.SPLIT_WORD_STR
elif index > 0:
song_line.chords[index].spaces_before = self.SPACE_CHORD_STR
index += 1
chords_line = u'<tr>'
for chord in song_line.chords:
chords_line += u'<th>%s%s</th>' % (chord, ' ' if song_line.is_lyrics_empty else '')
chords_line += u'</tr>'
lyrics_line = u'<tr>'
for lyrics in song_line.lyrics:
lyrics_line += u'<td>%s</td>' % lyrics
lyrics_line += u'</tr>'
content.append(self.LYRICS_CHORDS_BLOC.format(content=chords_line + lyrics_line))
continue
if song_line.is_structure:
content.append(u'<p class="structure">%s</p>' % song_line.text)
continue
if song_line.is_blank:
content.append(u'<div class="spacer">%s</div>' % song_line.text)
continue
content.append(u'<p>%s</p>' % song_line.text)
if current_bloc_name is not None:
content.append('</div>')
output.append(self.TEMPLATE_CONTENT.format(content='\n'.join(content), **{key: val or '' for key, val in output_song.get_metadata().iteritems()}))
# import os
# headers = u'<style type="text/css">'
# with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bootstrap.min.css'), 'r') as f:
# headers += f.read()
# headers += u'</style>'
# logo = ''
# if kwargs.get('logo_img_path'):
# with open(kwargs.get('logo_img_path'), 'r') as f:
# logo = 'data:image/png;base64,' + f.read().encode('base64').replace('\n', '')
headers = u''
if local_base_href:
headers += u'<base href="%s" />' % local_base_href
headers += u''.join([self.CSS_BLOC.format(css=css) for css in css_files or []])
output = self.TEMPLATE_LAYOUT.format(content=self.PAGE_BREAK.join(output), logo=logo_url or '', headers=headers, css=self.CSS, first_page_footer=first_page_footer or pages_footer or output_song.copyright, pages_footer=pages_footer or output_song.title)
return output.encode('utf-8')
class PDFFormat(BaseFormat):
def generate(self, *args, **kwargs):
from weasyprint import HTML
import logging
logger = logging.getLogger('weasyprint')
logger.handlers = []
logger.setLevel(logging.CRITICAL)
output = HTMLFormat(self.output_songs).get_output(*args, **kwargs)
doc = HTML(string=output)
return doc.write_pdf()
class LTCFormat(BaseFormat):
def parse(self, content):
from xml.etree import ElementTree
song = ElementTree.fromstring(content.encode('utf-8'))
songbook = ''
songbook_appearance = song.find('songbook_appearance')
if songbook_appearance is not None:
songbook = songbook_appearance.get('songbook')
#songbook = songbook_appearance.get('songbook_reference')
self.song.title = song.get('translated_title1') or song.get('original_title1')
self.song.author = song.get('author1') + (' / %s' % song.get('author2') if song.get('author2') else '')
self.song.songbook = songbook
self.song.aka = song.get('translated_title2') or song.get('original_title2')
self.song.chord_key = Chord(song.get('key'), locale_input='fr', locale_output='international').name
self.song.bpm = song.get('tempo')
self.song.copyright = song.get('copyright_line')
self.song.origin = 'ltc'
for section in song.findall('section'):
bloc_name = section.get('style') # verse
self.song.lines.append(SongLine(SongLine.INTERNAL_START_BLOC, bloc_name='verse'))
for line in section.findall('line'):
lyrics_line = line.get('os')
if not lyrics_line:
lyrics_line = line.text
# lyrics and chords
parts = re.split(r'\[' + ''.join(self.CHORD_PATTERN[:-1]) + '\]', lyrics_line)
if len(parts) == 1:
# lyrics
self.song.lines.append(SongLine(SongLine.LYRICS, text=lyrics_line))
continue
# lyrics and chords
# First part blank means that the line starts with a chord
if parts[0] == '':
# do not get the first blank item
chords = [Chord(c) for c in parts[1::2]]
lyrics = parts[2::2]
else:
# add a blank chord
chords = [Chord()] + [Chord(c) for c in parts[1::2]]
lyrics = parts[0::2]
self.song.lines.append(SongLine(SongLine.LYRICS_CHORDS, lyrics=lyrics, chords=chords))
self.song.lines.append(SongLine(SongLine.LYRICS, text=''))
self.song.lines.append(SongLine(SongLine.INTERNAL_END_BLOC, bloc_name='verse'))
if __name__ == '__main__':
import sys
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--song-file', help='Song file')
parser.add_argument('-f', '--input-format', help='Input song format')
parser.add_argument('-o', '--output-format', default='eliakim', help='Output song format')
parser.add_argument('-t', '--transpose', type=int, help='Perform chords transposition (in semitone)')
parser.add_argument('-l', '--list-formats', action='store_true', default=False, help='List available formats')
args = parser.parse_args()
formats_map = dict(
eliakim=EliakimFormat,
chordpro=ChordproFormat,
opensong=OpenSongFormat,
onsong=OnSongFormat,
ascii=AsciiFormat,
html=HTMLFormat,
pdf=PDFFormat,
)
if args.list_formats:
print '\n'.join(formats_map.keys())
sys.exit(0)
if not formats_map.get(args.input_format):
print 'Unknown input format %s' % args.input_format
sys.exit(1)
if not formats_map.get(args.input_format):
print 'Unknown ouput format %s' % args.output_format
sys.exit(1)
input_song = formats_map.get(args.input_format)().parse_input(content=open(args.song_file, 'r').read())
if args.transpose:
input_song.transpose = args.transpose
print formats_map.get(args.output_format)(songs=[input_song]).get_output()
_______________________________________________
openlp-dev mailing list
[email protected]
https://lists.openlp.io/mailman/listinfo/openlp-dev