This commit is contained in:
Tim Bentley 2014-06-04 05:55:28 +01:00
commit 2e82d92ebc
13 changed files with 554 additions and 31 deletions

View File

@ -278,7 +278,7 @@ class VerseType(object):
if verse_index is None: if verse_index is None:
verse_index = VerseType.from_string(verse_name, default) verse_index = VerseType.from_string(verse_name, default)
elif len(verse_name) == 1: elif len(verse_name) == 1:
verse_index = VerseType.from_translated_tag(verse_name, None) verse_index = VerseType.from_translated_tag(verse_name, default)
if verse_index is None: if verse_index is None:
verse_index = VerseType.from_tag(verse_name, default) verse_index = VerseType.from_tag(verse_name, default)
else: else:

View File

@ -527,15 +527,7 @@ class SongMediaItem(MediaManagerItem):
add_song = True add_song = True
if search_results: if search_results:
for song in search_results: for song in search_results:
author_list = item.data_string['authors'] if self._authors_match(song, item.data_string['authors']):
same_authors = True
for author in song.authors:
if author.display_name in author_list:
author_list = author_list.replace(author.display_name, '', 1)
else:
same_authors = False
break
if same_authors and author_list.strip(', ') == '':
add_song = False add_song = False
edit_id = song.id edit_id = song.id
break break
@ -561,6 +553,23 @@ class SongMediaItem(MediaManagerItem):
self.generate_footer(item, song) self.generate_footer(item, song)
return item return item
def _authors_match(self, song, authors):
"""
Checks whether authors from a song in the database match the authors of the song to be imported.
:param song: A list of authors from the song in the database
:param authors: A string with authors from the song to be imported
:return: True when Authors do match, else False.
"""
author_list = authors.split(', ')
for author in song.authors:
if author.display_name in author_list:
author_list.remove(author.display_name)
else:
return False
# List must be empty at the end
return not author_list
def search(self, string, show_error): def search(self, string, show_error):
""" """
Search for some songs Search for some songs

View File

@ -45,11 +45,11 @@ class OpenSongImport(SongImport):
""" """
Import songs exported from OpenSong Import songs exported from OpenSong
The format is described loosly on the `OpenSong File Format Specification The format is described loosely on the `OpenSong File Format Specification
<http://www.opensong.org/d/manual/song_file_format_specification>`_ page on the OpenSong web site. However, it <http://www.opensong.org/d/manual/song_file_format_specification>`_ page on the OpenSong web site. However, it
doesn't describe the <lyrics> section, so here's an attempt: doesn't describe the <lyrics> section, so here's an attempt:
If the first charachter of a line is a space, then the rest of that line is lyrics. If it is not a space the If the first character of a line is a space, then the rest of that line is lyrics. If it is not a space the
following applies. following applies.
Verses can be expressed in one of 2 ways, either in complete verses, or by line grouping, i.e. grouping all line 1's Verses can be expressed in one of 2 ways, either in complete verses, or by line grouping, i.e. grouping all line 1's
@ -93,12 +93,19 @@ class OpenSongImport(SongImport):
All verses are imported and tagged appropriately. All verses are imported and tagged appropriately.
Guitar chords can be provided "above" the lyrics (the line is preceeded by a period "."), and one or more "_" can Guitar chords can be provided "above" the lyrics (the line is preceded by a period "."), and one or more "_" can
be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example:: be used to signify long-drawn-out words. Chords and "_" are removed by this importer. For example::
. A7 Bm . A7 Bm
1 Some____ Words 1 Some____ Words
Lines that contain only whitespace are ignored.
| indicates a blank line, and || a new slide.
Slide 1 Line 1|Slide 1 Line 2||Slide 2 Line 1|Slide 2 Line 2
Lines beginning with ; are comments
The <presentation> tag is used to populate the OpenLP verse display order field. The Author and Copyright tags are The <presentation> tag is used to populate the OpenLP verse display order field. The Author and Copyright tags are
also imported to the appropriate places. also imported to the appropriate places.
""" """
@ -107,9 +114,14 @@ class OpenSongImport(SongImport):
""" """
Initialise the class. Initialise the class.
""" """
SongImport.__init__(self, manager, **kwargs) super(OpenSongImport, self).__init__(manager, **kwargs)
def do_import(self): def do_import(self):
"""
Receive a single file or a list of files to import.
"""
if not isinstance(self.import_source, list):
return
self.import_wizard.progress_bar.setMaximum(len(self.import_source)) self.import_wizard.progress_bar.setMaximum(len(self.import_source))
for filename in self.import_source: for filename in self.import_source:
if self.stop_import_flag: if self.stop_import_flag:
@ -141,19 +153,41 @@ class OpenSongImport(SongImport):
'author': self.parse_author, 'author': self.parse_author,
'title': 'title', 'title': 'title',
'aka': 'alternate_title', 'aka': 'alternate_title',
'hymn_number': 'song_number' 'hymn_number': self.parse_song_book_name_and_number,
'user1': self.add_comment,
'user2': self.add_comment,
'user3': self.add_comment
} }
for attr, fn_or_string in list(decode.items()): for attr, fn_or_string in list(decode.items()):
if attr in fields: if attr in fields:
ustring = str(root.__getattr__(attr)) ustring = str(root.__getattr__(attr))
if isinstance(fn_or_string, str): if isinstance(fn_or_string, str):
setattr(self, fn_or_string, ustring) if attr in ['ccli']:
if ustring:
setattr(self, fn_or_string, int(ustring))
else:
setattr(self, fn_or_string, None)
else:
setattr(self, fn_or_string, ustring)
else: else:
fn_or_string(ustring) fn_or_string(ustring)
if 'theme' in fields and str(root.theme) not in self.topics: # Themes look like "God: Awe/Wonder", but we just want
self.topics.append(str(root.theme)) # "Awe" and "Wonder". We use a set to ensure each topic
if 'alttheme' in fields and str(root.alttheme) not in self.topics: # is only added once, in case it is already there, which
self.topics.append(str(root.alttheme)) # is actually quite likely if the alttheme is set
topics = set(self.topics)
if 'theme' in fields:
theme = str(root.theme)
subthemes = theme[theme.find(':')+1:].split('/')
for topic in subthemes:
topics.add(topic.strip())
if 'alttheme' in fields:
theme = str(root.alttheme)
subthemes = theme[theme.find(':')+1:].split('/')
for topic in subthemes:
topics.add(topic.strip())
self.topics = list(topics)
self.topics.sort()
# data storage while importing # data storage while importing
verses = {} verses = {}
# keep track of verses appearance order # keep track of verses appearance order
@ -168,7 +202,7 @@ class OpenSongImport(SongImport):
else: else:
lyrics = '' lyrics = ''
for this_line in lyrics.split('\n'): for this_line in lyrics.split('\n'):
if not this_line: if not this_line.strip():
continue continue
# skip this line if it is a comment # skip this line if it is a comment
if this_line.startswith(';'): if this_line.startswith(';'):
@ -209,8 +243,14 @@ class OpenSongImport(SongImport):
# Tidy text and remove the ____s from extended words # Tidy text and remove the ____s from extended words
this_line = self.tidy_text(this_line) this_line = self.tidy_text(this_line)
this_line = this_line.replace('_', '') this_line = this_line.replace('_', '')
this_line = this_line.replace('|', '\n') this_line = this_line.replace('||', '\n[---]\n')
this_line = this_line.strip() this_line = this_line.strip()
# If the line consists solely of a '|', then just use the implicit newline
# Otherwise, add a newline for each '|'
if this_line == '|':
this_line = ''
else:
this_line = this_line.replace('|', '\n')
verses[verse_tag][verse_num][inst].append(this_line) verses[verse_tag][verse_num][inst].append(this_line)
# done parsing # done parsing
# add verses in original order # add verses in original order
@ -223,7 +263,14 @@ class OpenSongImport(SongImport):
verse_def = '%s%s' % (verse_tag, verse_num[:length]) verse_def = '%s%s' % (verse_tag, verse_num[:length])
verse_joints[verse_def] = '%s\n[---]\n%s' % (verse_joints[verse_def], lines) \ verse_joints[verse_def] = '%s\n[---]\n%s' % (verse_joints[verse_def], lines) \
if verse_def in verse_joints else lines if verse_def in verse_joints else lines
for verse_def, lines in verse_joints.items(): # Parsing the dictionary produces the elements in a non-intuitive order. While it "works", it's not a
# natural layout should the user come back to edit the song. Instead we sort by the verse type, so that we
# get all the verses in order (v1, v2, ...), then the chorus(es), bridge(s), pre-chorus(es) etc. We use a
# tuple for the key, since tuples naturally sort in this manner.
verse_defs = sorted(verse_joints.keys(),
key=lambda verse_def: (VerseType.from_tag(verse_def[0]), int(verse_def[1:])))
for verse_def in verse_defs:
lines = verse_joints[verse_def]
self.add_verse(lines, verse_def) self.add_verse(lines, verse_def)
if not self.verses: if not self.verses:
self.add_verse('') self.add_verse('')
@ -244,6 +291,8 @@ class OpenSongImport(SongImport):
# Assume it's no.1 if there are no digits # Assume it's no.1 if there are no digits
verse_tag = verse_def verse_tag = verse_def
verse_num = '1' verse_num = '1'
verse_index = VerseType.from_loose_input(verse_tag)
verse_tag = VerseType.tags[verse_index]
verse_def = '%s%s' % (verse_tag, verse_num) verse_def = '%s%s' % (verse_tag, verse_num)
if verse_num in verses.get(verse_tag, {}): if verse_num in verses.get(verse_tag, {}):
self.verse_order_list.append(verse_def) self.verse_order_list.append(verse_def)

View File

@ -188,13 +188,61 @@ class SongImport(QtCore.QObject):
self.title = lines[0] self.title = lines[0]
self.add_verse(text) self.add_verse(text)
def parse_song_book_name_and_number(self, book_and_number):
"""
Build the book name and song number from a single string
"""
# Turn 'Spring Harvest 1997 No. 34' or
# 'Spring Harvest 1997 (34)' or
# 'Spring Harvest 1997 34' into
# Book name:'Spring Harvest 1997' and
# Song number: 34
#
# Also, turn 'NRH231.' into
# Book name:'NRH' and
# Song number: 231
book_and_number = book_and_number.strip()
if not book_and_number:
return
book_and_number = book_and_number.replace('No.', ' ')
if ' ' in book_and_number:
parts = book_and_number.split(' ')
self.song_book_name = ' '.join(parts[:-1])
self.song_number = parts[-1].strip('()')
else:
# Something like 'ABC123.'
match = re.match(r'(.*\D)(\d+)', book_and_number)
match_num = re.match(r'(\d+)', book_and_number)
if match:
# Name and number
self.song_book_name = match.group(1)
self.song_number = match.group(2)
# These last two cases aren't tested yet, but
# are here in an attempt to do something vaguely
# sensible if we get a string in a different format
elif match_num:
# Number only
self.song_number = match_num.group(1)
else:
# Name only
self.song_book_name = book_and_number
def add_comment(self, comment):
"""
Build the comments field
"""
if self.comments.find(comment) >= 0:
return
if comment:
self.comments += comment.strip() + '\n'
def add_copyright(self, copyright): def add_copyright(self, copyright):
""" """
Build the copyright field Build the copyright field
""" """
if self.copyright.find(copyright) >= 0: if self.copyright.find(copyright) >= 0:
return return
if self.copyright != '': if self.copyright:
self.copyright += ' ' self.copyright += ' '
self.copyright += copyright self.copyright += copyright

View File

@ -153,3 +153,52 @@ class TestMediaItem(TestCase, TestMixin):
# THEN: The songbook should be in the footer # THEN: The songbook should be in the footer
self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12']) self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12'])
def authors_match_test(self):
"""
Test the author matching when importing a song from a service
"""
# GIVEN: A song and a string with authors
song = MagicMock()
song.authors = []
author = MagicMock()
author.display_name = "Hans Wurst"
song.authors.append(author)
author2 = MagicMock()
author2.display_name = "Max Mustermann"
song.authors.append(author2)
# There are occasions where an author appears twice in a song (with different types).
# We need to make sure that this case works (lp#1313538)
author3 = MagicMock()
author3.display_name = "Max Mustermann"
song.authors.append(author3)
authors_str = "Hans Wurst, Max Mustermann, Max Mustermann"
# WHEN: Checking for matching
result = self.media_item._authors_match(song, authors_str)
# THEN: They should match
self.assertTrue(result, "Authors should match")
def authors_dont_match_test(self):
# GIVEN: A song and a string with authors
song = MagicMock()
song.authors = []
author = MagicMock()
author.display_name = "Hans Wurst"
song.authors.append(author)
author2 = MagicMock()
author2.display_name = "Max Mustermann"
song.authors.append(author2)
# There are occasions where an author appears twice in a song (with different types).
# We need to make sure that this case works (lp#1313538)
author3 = MagicMock()
author3.display_name = "Max Mustermann"
song.authors.append(author3)
# WHEN: An author is missing in the string
authors_str = "Hans Wurst, Max Mustermann"
result = self.media_item._authors_match(song, authors_str)
# THEN: They should not match
self.assertFalse(result, "Authors should not match")

View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2014 Raoul Snyman #
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This module contains tests for the OpenSong song importer.
"""
import os
from unittest import TestCase
from tests.helpers.songfileimport import SongImportTestHelper
from openlp.plugins.songs.lib.opensongimport import OpenSongImport
from tests.functional import patch, MagicMock
TEST_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'opensongsongs'))
class TestOpenSongFileImport(SongImportTestHelper):
def __init__(self, *args, **kwargs):
self.importer_class_name = 'OpenSongImport'
self.importer_module_name = 'opensongimport'
super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
def test_song_import(self):
"""
Test that loading an OpenSong file works correctly on various files
"""
self.file_import(os.path.join(TEST_PATH, 'Amazing Grace'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
self.file_import(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer'),
self.load_external_result_data(os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer.json')))
self.file_import(os.path.join(TEST_PATH, 'One, Two, Three, Four, Five'),
self.load_external_result_data(os.path.join(TEST_PATH, 'One, Two, Three, Four, Five.json')))
class TestOpenSongImport(TestCase):
"""
Test the functions in the :mod:`opensongimport` module.
"""
def create_importer_test(self):
"""
Test creating an instance of the OpenSong file importer
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.opensongimport.SongImport'):
mocked_manager = MagicMock()
# WHEN: An importer object is created
importer = OpenSongImport(mocked_manager, filenames=[])
# THEN: The importer object should not be None
self.assertIsNotNone(importer, 'Import should not be none')
def invalid_import_source_test(self):
"""
Test OpenSongImport.do_import handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.opensongimport.SongImport'):
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
importer = OpenSongImport(mocked_manager, filenames=[])
importer.import_wizard = mocked_import_wizard
importer.stop_import_flag = True
# WHEN: Import source is not a list
for source in ['not a list', 0]:
importer.import_source = source
# THEN: do_import should return none and the progress bar maximum should not be set.
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
'setMaximum on import_wizard.progress_bar should not have been called')
def valid_import_source_test(self):
"""
Test OpenSongImport.do_import handles different invalid import_source values
"""
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
with patch('openlp.plugins.songs.lib.opensongimport.SongImport'):
mocked_manager = MagicMock()
mocked_import_wizard = MagicMock()
importer = OpenSongImport(mocked_manager, filenames=[])
importer.import_wizard = mocked_import_wizard
importer.stop_import_flag = True
# WHEN: Import source is a list
importer.import_source = ['List', 'of', 'files']
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
# import_source.
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is a list '
'and stop_import_flag is True')
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))

View File

@ -33,7 +33,7 @@ song files from third party applications.
import json import json
from unittest import TestCase from unittest import TestCase
from tests.functional import patch, MagicMock from tests.functional import patch, MagicMock, call
class SongImportTestHelper(TestCase): class SongImportTestHelper(TestCase):
@ -56,13 +56,13 @@ class SongImportTestHelper(TestCase):
'openlp.plugins.songs.lib.%s.%s.add_verse' % (self.importer_module_name, self.importer_class_name)) 'openlp.plugins.songs.lib.%s.%s.add_verse' % (self.importer_module_name, self.importer_class_name))
self.finish_patcher = patch( self.finish_patcher = patch(
'openlp.plugins.songs.lib.%s.%s.finish' % (self.importer_module_name, self.importer_class_name)) 'openlp.plugins.songs.lib.%s.%s.finish' % (self.importer_module_name, self.importer_class_name))
self.parse_author_patcher = patch( self.add_author_patcher = patch(
'openlp.plugins.songs.lib.%s.%s.parse_author' % (self.importer_module_name, self.importer_class_name)) 'openlp.plugins.songs.lib.%s.%s.add_author' % (self.importer_module_name, self.importer_class_name))
self.song_import_patcher = patch('openlp.plugins.songs.lib.%s.SongImport' % self.importer_module_name) self.song_import_patcher = patch('openlp.plugins.songs.lib.%s.SongImport' % self.importer_module_name)
self.mocked_add_copyright = self.add_copyright_patcher.start() self.mocked_add_copyright = self.add_copyright_patcher.start()
self.mocked_add_verse = self.add_verse_patcher.start() self.mocked_add_verse = self.add_verse_patcher.start()
self.mocked_finish = self.finish_patcher.start() self.mocked_finish = self.finish_patcher.start()
self.mocked_parse_author = self.parse_author_patcher.start() self.mocked_add_author = self.add_author_patcher.start()
self.mocked_song_importer = self.song_import_patcher.start() self.mocked_song_importer = self.song_import_patcher.start()
self.mocked_manager = MagicMock() self.mocked_manager = MagicMock()
self.mocked_import_wizard = MagicMock() self.mocked_import_wizard = MagicMock()
@ -75,7 +75,7 @@ class SongImportTestHelper(TestCase):
self.add_copyright_patcher.stop() self.add_copyright_patcher.stop()
self.add_verse_patcher.stop() self.add_verse_patcher.stop()
self.finish_patcher.stop() self.finish_patcher.stop()
self.parse_author_patcher.stop() self.add_author_patcher.stop()
self.song_import_patcher.stop() self.song_import_patcher.stop()
def load_external_result_data(self, file_name): def load_external_result_data(self, file_name):
@ -112,14 +112,17 @@ class SongImportTestHelper(TestCase):
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed') self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title)) self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title))
for author in author_calls: for author in author_calls:
self.mocked_parse_author.assert_any_call(author) self.mocked_add_author.assert_any_call(author)
if song_copyright: if song_copyright:
self.mocked_add_copyright.assert_called_with(song_copyright) self.mocked_add_copyright.assert_called_with(song_copyright)
if ccli_number: if ccli_number:
self.assertEqual(importer.ccli_number, ccli_number, self.assertEqual(importer.ccli_number, ccli_number,
'ccli_number for %s should be %s' % (source_file_name, ccli_number)) 'ccli_number for %s should be %s' % (source_file_name, ccli_number))
expected_calls = []
for verse_text, verse_tag in add_verse_calls: for verse_text, verse_tag in add_verse_calls:
self.mocked_add_verse.assert_any_call(verse_text, verse_tag) self.mocked_add_verse.assert_any_call(verse_text, verse_tag)
expected_calls.append(call(verse_text, verse_tag))
self.mocked_add_verse.assert_has_calls(expected_calls, any_order=False)
if topics: if topics:
self.assertEqual(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics)) self.assertEqual(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics))
if comments: if comments:
@ -132,7 +135,7 @@ class SongImportTestHelper(TestCase):
self.assertEqual(importer.song_number, song_number, self.assertEqual(importer.song_number, song_number,
'song_number for %s should be %s' % (source_file_name, song_number)) 'song_number for %s should be %s' % (source_file_name, song_number))
if verse_order_list: if verse_order_list:
self.assertEqual(importer.verse_order_list, [], self.assertEqual(importer.verse_order_list, verse_order_list,
'verse_order_list for %s should be %s' % (source_file_name, verse_order_list)) 'verse_order_list for %s should be %s' % (source_file_name, verse_order_list))
self.mocked_finish.assert_called_with() self.mocked_finish.assert_called_with()

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<song>
<title>Amazing Grace (Demonstration)</title>
<author>John Newton, Edwin Excell &amp; John P. Rees</author>
<copyright>Public Domain </copyright>
<presentation>V1 V2 V3 V4 V5</presentation>
<capo print="false"></capo>
<tempo></tempo>
<ccli>22025</ccli>
<theme>God: Assurance/Grace/Salvation</theme>
<alttheme>Worship: Praise</alttheme>
<user1> </user1>
<user2> </user2>
<user3> </user3>
<lyrics>[V]
;Test the chords format
;Chords beging with .
;Verses begin with their verse number
;Link words with _
;Comments begin with ;
. D D7 G D
1A______ma________zing grace! How sweet the sound!
2'Twas grace that taught my heart to fear,
3The Lord has pro____mised good to me,
4Thro' ma________ny dan____gers, toils and snares
5When we've been there ten thou__sand years,
. Bm E A A7
1That saved a wretch like me!
2And grace my fears re___lieved.
3His Word my hope se___cures.
4I have al___rea____dy come.
5Bright shi___ning as the sun,
. D D7 G D
1I once was lost, but now am found;
2How pre___cious did that grace ap____pear,
3He will my shield and por___tion be
4'Tis grace that brought me safe thus far,
5We've no less days to sing God's praise,
. Bm A G D
1Was blind, but now I see.
2The hour I first be_lieved.
3As long as life en_dures.
4And grace will lead me home.
5Than when we first be_gun.
</lyrics>
<hymn_number>Demonstration Songs 0</hymn_number>
<key></key>
<aka></aka>
<key_line></key_line>
<time_sig></time_sig>
<style index="default_style"></style>
</song>

View File

@ -0,0 +1,42 @@
{
"authors": [
"John Newton",
"Edwin Excell",
"John P. Rees"
],
"ccli_number": 22025,
"comments": "\n\n\n",
"copyright": "Public Domain ",
"song_book_name": "Demonstration Songs",
"song_number": 0,
"title": "Amazing Grace (Demonstration)",
"topics": [
"Assurance",
"Grace",
"Praise",
"Salvation"
],
"verse_order_list": [],
"verses": [
[
"Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
"v1"
],
[
"'Twas grace that taught my heart to fear,\nAnd grace my fears relieved.\nHow precious did that grace appear,\nThe hour I first believed.",
"v2"
],
[
"The Lord has promised good to me,\nHis Word my hope secures.\nHe will my shield and portion be\nAs long as life endures.",
"v3"
],
[
"Thro' many dangers, toils and snares\nI have already come.\n'Tis grace that brought me safe thus far,\nAnd grace will lead me home.",
"v4"
],
[
"When we've been there ten thousand years,\nBright shining as the sun,\nWe've no less days to sing God's praise,\nThan when we first begun.",
"v5"
]
]
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<song>
<title>Beautiful Garden Of Prayer (Demonstration)</title>
<author>Eleanor Allen Schroll &amp; James H. Fillmore</author>
<copyright>Public Domain </copyright>
<presentation>V1 C V2 C V3 C</presentation>
<capo print="false"></capo>
<tempo></tempo>
<ccli>60252</ccli>
<theme>God: Prayer/Devotion</theme>
<alttheme>Prayer: Prayer/Devotion</alttheme>
<user1></user1>
<user2></user2>
<user3></user3>
<lyrics>
;Test breaks and newlines
;A single | on the end of a line adds an extra \n
;Blank lines are ignored, even with a space prefix
;We also check that the chorus is added after the verses, despite the order in the file
[V1]
There's a garden where Jesus is waiting,
There's a place that is wondrously fair.
For it glows with the light of His presence,|
'Tis the beautiful garden of prayer.
;A double || on a line adds a new slide
[C]
O the beautiful garden, the garden of prayer,
O the beautiful garden of prayer.
There my Savior awaits, and He opens the gates
||
To the beautiful garden of prayer.
;A double || on the end of a line adds a new slide
[V2]
There's a garden where Jesus is waiting,
And I go with my burden and care.
Just to learn from His lips, words of comfort,||
In the beautiful garden of prayer.
;A single | on a line adds just one line break
[V3]
There's a garden where Jesus is waiting,
And He bids you to come meet Him there,
Just to bow and receive a new blessing,
|
In the beautiful garden of prayer.
</lyrics>
<hymn_number>DS0</hymn_number>
<key></key>
<aka></aka>
<key_line></key_line>
<time_sig></time_sig>
<style index="default_style"></style>
</song>

View File

@ -0,0 +1,35 @@
{
"authors": [
"Eleanor Allen Schroll",
"James H. Fillmore"
],
"ccli_number": 60252,
"comments": "",
"copyright": "Public Domain ",
"song_book_name": "DS",
"song_number": 0,
"title": "Beautiful Garden Of Prayer (Demonstration)",
"topics": [
"Devotion",
"Prayer"
],
"verse_order_list": ["v1", "c1", "v2", "c1", "v3", "c1"],
"verses": [
[
"There's a garden where Jesus is waiting,\nThere's a place that is wondrously fair.\nFor it glows with the light of His presence,\n\n'Tis the beautiful garden of prayer.",
"v1"
],
[
"There's a garden where Jesus is waiting,\nAnd I go with my burden and care.\nJust to learn from His lips, words of comfort,\n[---]\nIn the beautiful garden of prayer.",
"v2"
],
[
"There's a garden where Jesus is waiting,\nAnd He bids you to come meet Him there,\nJust to bow and receive a new blessing,\n\nIn the beautiful garden of prayer.",
"v3"
],
[
"O the beautiful garden, the garden of prayer,\nO the beautiful garden of prayer.\nThere my Savior awaits, and He opens the gates\n[---]\nTo the beautiful garden of prayer.",
"c1"
]
]
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<song>
<title>12345</title>
<author>Traditional</author>
<copyright>Public Domain </copyright>
<presentation>T</presentation>
<capo print="false"></capo>
<ccli></ccli>
<tempo></tempo>
<theme></theme>
<alttheme></alttheme>
<user1></user1>
<user2></user2>
<user3></user3>
<lyrics>
;Test [T]ag element - should be turned into [o]ther
;And lines beginning with numbers
;And a title that contains only numeric characters
;That isdiffernt to the filename
;And most elements are empty
[T]
1, 2, 3, 4, 5,
Once I caught a fish alive.
6, 7, 8, 9, 10,
Then I let it go again.
Why did you let it go?
Because it bit my finger so.
Which finger did it bite?
This little finger on my right.
</lyrics>
<hymn_number></hymn_number>
<key></key>
<aka></aka>
<key_line></key_line>
<time_sig></time_sig>
<style index="default_style"></style>
</song>

View File

@ -0,0 +1,17 @@
{
"authors": [
"Traditional"
],
"comments": "",
"copyright": "Public Domain ",
"title": "12345",
"topics": [
],
"verse_order_list": ["o1"],
"verses": [
[
"1, 2, 3, 4, 5,\nOnce I caught a fish alive.\n6, 7, 8, 9, 10,\nThen I let it go again.\nWhy did you let it go?\nBecause it bit my finger so.\nWhich finger did it bite?\nThis little finger on my right.",
"o1"
]
]
}