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}&nbsp;&nbsp;{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 = '&nbsp;-&nbsp;'
    SPACE_CHORD_STR = '&nbsp;&nbsp;&nbsp;'

    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 = '&nbsp;'
                            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, '&nbsp;&nbsp;&nbsp;&nbsp;' 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

Reply via email to