forked from openlp/openlp
Head
This commit is contained in:
commit
234ba34a5b
|
@ -246,7 +246,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
||||||
Settings().setValue('core/application version', openlp_version)
|
Settings().setValue('core/application version', openlp_version)
|
||||||
# If data_version is different from the current version ask if we should backup the data folder
|
# If data_version is different from the current version ask if we should backup the data folder
|
||||||
elif data_version != openlp_version:
|
elif data_version != openlp_version:
|
||||||
if self.splash.isVisible():
|
if can_show_splash and self.splash.isVisible():
|
||||||
self.splash.hide()
|
self.splash.hide()
|
||||||
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
||||||
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
|
||||||
|
|
|
@ -24,7 +24,7 @@ The :mod:`common` module contains most of the components and libraries that make
|
||||||
OpenLP work.
|
OpenLP work.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -32,6 +32,7 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
from chardet.universaldetector import UniversalDetector
|
from chardet.universaldetector import UniversalDetector
|
||||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||||
|
from pathlib import Path
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from subprocess import check_output, CalledProcessError, STDOUT
|
from subprocess import check_output, CalledProcessError, STDOUT
|
||||||
|
|
||||||
|
@ -79,6 +80,49 @@ def check_directory_exists(directory, do_not_log=False):
|
||||||
log.exception('failed to check if directory exists or create directory')
|
log.exception('failed to check if directory exists or create directory')
|
||||||
|
|
||||||
|
|
||||||
|
def extension_loader(glob_pattern, excluded_files=[]):
|
||||||
|
"""
|
||||||
|
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
|
||||||
|
importers.
|
||||||
|
|
||||||
|
:param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
|
||||||
|
application directory. i.e. openlp/plugins/*/*plugin.py
|
||||||
|
:type glob_pattern: str
|
||||||
|
|
||||||
|
:param excluded_files: A list of file names to exclude that the glob pattern may find.
|
||||||
|
:type excluded_files: list of strings
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent
|
||||||
|
for extension_path in app_dir.glob(glob_pattern):
|
||||||
|
extension_path = extension_path.relative_to(app_dir)
|
||||||
|
if extension_path.name in excluded_files:
|
||||||
|
continue
|
||||||
|
module_name = path_to_module(extension_path)
|
||||||
|
try:
|
||||||
|
importlib.import_module(module_name)
|
||||||
|
except (ImportError, OSError):
|
||||||
|
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
|
||||||
|
log.warning('Failed to import {module_name} on path {extension_path}'
|
||||||
|
.format(module_name=module_name, extension_path=str(extension_path)))
|
||||||
|
|
||||||
|
|
||||||
|
def path_to_module(path):
|
||||||
|
"""
|
||||||
|
Convert a path to a module name (i.e openlp.core.common)
|
||||||
|
|
||||||
|
:param path: The path to convert to a module name.
|
||||||
|
:type path: Path
|
||||||
|
|
||||||
|
:return: The module name.
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
module_path = path.with_suffix('')
|
||||||
|
return '.'.join(module_path.parts)
|
||||||
|
|
||||||
|
|
||||||
def get_frozen_path(frozen_option, non_frozen_option):
|
def get_frozen_path(frozen_option, non_frozen_option):
|
||||||
"""
|
"""
|
||||||
Return a path based on the system status.
|
Return a path based on the system status.
|
||||||
|
|
|
@ -252,4 +252,5 @@ def url_get_file(callback, url, f_path, sha256=None):
|
||||||
os.remove(f_path)
|
os.remove(f_path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['get_web_page']
|
__all__ = ['get_web_page']
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
The :mod:`lib` module contains most of the components and libraries that make
|
The :mod:`lib` module contains most of the components and libraries that make
|
||||||
OpenLP work.
|
OpenLP work.
|
||||||
"""
|
"""
|
||||||
|
import html
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from distutils.version import LooseVersion
|
import re
|
||||||
|
import math
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||||
|
|
||||||
|
@ -34,6 +35,8 @@ from openlp.core.common import translate
|
||||||
|
|
||||||
log = logging.getLogger(__name__ + '.__init__')
|
log = logging.getLogger(__name__ + '.__init__')
|
||||||
|
|
||||||
|
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||||
|
|
||||||
|
|
||||||
class ServiceItemContext(object):
|
class ServiceItemContext(object):
|
||||||
"""
|
"""
|
||||||
|
@ -281,11 +284,12 @@ def check_item_selected(list_widget, message):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def clean_tags(text):
|
def clean_tags(text, remove_chords=False):
|
||||||
"""
|
"""
|
||||||
Remove Tags from text for display
|
Remove Tags from text for display
|
||||||
|
|
||||||
:param text: Text to be cleaned
|
:param text: Text to be cleaned
|
||||||
|
:param remove_chords: Clean ChordPro tags
|
||||||
"""
|
"""
|
||||||
text = text.replace('<br>', '\n')
|
text = text.replace('<br>', '\n')
|
||||||
text = text.replace('{br}', '\n')
|
text = text.replace('{br}', '\n')
|
||||||
|
@ -293,21 +297,296 @@ def clean_tags(text):
|
||||||
for tag in FormattingTags.get_html_tags():
|
for tag in FormattingTags.get_html_tags():
|
||||||
text = text.replace(tag['start tag'], '')
|
text = text.replace(tag['start tag'], '')
|
||||||
text = text.replace(tag['end tag'], '')
|
text = text.replace(tag['end tag'], '')
|
||||||
|
# Remove ChordPro tags
|
||||||
|
if remove_chords:
|
||||||
|
text = re.sub(r'\[.+?\]', r'', text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def expand_tags(text):
|
def expand_tags(text, expand_chord_tags=False, for_printing=False):
|
||||||
"""
|
"""
|
||||||
Expand tags HTML for display
|
Expand tags HTML for display
|
||||||
|
|
||||||
:param text: The text to be expanded.
|
:param text: The text to be expanded.
|
||||||
"""
|
"""
|
||||||
|
if expand_chord_tags:
|
||||||
|
if for_printing:
|
||||||
|
text = expand_chords_for_printing(text, '{br}')
|
||||||
|
else:
|
||||||
|
text = expand_chords(text)
|
||||||
for tag in FormattingTags.get_html_tags():
|
for tag in FormattingTags.get_html_tags():
|
||||||
text = text.replace(tag['start tag'], tag['start html'])
|
text = text.replace(tag['start tag'], tag['start html'])
|
||||||
text = text.replace(tag['end tag'], tag['end html'])
|
text = text.replace(tag['end tag'], tag['end html'])
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def expand_and_align_chords_in_line(match):
|
||||||
|
"""
|
||||||
|
Expand the chords in the line and align them using whitespaces.
|
||||||
|
NOTE: There is equivalent javascript code in chords.js, in the updateSlide function. Make sure to update both!
|
||||||
|
|
||||||
|
:param match:
|
||||||
|
:return: The line with expanded html-chords
|
||||||
|
"""
|
||||||
|
whitespaces = ''
|
||||||
|
chordlen = 0
|
||||||
|
taillen = 0
|
||||||
|
# The match could be "[G]sweet the " from a line like "A[D]mazing [D7]grace! How [G]sweet the [D]sound!"
|
||||||
|
# The actual chord, would be "G" in match "[G]sweet the "
|
||||||
|
chord = match.group(1)
|
||||||
|
# The tailing word of the chord, would be "sweet" in match "[G]sweet the "
|
||||||
|
tail = match.group(2)
|
||||||
|
# The remainder of the line, until line end or next chord. Would be " the " in match "[G]sweet the "
|
||||||
|
remainder = match.group(3)
|
||||||
|
# Line end if found, else None
|
||||||
|
end = match.group(4)
|
||||||
|
# Based on char width calculate width of chord
|
||||||
|
for chord_char in chord:
|
||||||
|
if chord_char not in SLIMCHARS:
|
||||||
|
chordlen += 2
|
||||||
|
else:
|
||||||
|
chordlen += 1
|
||||||
|
# Based on char width calculate width of tail
|
||||||
|
for tail_char in tail:
|
||||||
|
if tail_char not in SLIMCHARS:
|
||||||
|
taillen += 2
|
||||||
|
else:
|
||||||
|
taillen += 1
|
||||||
|
# Based on char width calculate width of remainder
|
||||||
|
for remainder_char in remainder:
|
||||||
|
if remainder_char not in SLIMCHARS:
|
||||||
|
taillen += 2
|
||||||
|
else:
|
||||||
|
taillen += 1
|
||||||
|
# If the chord is wider than the tail+remainder and the line goes on, some padding is needed
|
||||||
|
if chordlen >= taillen and end is None:
|
||||||
|
# Decide if the padding should be "_" for drawing out words or spaces
|
||||||
|
if tail:
|
||||||
|
if not remainder:
|
||||||
|
for c in range(math.ceil((chordlen - taillen) / 2) + 2):
|
||||||
|
whitespaces += '_'
|
||||||
|
else:
|
||||||
|
for c in range(chordlen - taillen + 1):
|
||||||
|
whitespaces += ' '
|
||||||
|
else:
|
||||||
|
if not remainder:
|
||||||
|
for c in range(math.floor((chordlen - taillen) / 2)):
|
||||||
|
whitespaces += '_'
|
||||||
|
else:
|
||||||
|
for c in range(chordlen - taillen + 1):
|
||||||
|
whitespaces += ' '
|
||||||
|
else:
|
||||||
|
if not tail and remainder and remainder[0] == ' ':
|
||||||
|
for c in range(chordlen):
|
||||||
|
whitespaces += ' '
|
||||||
|
if whitespaces:
|
||||||
|
if '_' in whitespaces:
|
||||||
|
ws_length = len(whitespaces)
|
||||||
|
if ws_length == 1:
|
||||||
|
whitespaces = '–'
|
||||||
|
else:
|
||||||
|
wsl_mod = ws_length // 2
|
||||||
|
ws_right = ws_left = ' ' * wsl_mod
|
||||||
|
whitespaces = ws_left + '–' + ws_right
|
||||||
|
whitespaces = '<span class="ws">' + whitespaces + '</span>'
|
||||||
|
return '<span class="chord"><span><strong>' + html.escape(chord) + '</strong></span></span>' + html.escape(tail) + \
|
||||||
|
whitespaces + html.escape(remainder)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_chords(text):
|
||||||
|
"""
|
||||||
|
Expand ChordPro tags
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
"""
|
||||||
|
text_lines = text.split('{br}')
|
||||||
|
expanded_text_lines = []
|
||||||
|
chords_on_prev_line = False
|
||||||
|
for line in text_lines:
|
||||||
|
# If a ChordPro is detected in the line, replace it with a html-span tag and wrap the line in a span tag.
|
||||||
|
if '[' in line and ']' in line:
|
||||||
|
if chords_on_prev_line:
|
||||||
|
new_line = '<span class="chordline">'
|
||||||
|
else:
|
||||||
|
new_line = '<span class="chordline firstchordline">'
|
||||||
|
chords_on_prev_line = True
|
||||||
|
# Matches a chord, a tail, a remainder and a line end. See expand_and_align_chords_in_line() for more info.
|
||||||
|
new_line += re.sub(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
|
||||||
|
'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?',
|
||||||
|
expand_and_align_chords_in_line, line)
|
||||||
|
new_line += '</span>'
|
||||||
|
expanded_text_lines.append(new_line)
|
||||||
|
else:
|
||||||
|
chords_on_prev_line = False
|
||||||
|
expanded_text_lines.append(html.escape(line))
|
||||||
|
return '{br}'.join(expanded_text_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_chord_lyric(chord, lyric):
|
||||||
|
"""
|
||||||
|
Compare the width of chord and lyrics. If chord width is greater than the lyric width the diff is returned.
|
||||||
|
|
||||||
|
:param chord:
|
||||||
|
:param lyric:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
chordlen = 0
|
||||||
|
if chord == ' ':
|
||||||
|
return 0
|
||||||
|
chord = re.sub(r'\{.*?\}', r'', chord)
|
||||||
|
lyric = re.sub(r'\{.*?\}', r'', lyric)
|
||||||
|
for chord_char in chord:
|
||||||
|
if chord_char not in SLIMCHARS:
|
||||||
|
chordlen += 2
|
||||||
|
else:
|
||||||
|
chordlen += 1
|
||||||
|
lyriclen = 0
|
||||||
|
for lyric_char in lyric:
|
||||||
|
if lyric_char not in SLIMCHARS:
|
||||||
|
lyriclen += 2
|
||||||
|
else:
|
||||||
|
lyriclen += 1
|
||||||
|
if chordlen > lyriclen:
|
||||||
|
return chordlen - lyriclen
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def find_formatting_tags(text, active_formatting_tags):
|
||||||
|
"""
|
||||||
|
Look for formatting tags in lyrics and adds/removes them to/from the given list. Returns the update list.
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
:param active_formatting_tags:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not re.search(r'\{.*?\}', text):
|
||||||
|
return active_formatting_tags
|
||||||
|
word_it = iter(text)
|
||||||
|
# Loop through lyrics to find any formatting tags
|
||||||
|
for char in word_it:
|
||||||
|
if char == '{':
|
||||||
|
tag = ''
|
||||||
|
char = next(word_it)
|
||||||
|
start_tag = True
|
||||||
|
if char == '/':
|
||||||
|
start_tag = False
|
||||||
|
char = next(word_it)
|
||||||
|
while char != '}':
|
||||||
|
tag += char
|
||||||
|
char = next(word_it)
|
||||||
|
# See if the found tag has an end tag
|
||||||
|
for formatting_tag in FormattingTags.get_html_tags():
|
||||||
|
if formatting_tag['start tag'] == '{' + tag + '}':
|
||||||
|
if formatting_tag['end tag']:
|
||||||
|
if start_tag:
|
||||||
|
# prepend the new tag to the list of active formatting tags
|
||||||
|
active_formatting_tags[:0] = [tag]
|
||||||
|
else:
|
||||||
|
# remove the tag from the list
|
||||||
|
active_formatting_tags.remove(tag)
|
||||||
|
# Break out of the loop matching the found tag against the tag list.
|
||||||
|
break
|
||||||
|
return active_formatting_tags
|
||||||
|
|
||||||
|
|
||||||
|
def expand_chords_for_printing(text, line_split):
|
||||||
|
"""
|
||||||
|
Expand ChordPro tags
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
:param line_split:
|
||||||
|
"""
|
||||||
|
if not re.search(r'\[.*?\]', text):
|
||||||
|
return text
|
||||||
|
text_lines = text.split(line_split)
|
||||||
|
expanded_text_lines = []
|
||||||
|
for line in text_lines:
|
||||||
|
# If a ChordPro is detected in the line, build html tables.
|
||||||
|
new_line = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td>'
|
||||||
|
active_formatting_tags = []
|
||||||
|
if re.search(r'\[.*?\]', line):
|
||||||
|
words = line.split(' ')
|
||||||
|
in_chord = False
|
||||||
|
for word in words:
|
||||||
|
chords = []
|
||||||
|
lyrics = []
|
||||||
|
new_line += '<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left">'
|
||||||
|
# If the word contains a chord, we need to handle it.
|
||||||
|
if re.search(r'\[.*?\]', word):
|
||||||
|
chord = ''
|
||||||
|
lyric = ''
|
||||||
|
# Loop over each character of the word
|
||||||
|
for char in word:
|
||||||
|
if char == '[':
|
||||||
|
in_chord = True
|
||||||
|
if lyric != '':
|
||||||
|
if chord == '':
|
||||||
|
chord = ' '
|
||||||
|
chords.append(chord)
|
||||||
|
lyrics.append(lyric)
|
||||||
|
chord = ''
|
||||||
|
lyric = ''
|
||||||
|
elif char == ']' and in_chord:
|
||||||
|
in_chord = False
|
||||||
|
elif in_chord:
|
||||||
|
chord += char
|
||||||
|
else:
|
||||||
|
lyric += char
|
||||||
|
if lyric != '' or chord != '':
|
||||||
|
if chord == '':
|
||||||
|
chord = ' '
|
||||||
|
if lyric == '':
|
||||||
|
lyric = ' '
|
||||||
|
chords.append(chord)
|
||||||
|
lyrics.append(lyric)
|
||||||
|
new_chord_line = '<tr class="chordrow">'
|
||||||
|
new_lyric_line = '</tr><tr>'
|
||||||
|
for i in range(len(lyrics)):
|
||||||
|
spacer = compare_chord_lyric(chords[i], lyrics[i])
|
||||||
|
# Handle formatting tags
|
||||||
|
start_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||||
|
# Update list of active formatting tags
|
||||||
|
active_formatting_tags = find_formatting_tags(lyrics[i], active_formatting_tags)
|
||||||
|
end_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||||
|
new_chord_line += '<td class="chord">%s</td>' % chords[i]
|
||||||
|
# Check if this is the last column, if so skip spacing calc and instead insert a single space
|
||||||
|
if i + 1 == len(lyrics):
|
||||||
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics} {endtags}</td>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=lyrics[i], endtags=end_formatting_tags)
|
||||||
|
else:
|
||||||
|
spacing = ''
|
||||||
|
if spacer > 0:
|
||||||
|
space = ' ' * int(math.ceil(spacer / 2))
|
||||||
|
spacing = '<span class="chordspacing">%s-%s</span>' % (space, space)
|
||||||
|
new_lyric_line += '<td class="lyrics">{starttags}{lyrics}{spacing}{endtags}</td>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=lyrics[i], spacing=spacing,
|
||||||
|
endtags=end_formatting_tags)
|
||||||
|
new_line += new_chord_line + new_lyric_line + '</tr>'
|
||||||
|
else:
|
||||||
|
start_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
start_formatting_tags = '{' + '}{'.join(active_formatting_tags) + '}'
|
||||||
|
active_formatting_tags = find_formatting_tags(word, active_formatting_tags)
|
||||||
|
end_formatting_tags = ''
|
||||||
|
if active_formatting_tags:
|
||||||
|
end_formatting_tags = '{/' + '}{/'.join(active_formatting_tags) + '}'
|
||||||
|
new_line += '<tr class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">' \
|
||||||
|
'{starttags}{lyrics} {endtags}</td></tr>'.format(
|
||||||
|
starttags=start_formatting_tags, lyrics=word, endtags=end_formatting_tags)
|
||||||
|
new_line += '</table>'
|
||||||
|
else:
|
||||||
|
new_line += line
|
||||||
|
new_line += '</td></tr></table>'
|
||||||
|
expanded_text_lines.append(new_line)
|
||||||
|
# the {br} tag used to split lines is not inserted again since the style of the line-tables makes them redundant.
|
||||||
|
return ''.join(expanded_text_lines)
|
||||||
|
|
||||||
|
|
||||||
def create_separated_list(string_list):
|
def create_separated_list(string_list):
|
||||||
"""
|
"""
|
||||||
Returns a string that represents a join of a list of strings with a localized separator.
|
Returns a string that represents a join of a list of strings with a localized separator.
|
||||||
|
@ -337,10 +616,10 @@ from .plugin import PluginStatus, StringContent, Plugin
|
||||||
from .pluginmanager import PluginManager
|
from .pluginmanager import PluginManager
|
||||||
from .settingstab import SettingsTab
|
from .settingstab import SettingsTab
|
||||||
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
|
||||||
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
|
from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||||
from .imagemanager import ImageManager
|
from .imagemanager import ImageManager
|
||||||
from .renderer import Renderer
|
from .renderer import Renderer
|
||||||
from .mediamanageritem import MediaManagerItem
|
from .mediamanageritem import MediaManagerItem
|
||||||
from .projector.db import ProjectorDB, Projector
|
from .projector.db import ProjectorDB, Projector
|
||||||
from .projector.pjlink1 import PJLink1
|
from .projector.pjlink1 import PJLink
|
||||||
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
|
from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
|
||||||
|
|
|
@ -172,6 +172,7 @@ def upgrade_db(url, upgrade):
|
||||||
else:
|
else:
|
||||||
version = int(version_meta.value)
|
version = int(version_meta.value)
|
||||||
if version > upgrade.__version__:
|
if version > upgrade.__version__:
|
||||||
|
session.remove()
|
||||||
return version, upgrade.__version__
|
return version, upgrade.__version__
|
||||||
version += 1
|
version += 1
|
||||||
try:
|
try:
|
||||||
|
@ -194,7 +195,7 @@ def upgrade_db(url, upgrade):
|
||||||
session.commit()
|
session.commit()
|
||||||
upgrade_version = upgrade.__version__
|
upgrade_version = upgrade.__version__
|
||||||
version = int(version_meta.value)
|
version = int(version_meta.value)
|
||||||
session.close()
|
session.remove()
|
||||||
return version, upgrade_version
|
return version, upgrade_version
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,25 @@ is the function which has to be called from outside. The generated and returned
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
|
@ -444,6 +463,7 @@ HTML_SRC = Template("""
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */${chords_css}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
|
@ -592,6 +612,30 @@ LYRICS_FORMAT_SRC = Template("""
|
||||||
height: ${height}px;${font_style}${font_weight}
|
height: ${height}px;${font_style}${font_weight}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
CHORDS_FORMAT = Template("""
|
||||||
|
.chordline {
|
||||||
|
line-height: ${chord_line_height};
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: ${chords_display};
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: ${first_chord_line_height};
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: ${chords_display};
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}""")
|
||||||
|
|
||||||
|
|
||||||
def build_html(item, screen, is_live, background, image=None, plugins=None):
|
def build_html(item, screen, is_live, background, image=None, plugins=None):
|
||||||
"""
|
"""
|
||||||
|
@ -636,7 +680,8 @@ def build_html(item, screen, is_live, background, image=None, plugins=None):
|
||||||
js_additions=js_additions,
|
js_additions=js_additions,
|
||||||
bg_image=bgimage_src,
|
bg_image=bgimage_src,
|
||||||
image=image_src,
|
image=image_src,
|
||||||
html_additions=html_additions)
|
html_additions=html_additions,
|
||||||
|
chords_css=build_chords_css())
|
||||||
|
|
||||||
|
|
||||||
def webkit_version():
|
def webkit_version():
|
||||||
|
@ -768,3 +813,16 @@ def build_footer_css(item, height):
|
||||||
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
|
return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
|
||||||
family=theme.font_footer_name, size=theme.font_footer_size,
|
family=theme.font_footer_name, size=theme.font_footer_size,
|
||||||
color=theme.font_footer_color, space=whitespace)
|
color=theme.font_footer_color, space=whitespace)
|
||||||
|
|
||||||
|
|
||||||
|
def build_chords_css():
|
||||||
|
if Settings().value('songs/enable chords') and Settings().value('songs/mainview chords'):
|
||||||
|
chord_line_height = '2.0em'
|
||||||
|
chords_display = 'inline'
|
||||||
|
first_chord_line_height = '2.1em'
|
||||||
|
else:
|
||||||
|
chord_line_height = '1.0em'
|
||||||
|
chords_display = 'none'
|
||||||
|
first_chord_line_height = '1.0em'
|
||||||
|
return CHORDS_FORMAT.substitute(chord_line_height=chord_line_height, chords_display=chords_display,
|
||||||
|
first_chord_line_height=first_chord_line_height)
|
||||||
|
|
|
@ -23,10 +23,9 @@
|
||||||
Provide plugin management
|
Provide plugin management
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import imp
|
|
||||||
|
|
||||||
from openlp.core.lib import Plugin, PluginStatus
|
from openlp.core.lib import Plugin, PluginStatus
|
||||||
from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin
|
from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin, extension_loader
|
||||||
|
|
||||||
|
|
||||||
class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
|
@ -70,32 +69,8 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
Scan a directory for objects inheriting from the ``Plugin`` class.
|
Scan a directory for objects inheriting from the ``Plugin`` class.
|
||||||
"""
|
"""
|
||||||
start_depth = len(os.path.abspath(self.base_path).split(os.sep))
|
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
|
||||||
present_plugin_dir = os.path.join(self.base_path, 'presentations')
|
extension_loader(glob_pattern)
|
||||||
self.log_debug('finding plugins in {path} at depth {depth:d}'.format(path=self.base_path, depth=start_depth))
|
|
||||||
for root, dirs, files in os.walk(self.base_path):
|
|
||||||
for name in files:
|
|
||||||
if name.endswith('.py') and not name.startswith('__'):
|
|
||||||
path = os.path.abspath(os.path.join(root, name))
|
|
||||||
this_depth = len(path.split(os.sep))
|
|
||||||
if this_depth - start_depth > 2:
|
|
||||||
# skip anything lower down
|
|
||||||
break
|
|
||||||
module_name = name[:-3]
|
|
||||||
# import the modules
|
|
||||||
self.log_debug('Importing {name} from {root}. Depth {depth:d}'.format(name=module_name,
|
|
||||||
root=root,
|
|
||||||
depth=this_depth))
|
|
||||||
try:
|
|
||||||
# Use the "imp" library to try to get around a problem with the PyUNO library which
|
|
||||||
# monkey-patches the __import__ function to do some magic. This causes issues with our tests.
|
|
||||||
# First, try to find the module we want to import, searching the directory in root
|
|
||||||
fp, path_name, description = imp.find_module(module_name, [root])
|
|
||||||
# Then load the module (do the actual import) using the details from find_module()
|
|
||||||
imp.load_module(module_name, fp, path_name, description)
|
|
||||||
except ImportError as e:
|
|
||||||
self.log_exception('Failed to import module {name} on path {path}: '
|
|
||||||
'{args}'.format(name=module_name, path=path, args=e.args[0]))
|
|
||||||
plugin_classes = Plugin.__subclasses__()
|
plugin_classes = Plugin.__subclasses__()
|
||||||
plugin_objects = []
|
plugin_objects = []
|
||||||
for p in plugin_classes:
|
for p in plugin_classes:
|
||||||
|
|
|
@ -48,7 +48,8 @@ __all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
|
||||||
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
|
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
|
||||||
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
||||||
'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
|
'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
|
||||||
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS']
|
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS',
|
||||||
|
'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS']
|
||||||
|
|
||||||
# Set common constants.
|
# Set common constants.
|
||||||
CR = chr(0x0D) # \r
|
CR = chr(0x0D) # \r
|
||||||
|
@ -56,20 +57,35 @@ LF = chr(0x0A) # \n
|
||||||
PJLINK_PORT = 4352
|
PJLINK_PORT = 4352
|
||||||
TIMEOUT = 30.0
|
TIMEOUT = 30.0
|
||||||
PJLINK_MAX_PACKET = 136
|
PJLINK_MAX_PACKET = 136
|
||||||
PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection
|
# NOTE: Change format to account for some commands are both class 1 and 2
|
||||||
'POWR', # Power option
|
PJLINK_VALID_CMD = {
|
||||||
'INPT', # Video sources option
|
'ACKN': ['2', ], # UDP Reply to 'SRCH'
|
||||||
'AVMT', # Shutter option
|
'AVMT': ['1', ], # Shutter option
|
||||||
'ERST', # Error status option
|
'CLSS': ['1', ], # PJLink class support query
|
||||||
'LAMP', # Lamp(s) query (Includes fans)
|
'ERST': ['1', '2'], # Error status option
|
||||||
'INST', # Input sources available query
|
'FILT': ['2', ], # Get current filter usage time
|
||||||
'NAME', # Projector name query
|
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected
|
||||||
'INF1', # Manufacturer name query
|
'INF1': ['1', ], # Manufacturer name query
|
||||||
'INF2', # Product name query
|
'INF2': ['1', ], # Product name query
|
||||||
'INFO', # Other information query
|
'INFO': ['1', ], # Other information query
|
||||||
'CLSS' # PJLink class support query
|
'INNM': ['2', ], # Get Video source input terminal name
|
||||||
]}
|
'INPT': ['1', ], # Video sources option
|
||||||
|
'INST': ['1', ], # Input sources available query
|
||||||
|
'IRES': ['2', ], # Get Video source resolution
|
||||||
|
'LAMP': ['1', ], # Lamp(s) query (Includes fans)
|
||||||
|
'LKUP': ['2', ], # UPD Linkup status notification
|
||||||
|
'MVOL': ['2', ], # Set microphone volume
|
||||||
|
'NAME': ['1', ], # Projector name query
|
||||||
|
'PJLINK': ['1', ], # Initial connection
|
||||||
|
'POWR': ['1', ], # Power option
|
||||||
|
'RFIL': ['2', ], # Get replacement air filter model number
|
||||||
|
'RLMP': ['2', ], # Get lamp replacement model number
|
||||||
|
'RRES': ['2', ], # Get projector recommended video resolution
|
||||||
|
'SNUM': ['2', ], # Get projector serial number
|
||||||
|
'SRCH': ['2', ], # UDP broadcast search for available projectors on local network
|
||||||
|
'SVER': ['2', ], # Get projector software version
|
||||||
|
'SVOL': ['2', ] # Set speaker volume
|
||||||
|
}
|
||||||
# Error and status codes
|
# Error and status codes
|
||||||
S_OK = E_OK = 0 # E_OK included since I sometimes forget
|
S_OK = E_OK = 0 # E_OK included since I sometimes forget
|
||||||
# Error codes. Start at 200 so we don't duplicate system error codes.
|
# Error codes. Start at 200 so we don't duplicate system error codes.
|
||||||
|
@ -321,53 +337,54 @@ PJLINK_DEFAULT_SOURCES = {
|
||||||
'2': translate('OpenLP.DB', 'Video'),
|
'2': translate('OpenLP.DB', 'Video'),
|
||||||
'3': translate('OpenLP.DB', 'Digital'),
|
'3': translate('OpenLP.DB', 'Digital'),
|
||||||
'4': translate('OpenLP.DB', 'Storage'),
|
'4': translate('OpenLP.DB', 'Storage'),
|
||||||
'5': translate('OpenLP.DB', 'Network')
|
'5': translate('OpenLP.DB', 'Network'),
|
||||||
|
'6': translate('OpenLP.DB', 'Internal')
|
||||||
}
|
}
|
||||||
|
|
||||||
PJLINK_DEFAULT_CODES = {
|
PJLINK_DEFAULT_ITEMS = {
|
||||||
'11': translate('OpenLP.DB', 'RGB 1'),
|
'1': translate('OpenLP.DB', '1'),
|
||||||
'12': translate('OpenLP.DB', 'RGB 2'),
|
'2': translate('OpenLP.DB', '2'),
|
||||||
'13': translate('OpenLP.DB', 'RGB 3'),
|
'3': translate('OpenLP.DB', '3'),
|
||||||
'14': translate('OpenLP.DB', 'RGB 4'),
|
'4': translate('OpenLP.DB', '4'),
|
||||||
'15': translate('OpenLP.DB', 'RGB 5'),
|
'5': translate('OpenLP.DB', '5'),
|
||||||
'16': translate('OpenLP.DB', 'RGB 6'),
|
'6': translate('OpenLP.DB', '6'),
|
||||||
'17': translate('OpenLP.DB', 'RGB 7'),
|
'7': translate('OpenLP.DB', '7'),
|
||||||
'18': translate('OpenLP.DB', 'RGB 8'),
|
'8': translate('OpenLP.DB', '8'),
|
||||||
'19': translate('OpenLP.DB', 'RGB 9'),
|
'9': translate('OpenLP.DB', '9'),
|
||||||
'21': translate('OpenLP.DB', 'Video 1'),
|
'A': translate('OpenLP.DB', 'A'),
|
||||||
'22': translate('OpenLP.DB', 'Video 2'),
|
'B': translate('OpenLP.DB', 'B'),
|
||||||
'23': translate('OpenLP.DB', 'Video 3'),
|
'C': translate('OpenLP.DB', 'C'),
|
||||||
'24': translate('OpenLP.DB', 'Video 4'),
|
'D': translate('OpenLP.DB', 'D'),
|
||||||
'25': translate('OpenLP.DB', 'Video 5'),
|
'E': translate('OpenLP.DB', 'E'),
|
||||||
'26': translate('OpenLP.DB', 'Video 6'),
|
'F': translate('OpenLP.DB', 'F'),
|
||||||
'27': translate('OpenLP.DB', 'Video 7'),
|
'G': translate('OpenLP.DB', 'G'),
|
||||||
'28': translate('OpenLP.DB', 'Video 8'),
|
'H': translate('OpenLP.DB', 'H'),
|
||||||
'29': translate('OpenLP.DB', 'Video 9'),
|
'I': translate('OpenLP.DB', 'I'),
|
||||||
'31': translate('OpenLP.DB', 'Digital 1'),
|
'J': translate('OpenLP.DB', 'J'),
|
||||||
'32': translate('OpenLP.DB', 'Digital 2'),
|
'K': translate('OpenLP.DB', 'K'),
|
||||||
'33': translate('OpenLP.DB', 'Digital 3'),
|
'L': translate('OpenLP.DB', 'L'),
|
||||||
'34': translate('OpenLP.DB', 'Digital 4'),
|
'M': translate('OpenLP.DB', 'M'),
|
||||||
'35': translate('OpenLP.DB', 'Digital 5'),
|
'N': translate('OpenLP.DB', 'N'),
|
||||||
'36': translate('OpenLP.DB', 'Digital 6'),
|
'O': translate('OpenLP.DB', 'O'),
|
||||||
'37': translate('OpenLP.DB', 'Digital 7'),
|
'P': translate('OpenLP.DB', 'P'),
|
||||||
'38': translate('OpenLP.DB', 'Digital 8'),
|
'Q': translate('OpenLP.DB', 'Q'),
|
||||||
'39': translate('OpenLP.DB', 'Digital 9'),
|
'R': translate('OpenLP.DB', 'R'),
|
||||||
'41': translate('OpenLP.DB', 'Storage 1'),
|
'S': translate('OpenLP.DB', 'S'),
|
||||||
'42': translate('OpenLP.DB', 'Storage 2'),
|
'T': translate('OpenLP.DB', 'T'),
|
||||||
'43': translate('OpenLP.DB', 'Storage 3'),
|
'U': translate('OpenLP.DB', 'U'),
|
||||||
'44': translate('OpenLP.DB', 'Storage 4'),
|
'V': translate('OpenLP.DB', 'V'),
|
||||||
'45': translate('OpenLP.DB', 'Storage 5'),
|
'W': translate('OpenLP.DB', 'W'),
|
||||||
'46': translate('OpenLP.DB', 'Storage 6'),
|
'X': translate('OpenLP.DB', 'X'),
|
||||||
'47': translate('OpenLP.DB', 'Storage 7'),
|
'Y': translate('OpenLP.DB', 'Y'),
|
||||||
'48': translate('OpenLP.DB', 'Storage 8'),
|
'Z': translate('OpenLP.DB', 'Z')
|
||||||
'49': translate('OpenLP.DB', 'Storage 9'),
|
|
||||||
'51': translate('OpenLP.DB', 'Network 1'),
|
|
||||||
'52': translate('OpenLP.DB', 'Network 2'),
|
|
||||||
'53': translate('OpenLP.DB', 'Network 3'),
|
|
||||||
'54': translate('OpenLP.DB', 'Network 4'),
|
|
||||||
'55': translate('OpenLP.DB', 'Network 5'),
|
|
||||||
'56': translate('OpenLP.DB', 'Network 6'),
|
|
||||||
'57': translate('OpenLP.DB', 'Network 7'),
|
|
||||||
'58': translate('OpenLP.DB', 'Network 8'),
|
|
||||||
'59': translate('OpenLP.DB', 'Network 9')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Due to the expanded nature of PJLink class 2 video sources,
|
||||||
|
# translate the individual types then build the video source
|
||||||
|
# dictionary from the translations.
|
||||||
|
PJLINK_DEFAULT_CODES = dict()
|
||||||
|
for source in PJLINK_DEFAULT_SOURCES:
|
||||||
|
for item in PJLINK_DEFAULT_ITEMS:
|
||||||
|
label = "{source}{item}".format(source=source, item=item)
|
||||||
|
PJLINK_DEFAULT_CODES[label] = "{source} {item}".format(source=PJLINK_DEFAULT_SOURCES[source],
|
||||||
|
item=PJLINK_DEFAULT_ITEMS[item])
|
||||||
|
|
|
@ -150,11 +150,15 @@ class Projector(CommonBase, Base):
|
||||||
name: Column(String(20))
|
name: Column(String(20))
|
||||||
location: Column(String(30))
|
location: Column(String(30))
|
||||||
notes: Column(String(200))
|
notes: Column(String(200))
|
||||||
pjlink_name: Column(String(128)) # From projector (future)
|
pjlink_name: Column(String(128)) # From projector
|
||||||
manufacturer: Column(String(128)) # From projector (future)
|
manufacturer: Column(String(128)) # From projector
|
||||||
model: Column(String(128)) # From projector (future)
|
model: Column(String(128)) # From projector
|
||||||
other: Column(String(128)) # From projector (future)
|
other: Column(String(128)) # From projector
|
||||||
sources: Column(String(128)) # From projector (future)
|
sources: Column(String(128)) # From projector
|
||||||
|
serial_no: Column(String(30)) # From projector (Class 2)
|
||||||
|
sw_version: Column(String(30)) # From projector (Class 2)
|
||||||
|
model_filter: Column(String(30)) # From projector (Class 2)
|
||||||
|
model_lamp: Column(String(30)) # From projector (Class 2)
|
||||||
|
|
||||||
ProjectorSource relates
|
ProjectorSource relates
|
||||||
"""
|
"""
|
||||||
|
@ -164,8 +168,9 @@ class Projector(CommonBase, Base):
|
||||||
"""
|
"""
|
||||||
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
|
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
|
||||||
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
|
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
|
||||||
'manufacturer="{manufacturer}", model="{model}", other="{other}", ' \
|
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
|
||||||
'sources="{sources}", source_list="{source_list}") >'.format(data=self.id,
|
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
|
||||||
|
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id,
|
||||||
ip=self.ip,
|
ip=self.ip,
|
||||||
port=self.port,
|
port=self.port,
|
||||||
pin=self.pin,
|
pin=self.pin,
|
||||||
|
@ -177,7 +182,11 @@ class Projector(CommonBase, Base):
|
||||||
model=self.model,
|
model=self.model,
|
||||||
other=self.other,
|
other=self.other,
|
||||||
sources=self.sources,
|
sources=self.sources,
|
||||||
source_list=self.source_list)
|
source_list=self.source_list,
|
||||||
|
serial=self.serial_no,
|
||||||
|
mfilter=self.model_filter,
|
||||||
|
mlamp=self.model_lamp,
|
||||||
|
sw_ver=self.sw_version)
|
||||||
ip = Column(String(100))
|
ip = Column(String(100))
|
||||||
port = Column(String(8))
|
port = Column(String(8))
|
||||||
pin = Column(String(20))
|
pin = Column(String(20))
|
||||||
|
@ -189,6 +198,10 @@ class Projector(CommonBase, Base):
|
||||||
model = Column(String(128))
|
model = Column(String(128))
|
||||||
other = Column(String(128))
|
other = Column(String(128))
|
||||||
sources = Column(String(128))
|
sources = Column(String(128))
|
||||||
|
serial_no = Column(String(30))
|
||||||
|
sw_version = Column(String(30))
|
||||||
|
model_filter = Column(String(30))
|
||||||
|
model_lamp = Column(String(30))
|
||||||
source_list = relationship('ProjectorSource',
|
source_list = relationship('ProjectorSource',
|
||||||
order_by='ProjectorSource.code',
|
order_by='ProjectorSource.code',
|
||||||
backref='projector',
|
backref='projector',
|
||||||
|
@ -359,6 +372,10 @@ class ProjectorDB(Manager):
|
||||||
old_projector.model = projector.model
|
old_projector.model = projector.model
|
||||||
old_projector.other = projector.other
|
old_projector.other = projector.other
|
||||||
old_projector.sources = projector.sources
|
old_projector.sources = projector.sources
|
||||||
|
old_projector.serial_no = projector.serial_no
|
||||||
|
old_projector.sw_version = projector.sw_version
|
||||||
|
old_projector.model_filter = projector.model_filter
|
||||||
|
old_projector.model_lamp = projector.model_lamp
|
||||||
return self.save_object(old_projector)
|
return self.save_object(old_projector)
|
||||||
|
|
||||||
def delete_projector(self, projector):
|
def delete_projector(self, projector):
|
||||||
|
|
|
@ -42,7 +42,7 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
log.debug('pjlink1 loaded')
|
log.debug('pjlink1 loaded')
|
||||||
|
|
||||||
__all__ = ['PJLink1']
|
__all__ = ['PJLink']
|
||||||
|
|
||||||
from codecs import decode
|
from codecs import decode
|
||||||
|
|
||||||
|
@ -53,20 +53,22 @@ from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG
|
||||||
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \
|
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \
|
||||||
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \
|
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \
|
||||||
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
|
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
|
||||||
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, S_NOT_CONNECTED, \
|
PJLINK_DEFAULT_CODES, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
|
||||||
S_OFF, S_OK, S_ON, S_STATUS
|
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
|
||||||
|
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
SocketError = QtNetwork.QAbstractSocket.SocketError
|
SocketError = QtNetwork.QAbstractSocket.SocketError
|
||||||
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
||||||
|
|
||||||
PJLINK_PREFIX = '%'
|
PJLINK_PREFIX = '%'
|
||||||
PJLINK_CLASS = '1'
|
PJLINK_CLASS = '1' # Default to class 1 until we query the projector
|
||||||
PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJLINK_CLASS)
|
# Add prefix here, but defer linkclass expansion until later when we have the actual
|
||||||
|
# PJLink class for the command
|
||||||
|
PJLINK_HEADER = '{prefix}{{linkclass}}'.format(prefix=PJLINK_PREFIX)
|
||||||
PJLINK_SUFFIX = CR
|
PJLINK_SUFFIX = CR
|
||||||
|
|
||||||
|
|
||||||
class PJLink1(QtNetwork.QTcpSocket):
|
class PJLink(QtNetwork.QTcpSocket):
|
||||||
"""
|
"""
|
||||||
Socket service for connecting to a PJLink-capable projector.
|
Socket service for connecting to a PJLink-capable projector.
|
||||||
"""
|
"""
|
||||||
|
@ -78,6 +80,33 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
|
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
|
||||||
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
||||||
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
|
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
|
||||||
|
# New commands available in PJLink Class 2
|
||||||
|
pjlink_future = [
|
||||||
|
'ACKN', # UDP Reply to 'SRCH'
|
||||||
|
'FILT', # Get current filter usage time
|
||||||
|
'FREZ', # Set freeze/unfreeze picture being projected
|
||||||
|
'INNM', # Get Video source input terminal name
|
||||||
|
'IRES', # Get Video source resolution
|
||||||
|
'LKUP', # UPD Linkup status notification
|
||||||
|
'MVOL', # Set microphone volume
|
||||||
|
'RFIL', # Get replacement air filter model number
|
||||||
|
'RLMP', # Get lamp replacement model number
|
||||||
|
'RRES', # Get projector recommended video resolution
|
||||||
|
'SNUM', # Get projector serial number
|
||||||
|
'SRCH', # UDP broadcast search for available projectors on local network
|
||||||
|
'SVER', # Get projector software version
|
||||||
|
'SVOL', # Set speaker volume
|
||||||
|
'TESTMEONLY' # For testing when other commands have been implemented
|
||||||
|
]
|
||||||
|
|
||||||
|
pjlink_udp_commands = [
|
||||||
|
'ACKN',
|
||||||
|
'ERST', # Class 1 or 2
|
||||||
|
'INPT', # Class 1 or 2
|
||||||
|
'LKUP',
|
||||||
|
'POWR', # Class 1 or 2
|
||||||
|
'SRCH'
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
|
def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -100,7 +129,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.port = port
|
self.port = port
|
||||||
self.pin = pin
|
self.pin = pin
|
||||||
super(PJLink1, self).__init__()
|
super(PJLink, self).__init__()
|
||||||
self.dbid = None
|
self.dbid = None
|
||||||
self.location = None
|
self.location = None
|
||||||
self.notes = None
|
self.notes = None
|
||||||
|
@ -133,7 +162,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
# Socket timer for some possible brain-dead projectors or network cable pulled
|
# Socket timer for some possible brain-dead projectors or network cable pulled
|
||||||
self.socket_timer = None
|
self.socket_timer = None
|
||||||
# Map command to function
|
# Map command to function
|
||||||
self.pjlink1_functions = {
|
self.pjlink_functions = {
|
||||||
'AVMT': self.process_avmt,
|
'AVMT': self.process_avmt,
|
||||||
'CLSS': self.process_clss,
|
'CLSS': self.process_clss,
|
||||||
'ERST': self.process_erst,
|
'ERST': self.process_erst,
|
||||||
|
@ -244,8 +273,6 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.send_command('INF2', queue=True)
|
self.send_command('INF2', queue=True)
|
||||||
if self.pjlink_name is None:
|
if self.pjlink_name is None:
|
||||||
self.send_command('NAME', queue=True)
|
self.send_command('NAME', queue=True)
|
||||||
if self.power == S_ON and self.source_available is None:
|
|
||||||
self.send_command('INST', queue=True)
|
|
||||||
|
|
||||||
def _get_status(self, status):
|
def _get_status(self, status):
|
||||||
"""
|
"""
|
||||||
|
@ -259,7 +286,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
elif status in STATUS_STRING:
|
elif status in STATUS_STRING:
|
||||||
return STATUS_STRING[status], ERROR_MSG[status]
|
return STATUS_STRING[status], ERROR_MSG[status]
|
||||||
else:
|
else:
|
||||||
return status, translate('OpenLP.PJLink1', 'Unknown status')
|
return status, translate('OpenLP.PJLink', 'Unknown status')
|
||||||
|
|
||||||
def change_status(self, status, msg=None):
|
def change_status(self, status, msg=None):
|
||||||
"""
|
"""
|
||||||
|
@ -269,7 +296,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
:param status: Status code
|
:param status: Status code
|
||||||
:param msg: Optional message
|
:param msg: Optional message
|
||||||
"""
|
"""
|
||||||
message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg
|
message = translate('OpenLP.PJLink', 'No message') if msg is None else msg
|
||||||
(code, message) = self._get_status(status)
|
(code, message) = self._get_status(status)
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
message = msg
|
message = msg
|
||||||
|
@ -322,7 +349,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
elif len(read) < 8:
|
elif len(read) < 8:
|
||||||
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
||||||
return
|
return
|
||||||
data = decode(read, 'ascii')
|
data = decode(read, 'utf-8')
|
||||||
# Possibility of extraneous data on input when reading.
|
# Possibility of extraneous data on input when reading.
|
||||||
# Clean out extraneous characters in buffer.
|
# Clean out extraneous characters in buffer.
|
||||||
dontcare = self.readLine(self.max_size)
|
dontcare = self.readLine(self.max_size)
|
||||||
|
@ -403,25 +430,24 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
return
|
return
|
||||||
self.socket_timer.stop()
|
self.socket_timer.stop()
|
||||||
self.projectorNetwork.emit(S_NETWORK_RECEIVED)
|
self.projectorNetwork.emit(S_NETWORK_RECEIVED)
|
||||||
data_in = decode(read, 'ascii')
|
# NOTE: Class2 has changed to some values being UTF-8
|
||||||
|
data_in = decode(read, 'utf-8')
|
||||||
data = data_in.strip()
|
data = data_in.strip()
|
||||||
if len(data) < 7:
|
if len(data) < 7:
|
||||||
# Not enough data for a packet
|
# Not enough data for a packet
|
||||||
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
|
||||||
return
|
|
||||||
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
|
|
||||||
if data.upper().startswith('PJLINK'):
|
|
||||||
# Reconnected from remote host disconnect ?
|
|
||||||
self.check_login(data)
|
|
||||||
self.send_busy = False
|
|
||||||
self.projectorReceivedData.emit()
|
|
||||||
return
|
return
|
||||||
elif '=' not in data:
|
elif '=' not in data:
|
||||||
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
return
|
||||||
|
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
|
||||||
|
# At this point, we should have something to work with
|
||||||
|
if data.upper().startswith('PJLINK'):
|
||||||
|
# Reconnected from remote host disconnect ?
|
||||||
|
self.check_login(data)
|
||||||
|
self.receive_data_signal()
|
||||||
return
|
return
|
||||||
data_split = data.split('=')
|
data_split = data.split('=')
|
||||||
try:
|
try:
|
||||||
|
@ -430,15 +456,15 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip))
|
log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip))
|
||||||
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
|
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
|
||||||
self.change_status(E_INVALID_DATA)
|
self.change_status(E_INVALID_DATA)
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
|
||||||
return
|
return
|
||||||
|
if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]):
|
||||||
if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
|
|
||||||
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
||||||
self.send_busy = False
|
self.receive_data_signal()
|
||||||
self.projectorReceivedData.emit()
|
|
||||||
return
|
return
|
||||||
|
if int(self.pjlink_class) < int(class_):
|
||||||
|
log.warn('({ip}) get_data(): Projector returned class reply higher '
|
||||||
|
'than projector stated class'.format(ip=self.ip))
|
||||||
return self.process_command(cmd, data)
|
return self.process_command(cmd, data)
|
||||||
|
|
||||||
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
||||||
|
@ -487,8 +513,10 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
data=opts,
|
data=opts,
|
||||||
salt='' if salt is None
|
salt='' if salt is None
|
||||||
else ' with hash'))
|
else ' with hash'))
|
||||||
|
# TODO: Check for class of command rather than default to projector PJLink class
|
||||||
|
header = PJLINK_HEADER.format(linkclass=self.pjlink_class)
|
||||||
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
|
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
|
||||||
header=PJLINK_HEADER,
|
header=header,
|
||||||
command=cmd,
|
command=cmd,
|
||||||
options=opts,
|
options=opts,
|
||||||
suffix=CR)
|
suffix=CR)
|
||||||
|
@ -510,11 +538,12 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self._send_command()
|
self._send_command()
|
||||||
|
|
||||||
@QtCore.pyqtSlot()
|
@QtCore.pyqtSlot()
|
||||||
def _send_command(self, data=None):
|
def _send_command(self, data=None, utf8=False):
|
||||||
"""
|
"""
|
||||||
Socket interface to send data. If data=None, then check queue.
|
Socket interface to send data. If data=None, then check queue.
|
||||||
|
|
||||||
:param data: Immediate data to send
|
:param data: Immediate data to send
|
||||||
|
:param utf8: Send as UTF-8 string otherwise send as ASCII string
|
||||||
"""
|
"""
|
||||||
log.debug('({ip}) _send_string()'.format(ip=self.ip))
|
log.debug('({ip}) _send_string()'.format(ip=self.ip))
|
||||||
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
|
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
|
||||||
|
@ -542,12 +571,12 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
|
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
|
||||||
self.socket_timer.start()
|
self.socket_timer.start()
|
||||||
self.projectorNetwork.emit(S_NETWORK_SENDING)
|
self.projectorNetwork.emit(S_NETWORK_SENDING)
|
||||||
sent = self.write(out.encode('ascii'))
|
sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii')))
|
||||||
self.waitForBytesWritten(2000) # 2 seconds should be enough
|
self.waitForBytesWritten(2000) # 2 seconds should be enough
|
||||||
if sent == -1:
|
if sent == -1:
|
||||||
# Network error?
|
# Network error?
|
||||||
self.change_status(E_NETWORK,
|
self.change_status(E_NETWORK,
|
||||||
translate('OpenLP.PJLink1', 'Error while sending data to projector'))
|
translate('OpenLP.PJLink', 'Error while sending data to projector'))
|
||||||
|
|
||||||
def process_command(self, cmd, data):
|
def process_command(self, cmd, data):
|
||||||
"""
|
"""
|
||||||
|
@ -556,7 +585,13 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
:param cmd: Command to process
|
:param cmd: Command to process
|
||||||
:param data: Data being processed
|
:param data: Data being processed
|
||||||
"""
|
"""
|
||||||
log.debug('({ip}) Processing command "{data}"'.format(ip=self.ip, data=cmd))
|
log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip,
|
||||||
|
cmd=cmd,
|
||||||
|
data=data))
|
||||||
|
# Check if we have a future command not available yet
|
||||||
|
if cmd in self.pjlink_future:
|
||||||
|
self._not_implemented(cmd)
|
||||||
|
return
|
||||||
if data in PJLINK_ERRORS:
|
if data in PJLINK_ERRORS:
|
||||||
# Oops - projector error
|
# Oops - projector error
|
||||||
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
||||||
|
@ -568,8 +603,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.projectorAuthentication.emit(self.name)
|
self.projectorAuthentication.emit(self.name)
|
||||||
elif data.upper() == 'ERR1':
|
elif data.upper() == 'ERR1':
|
||||||
# Undefined command
|
# Undefined command
|
||||||
self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1',
|
self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
|
||||||
'Undefined command:'),
|
|
||||||
data=cmd))
|
data=cmd))
|
||||||
elif data.upper() == 'ERR2':
|
elif data.upper() == 'ERR2':
|
||||||
# Invalid parameter
|
# Invalid parameter
|
||||||
|
@ -591,8 +625,9 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
self.projectorReceivedData.emit()
|
self.projectorReceivedData.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd in self.pjlink1_functions:
|
if cmd in self.pjlink_functions:
|
||||||
self.pjlink1_functions[cmd](data)
|
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
|
||||||
|
self.pjlink_functions[cmd](data)
|
||||||
else:
|
else:
|
||||||
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
||||||
self.send_busy = False
|
self.send_busy = False
|
||||||
|
@ -628,6 +663,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
|
|
||||||
:param data: Power status
|
:param data: Power status
|
||||||
"""
|
"""
|
||||||
|
log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
|
||||||
if data in PJLINK_POWR_STATUS:
|
if data in PJLINK_POWR_STATUS:
|
||||||
power = PJLINK_POWR_STATUS[data]
|
power = PJLINK_POWR_STATUS[data]
|
||||||
update_icons = self.power != power
|
update_icons = self.power != power
|
||||||
|
@ -962,3 +998,19 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||||
log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip))
|
log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip))
|
||||||
self.send_command(cmd='AVMT', opts='10')
|
self.send_command(cmd='AVMT', opts='10')
|
||||||
self.poll_loop()
|
self.poll_loop()
|
||||||
|
|
||||||
|
def receive_data_signal(self):
|
||||||
|
"""
|
||||||
|
Clear any busy flags and send data received signal
|
||||||
|
"""
|
||||||
|
self.send_busy = False
|
||||||
|
self.projectorReceivedData.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
def _not_implemented(self, cmd):
|
||||||
|
"""
|
||||||
|
Log when a future PJLink command has not been implemented yet.
|
||||||
|
"""
|
||||||
|
log.warn("({ip}) Future command '{cmd}' has not been implemented yet".format(ip=self.ip,
|
||||||
|
cmd=cmd))
|
||||||
|
return
|
||||||
|
|
|
@ -27,7 +27,7 @@ from PyQt5 import QtGui, QtCore, QtWebKitWidgets
|
||||||
|
|
||||||
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
|
||||||
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
|
||||||
build_lyrics_format_css, build_lyrics_outline_css
|
build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||||
from openlp.core.common import ThemeLevel
|
from openlp.core.common import ThemeLevel
|
||||||
from openlp.core.ui import MainDisplay
|
from openlp.core.ui import MainDisplay
|
||||||
|
|
||||||
|
@ -383,13 +383,14 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
*{margin: 0; padding: 0; border: 0;}
|
*{margin: 0; padding: 0; border: 0;}
|
||||||
#main {position: absolute; top: 0px; ${format_css} ${outline_css}}
|
#main {position: absolute; top: 0px; ${format_css} ${outline_css}} ${chords_css}
|
||||||
</style></head>
|
</style></head>
|
||||||
<body><div id="main"></div></body></html>""")
|
<body><div id="main"></div></body></html>""")
|
||||||
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
||||||
self.page_width,
|
self.page_width,
|
||||||
self.page_height),
|
self.page_height),
|
||||||
outline_css=build_lyrics_outline_css(theme_data)))
|
outline_css=build_lyrics_outline_css(theme_data),
|
||||||
|
chords_css=build_chords_css()))
|
||||||
self.empty_height = self.web_frame.contentsSize().height()
|
self.empty_height = self.web_frame.contentsSize().height()
|
||||||
|
|
||||||
def _paginate_slide(self, lines, line_end):
|
def _paginate_slide(self, lines, line_end):
|
||||||
|
|
|
@ -34,7 +34,7 @@ import ntpath
|
||||||
from PyQt5 import QtGui
|
from PyQt5 import QtGui
|
||||||
|
|
||||||
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
|
from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
|
||||||
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags
|
from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords, create_thumb
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -117,7 +117,6 @@ class ItemCapabilities(object):
|
||||||
|
|
||||||
``HasThumbnails``
|
``HasThumbnails``
|
||||||
The item has related thumbnails available
|
The item has related thumbnails available
|
||||||
|
|
||||||
"""
|
"""
|
||||||
CanPreview = 1
|
CanPreview = 1
|
||||||
CanEdit = 2
|
CanEdit = 2
|
||||||
|
@ -247,6 +246,8 @@ class ServiceItem(RegistryProperties):
|
||||||
self.renderer.set_item_theme(self.theme)
|
self.renderer.set_item_theme(self.theme)
|
||||||
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
||||||
if self.service_item_type == ServiceItemType.Text:
|
if self.service_item_type == ServiceItemType.Text:
|
||||||
|
expand_chord_tags = hasattr(self, 'name') and self.name == 'songs' and Settings().value(
|
||||||
|
'songs/enable chords')
|
||||||
log.debug('Formatting slides: {title}'.format(title=self.title))
|
log.debug('Formatting slides: {title}'.format(title=self.title))
|
||||||
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
|
||||||
# the dict instead of rendering them again.
|
# the dict instead of rendering them again.
|
||||||
|
@ -260,13 +261,16 @@ class ServiceItem(RegistryProperties):
|
||||||
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
||||||
for page in pages:
|
for page in pages:
|
||||||
page = page.replace('<br>', '{br}')
|
page = page.replace('<br>', '{br}')
|
||||||
html_data = expand_tags(html.escape(page.rstrip()))
|
html_data = expand_tags(page.rstrip(), expand_chord_tags)
|
||||||
self._display_frames.append({
|
new_frame = {
|
||||||
'title': clean_tags(page),
|
'title': clean_tags(page),
|
||||||
'text': clean_tags(page.rstrip()),
|
'text': clean_tags(page.rstrip(), expand_chord_tags),
|
||||||
|
'chords_text': expand_chords(clean_tags(page.rstrip(), False)),
|
||||||
'html': html_data.replace('&nbsp;', ' '),
|
'html': html_data.replace('&nbsp;', ' '),
|
||||||
'verseTag': verse_tag
|
'printing_html': expand_tags(html.escape(page.rstrip()), expand_chord_tags, True),
|
||||||
})
|
'verseTag': verse_tag,
|
||||||
|
}
|
||||||
|
self._display_frames.append(new_frame)
|
||||||
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -143,6 +143,7 @@ def format_milliseconds(milliseconds):
|
||||||
seconds=seconds,
|
seconds=seconds,
|
||||||
millis=millis)
|
millis=millis)
|
||||||
|
|
||||||
|
|
||||||
from .mediacontroller import MediaController
|
from .mediacontroller import MediaController
|
||||||
from .playertab import PlayerTab
|
from .playertab import PlayerTab
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ import os
|
||||||
import datetime
|
import datetime
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate
|
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
|
||||||
|
extension_loader, translate
|
||||||
from openlp.core.lib import ItemCapabilities
|
from openlp.core.lib import ItemCapabilities
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
from openlp.core.common import AppLocation
|
from openlp.core.common import AppLocation
|
||||||
|
@ -39,6 +40,7 @@ from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_pla
|
||||||
parse_optical_path
|
parse_optical_path
|
||||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
TICK_TIME = 200
|
TICK_TIME = 200
|
||||||
|
@ -172,19 +174,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
Check to see if we have any media Player's available.
|
Check to see if we have any media Player's available.
|
||||||
"""
|
"""
|
||||||
log.debug('_check_available_media_players')
|
log.debug('_check_available_media_players')
|
||||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')
|
controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
|
||||||
for filename in os.listdir(controller_dir):
|
glob_pattern = os.path.join(controller_dir, '*player.py')
|
||||||
if filename.endswith('player.py') and filename != 'mediaplayer.py':
|
extension_loader(glob_pattern, ['mediaplayer.py'])
|
||||||
path = os.path.join(controller_dir, filename)
|
|
||||||
if os.path.isfile(path):
|
|
||||||
module_name = 'openlp.core.ui.media.' + os.path.splitext(filename)[0]
|
|
||||||
log.debug('Importing controller %s', module_name)
|
|
||||||
try:
|
|
||||||
__import__(module_name, globals(), locals(), [])
|
|
||||||
# On some platforms importing vlc.py might cause
|
|
||||||
# also OSError exceptions. (e.g. Mac OS X)
|
|
||||||
except (ImportError, OSError):
|
|
||||||
log.warning('Failed to import %s on path %s', module_name, path)
|
|
||||||
player_classes = MediaPlayer.__subclasses__()
|
player_classes = MediaPlayer.__subclasses__()
|
||||||
for player_class in player_classes:
|
for player_class in player_classes:
|
||||||
self.register_players(player_class(self))
|
self.register_players(player_class(self))
|
||||||
|
|
|
@ -95,7 +95,7 @@ class Ui_PrintServiceDialog(object):
|
||||||
self.main_layout.addWidget(self.preview_widget)
|
self.main_layout.addWidget(self.preview_widget)
|
||||||
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
||||||
self.options_widget.hide()
|
self.options_widget.hide()
|
||||||
self.options_widget.resize(400, 300)
|
self.options_widget.resize(400, 350)
|
||||||
self.options_widget.setAutoFillBackground(True)
|
self.options_widget.setAutoFillBackground(True)
|
||||||
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
||||||
self.options_layout.setContentsMargins(8, 8, 8, 8)
|
self.options_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
@ -121,6 +121,8 @@ class Ui_PrintServiceDialog(object):
|
||||||
self.group_layout.addWidget(self.notes_check_box)
|
self.group_layout.addWidget(self.notes_check_box)
|
||||||
self.meta_data_check_box = QtWidgets.QCheckBox()
|
self.meta_data_check_box = QtWidgets.QCheckBox()
|
||||||
self.group_layout.addWidget(self.meta_data_check_box)
|
self.group_layout.addWidget(self.meta_data_check_box)
|
||||||
|
self.show_chords_check_box = QtWidgets.QCheckBox()
|
||||||
|
self.group_layout.addWidget(self.show_chords_check_box)
|
||||||
self.group_layout.addStretch(1)
|
self.group_layout.addStretch(1)
|
||||||
self.options_group_box.setLayout(self.group_layout)
|
self.options_group_box.setLayout(self.group_layout)
|
||||||
self.options_layout.addWidget(self.options_group_box)
|
self.options_layout.addWidget(self.options_group_box)
|
||||||
|
@ -144,6 +146,7 @@ class Ui_PrintServiceDialog(object):
|
||||||
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))
|
self.page_break_after_text.setText(translate('OpenLP.PrintServiceForm', 'Add page break before each text item'))
|
||||||
self.notes_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include service item notes'))
|
self.notes_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include service item notes'))
|
||||||
self.meta_data_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include play length of media items'))
|
self.meta_data_check_box.setText(translate('OpenLP.PrintServiceForm', 'Include play length of media items'))
|
||||||
|
self.show_chords_check_box.setText(translate('OpenLP.PrintServiceForm', 'Show chords'))
|
||||||
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
||||||
# Do not change the order.
|
# Do not change the order.
|
||||||
self.zoom_combo_box.addItems([
|
self.zoom_combo_box.addItems([
|
||||||
|
|
|
@ -37,7 +37,7 @@ from openlp.core.common import AppLocation
|
||||||
DEFAULT_CSS = """/*
|
DEFAULT_CSS = """/*
|
||||||
Edit this file to customize the service order print. Note, that not all CSS
|
Edit this file to customize the service order print. Note, that not all CSS
|
||||||
properties are supported. See:
|
properties are supported. See:
|
||||||
http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
https://doc.qt.io/qt-5/richtext-html-subset.html#css-properties
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.serviceTitle {
|
.serviceTitle {
|
||||||
|
@ -101,6 +101,19 @@ http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
||||||
.newPage {
|
.newPage {
|
||||||
page-break-before: always;
|
page-break-before: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.line {}
|
||||||
|
|
||||||
|
table.segment {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.chord {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.lyrics {
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,6 +185,12 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||||
self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
|
self._add_element('h1', html.escape(self.title_line_edit.text()), html_data.body, classId='serviceTitle')
|
||||||
for index, item in enumerate(self.service_manager.service_items):
|
for index, item in enumerate(self.service_manager.service_items):
|
||||||
self._add_preview_item(html_data.body, item['service_item'], index)
|
self._add_preview_item(html_data.body, item['service_item'], index)
|
||||||
|
if not self.show_chords_check_box.isChecked():
|
||||||
|
# Remove chord row and spacing span elements when not printing chords
|
||||||
|
for chord_row in html_data.find_class('chordrow'):
|
||||||
|
chord_row.drop_tree()
|
||||||
|
for spacing_span in html_data.find_class('chordspacing'):
|
||||||
|
spacing_span.drop_tree()
|
||||||
# Add the custom service notes:
|
# Add the custom service notes:
|
||||||
if self.footer_text_edit.toPlainText():
|
if self.footer_text_edit.toPlainText():
|
||||||
div = self._add_element('div', parent=html_data.body, classId='customNotes')
|
div = self._add_element('div', parent=html_data.body, classId='customNotes')
|
||||||
|
@ -196,13 +215,13 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||||
verse_def = None
|
verse_def = None
|
||||||
verse_html = None
|
verse_html = None
|
||||||
for slide in item.get_frames():
|
for slide in item.get_frames():
|
||||||
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['html']:
|
if not verse_def or verse_def != slide['verseTag'] or verse_html == slide['printing_html']:
|
||||||
text_div = self._add_element('div', parent=div, classId='itemText')
|
text_div = self._add_element('div', parent=div, classId='itemText')
|
||||||
else:
|
elif 'chordspacing' not in slide['printing_html']:
|
||||||
self._add_element('br', parent=text_div)
|
self._add_element('br', parent=text_div)
|
||||||
self._add_element('span', slide['html'], text_div)
|
self._add_element('span', slide['printing_html'], text_div)
|
||||||
verse_def = slide['verseTag']
|
verse_def = slide['verseTag']
|
||||||
verse_html = slide['html']
|
verse_html = slide['printing_html']
|
||||||
# Break the page before the div element.
|
# Break the page before the div element.
|
||||||
if index != 0 and self.page_break_after_text.isChecked():
|
if index != 0 and self.page_break_after_text.isChecked():
|
||||||
div.set('class', 'item newPage')
|
div.set('class', 'item newPage')
|
||||||
|
|
|
@ -38,7 +38,7 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE
|
||||||
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
|
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
|
||||||
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
|
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
|
||||||
from openlp.core.lib.projector.db import ProjectorDB
|
from openlp.core.lib.projector.db import ProjectorDB
|
||||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
from openlp.core.lib.projector.pjlink1 import PJLink
|
||||||
from openlp.core.ui.projector.editform import ProjectorEditForm
|
from openlp.core.ui.projector.editform import ProjectorEditForm
|
||||||
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
||||||
|
|
||||||
|
@ -690,10 +690,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
|
||||||
Helper app to build a projector instance
|
Helper app to build a projector instance
|
||||||
|
|
||||||
:param projector: Dict of projector database information
|
:param projector: Dict of projector database information
|
||||||
:returns: PJLink1() instance
|
:returns: PJLink() instance
|
||||||
"""
|
"""
|
||||||
log.debug('_add_projector()')
|
log.debug('_add_projector()')
|
||||||
return PJLink1(dbid=projector.id,
|
return PJLink(dbid=projector.id,
|
||||||
ip=projector.ip,
|
ip=projector.ip,
|
||||||
port=int(projector.port),
|
port=int(projector.port),
|
||||||
name=projector.name,
|
name=projector.name,
|
||||||
|
@ -961,7 +961,7 @@ class ProjectorItem(QtCore.QObject):
|
||||||
"""
|
"""
|
||||||
Initialization for ProjectorItem instance
|
Initialization for ProjectorItem instance
|
||||||
|
|
||||||
:param link: PJLink1 instance for QListWidgetItem
|
:param link: PJLink instance for QListWidgetItem
|
||||||
"""
|
"""
|
||||||
self.link = link
|
self.link = link
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
|
|
@ -429,4 +429,5 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||||
for bible in self.db_cache:
|
for bible in self.db_cache:
|
||||||
self.db_cache[bible].finalise()
|
self.db_cache[bible].finalise()
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BibleFormat']
|
__all__ = ['BibleFormat']
|
||||||
|
|
|
@ -58,7 +58,8 @@ from PyQt5 import QtCore
|
||||||
|
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from openlp.core.common import get_uno_command, get_uno_instance
|
from openlp.core.common import get_uno_command, get_uno_instance
|
||||||
from .presentationcontroller import PresentationController, PresentationDocument, TextType
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
|
||||||
|
TextType
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
|
@ -29,7 +29,7 @@ from subprocess import check_output, CalledProcessError
|
||||||
from openlp.core.common import AppLocation, check_binary_exists
|
from openlp.core.common import AppLocation, check_binary_exists
|
||||||
from openlp.core.common import Settings, is_win
|
from openlp.core.common import Settings, is_win
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from .presentationcontroller import PresentationController, PresentationDocument
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||||
|
|
||||||
if is_win():
|
if is_win():
|
||||||
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
||||||
|
|
|
@ -43,7 +43,7 @@ if is_win():
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
|
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
|
||||||
from openlp.core.common import trace_error_handler, Registry
|
from openlp.core.common import trace_error_handler, Registry
|
||||||
from .presentationcontroller import PresentationController, PresentationDocument
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ if is_win():
|
||||||
|
|
||||||
from openlp.core.common import AppLocation
|
from openlp.core.common import AppLocation
|
||||||
from openlp.core.lib import ScreenList
|
from openlp.core.lib import ScreenList
|
||||||
from .presentationcontroller import PresentationController, PresentationDocument
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
|
@ -197,6 +197,7 @@ class PPTViewer(QtWidgets.QWidget):
|
||||||
def openDialog(self):
|
def openDialog(self):
|
||||||
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
||||||
pptdll.SetDebug(1)
|
pptdll.SetDebug(1)
|
||||||
|
|
|
@ -26,7 +26,7 @@ from openlp.core.common import Settings, UiStrings, translate
|
||||||
from openlp.core.lib import SettingsTab, build_icon
|
from openlp.core.lib import SettingsTab, build_icon
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
from openlp.core.ui.lib import PathEdit
|
from openlp.core.ui.lib import PathEdit
|
||||||
from .pdfcontroller import PdfController
|
from openlp.plugins.presentations.lib.pdfcontroller import PdfController
|
||||||
|
|
||||||
|
|
||||||
class PresentationTab(SettingsTab):
|
class PresentationTab(SettingsTab):
|
||||||
|
|
|
@ -20,19 +20,18 @@
|
||||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
"""
|
"""
|
||||||
The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document
|
The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
|
||||||
formats.
|
presentations from a variety of document formats.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtCore
|
from PyQt5 import QtCore
|
||||||
|
|
||||||
from openlp.core.common import AppLocation, translate
|
from openlp.core.common import AppLocation, extension_loader, translate
|
||||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||||
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -122,17 +121,9 @@ class PresentationPlugin(Plugin):
|
||||||
Check to see if we have any presentation software available. If not do not install the plugin.
|
Check to see if we have any presentation software available. If not do not install the plugin.
|
||||||
"""
|
"""
|
||||||
log.debug('check_pre_conditions')
|
log.debug('check_pre_conditions')
|
||||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
|
controller_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
|
||||||
for filename in os.listdir(controller_dir):
|
glob_pattern = os.path.join(controller_dir, '*controller.py')
|
||||||
if filename.endswith('controller.py') and filename != 'presentationcontroller.py':
|
extension_loader(glob_pattern, ['presentationcontroller.py'])
|
||||||
path = os.path.join(controller_dir, filename)
|
|
||||||
if os.path.isfile(path):
|
|
||||||
module_name = 'openlp.plugins.presentations.lib.' + os.path.splitext(filename)[0]
|
|
||||||
log.debug('Importing controller {name}'.format(name=module_name))
|
|
||||||
try:
|
|
||||||
__import__(module_name, globals(), locals(), [])
|
|
||||||
except ImportError:
|
|
||||||
log.warning('Failed to import {name} on path {path}'.format(name=module_name, path=path))
|
|
||||||
controller_classes = PresentationController.__subclasses__()
|
controller_classes = PresentationController.__subclasses__()
|
||||||
for controller_class in controller_classes:
|
for controller_class in controller_classes:
|
||||||
controller = controller_class(self)
|
controller = controller_class(self)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<!--
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; 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 #
|
||||||
|
###############################################################################
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>${chords_title}</title>
|
||||||
|
<link rel="stylesheet" href="/css/stage.css" />
|
||||||
|
<link rel="stylesheet" href="/css/chords.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||||
|
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/chords.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input type="hidden" id="next-text" value="${next}" />
|
||||||
|
<div id="right">
|
||||||
|
<div id="clock"></div>
|
||||||
|
<div id="chords" class="button">Toggle Chords</div>
|
||||||
|
<div id="notes"></div>
|
||||||
|
</div>
|
||||||
|
<div id="header">
|
||||||
|
<div id="verseorder"></div>
|
||||||
|
<div id="transpose">Transpose:</div> <div class="button" id="transposedown">-</div> <div id="transposevalue">0</div> <div class="button" id="transposeup">+</div> <div id="capodisplay">(Capo)</div>
|
||||||
|
</div>
|
||||||
|
<div id="currentslide"></div>
|
||||||
|
<div id="nextslide"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,96 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; 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 *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#header {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transpose,
|
||||||
|
#transposevalue,
|
||||||
|
#capodisplay {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 30pt;
|
||||||
|
color: gray;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid gray;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: 0 .2em;
|
||||||
|
min-width: 1.2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-size: 25pt;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0px 1px 0px white;
|
||||||
|
color: black;
|
||||||
|
background: linear-gradient(to bottom, white 5%, gray 100%);
|
||||||
|
background-color: gray;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: linear-gradient(to bottom, white 10%, gray 150%);
|
||||||
|
color: darkslategray ;
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
.button:active {
|
||||||
|
position:relative;
|
||||||
|
top:1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extending existing definition in stage.css */
|
||||||
|
#verseorder {
|
||||||
|
line-height: 1.5;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline {
|
||||||
|
line-height: 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline1 {
|
||||||
|
line-height: 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 30pt;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nextslide .chordline span.chord span strong {
|
||||||
|
color: gray;
|
||||||
|
}
|
|
@ -21,6 +21,10 @@ body {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
-webkit-user-select: none; /* Chrome/Safari */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* IE 10+ */
|
||||||
|
user-select: none; /* Future */
|
||||||
}
|
}
|
||||||
|
|
||||||
#currentslide {
|
#currentslide {
|
||||||
|
|
|
@ -0,0 +1,331 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* OpenLP - Open Source Lyrics Projection *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||||
|
* --------------------------------------------------------------------------- *
|
||||||
|
* This program is free software; you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU General Public License as published by the Free *
|
||||||
|
* Software Foundation; 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 *
|
||||||
|
******************************************************************************/
|
||||||
|
var lastChord;
|
||||||
|
|
||||||
|
var notesSharpNotation = {}
|
||||||
|
var notesFlatNotation = {}
|
||||||
|
|
||||||
|
// See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||||
|
notesSharpNotation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'];
|
||||||
|
notesFlatNotation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'];
|
||||||
|
notesSharpNotation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
||||||
|
notesFlatNotation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'];
|
||||||
|
notesSharpNotation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'];
|
||||||
|
notesFlatNotation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'];
|
||||||
|
|
||||||
|
function getTransposeValue(songId) {
|
||||||
|
if (localStorage.getItem(songId + '_transposeValue')) {return localStorage.getItem(songId + '_transposeValue');}
|
||||||
|
else {return 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeTransposeValue(songId,transposeValueToSet) {
|
||||||
|
localStorage.setItem(songId + '_transposeValue', transposeValueToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This function has a python equivalent in openlp/plugins/songs/lib/__init__.py - make sure to update both!
|
||||||
|
function transposeChord(chord, transposeValue, notation) {
|
||||||
|
var chordSplit = chord.replace('♭', 'b').split(/[\/]/);
|
||||||
|
var transposedChord = '', note, notenumber, rest, currentChord;
|
||||||
|
var notesSharp = notesSharpNotation[notation];
|
||||||
|
var notesFlat = notesFlatNotation[notation];
|
||||||
|
var notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#'];
|
||||||
|
for (i = 0; i <= chordSplit.length - 1; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
transposedChord += '/';
|
||||||
|
}
|
||||||
|
currentchord = chordSplit[i];
|
||||||
|
if (currentchord.length > 0 && currentchord.charAt(0) === '(') {
|
||||||
|
transposedChord += '(';
|
||||||
|
if (currentchord.length > 1) {
|
||||||
|
currentchord = currentchord.substr(1);
|
||||||
|
} else {
|
||||||
|
currentchord = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentchord.length > 0) {
|
||||||
|
if (currentchord.length > 1) {
|
||||||
|
if ('#b'.indexOf(currentchord.charAt(1)) === -1) {
|
||||||
|
note = currentchord.substr(0, 1);
|
||||||
|
rest = currentchord.substr(1);
|
||||||
|
} else {
|
||||||
|
note = currentchord.substr(0, 2);
|
||||||
|
rest = currentchord.substr(2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
note = currentchord;
|
||||||
|
rest = "";
|
||||||
|
}
|
||||||
|
notenumber = (notesSharp.indexOf(note) === -1 ? notesFlat.indexOf(note) : notesSharp.indexOf(note));
|
||||||
|
notenumber += parseInt(transposeValue);
|
||||||
|
while (notenumber > 11) {
|
||||||
|
notenumber -= 12;
|
||||||
|
}
|
||||||
|
while (notenumber < 0) {
|
||||||
|
notenumber += 12;
|
||||||
|
}
|
||||||
|
if (i === 0) {
|
||||||
|
currentChord = notesPreferred[notenumber] === '#' ? notesSharp[notenumber] : notesFlat[notenumber];
|
||||||
|
lastChord = currentChord;
|
||||||
|
} else {
|
||||||
|
currentChord = notesSharp.indexOf(lastChord) === -1 ? notesFlat[notenumber] : notesSharp[notenumber];
|
||||||
|
}
|
||||||
|
if (!(notesFlat.indexOf(note) === -1 && notesSharp.indexOf(note) === -1)) {
|
||||||
|
transposedChord += currentChord + rest;
|
||||||
|
} else {
|
||||||
|
transposedChord += note + rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transposedChord;
|
||||||
|
}
|
||||||
|
|
||||||
|
var OpenLPChordOverflowFillCount = 0;
|
||||||
|
window.OpenLP = {
|
||||||
|
showchords:true,
|
||||||
|
loadService: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/service/list",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.nextSong = "";
|
||||||
|
$("#notes").html("");
|
||||||
|
for (idx in data.results.items) {
|
||||||
|
idx = parseInt(idx, 10);
|
||||||
|
if (data.results.items[idx]["selected"]) {
|
||||||
|
$("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
|
||||||
|
if (data.results.items.length > idx + 1) {
|
||||||
|
OpenLP.nextSong = data.results.items[idx + 1]["title"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLP.updateSlide();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loadSlides: function (event) {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/controller/live/text",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.currentSlides = data.results.slides;
|
||||||
|
$('#transposevalue').text(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]));
|
||||||
|
OpenLP.currentSlide = 0;
|
||||||
|
OpenLP.currentTags = Array();
|
||||||
|
var div = $("#verseorder");
|
||||||
|
div.html("");
|
||||||
|
var tag = "";
|
||||||
|
var tags = 0;
|
||||||
|
var lastChange = 0;
|
||||||
|
$.each(data.results.slides, function(idx, slide) {
|
||||||
|
var prevtag = tag;
|
||||||
|
tag = slide["tag"];
|
||||||
|
if (tag != prevtag) {
|
||||||
|
// If the tag has changed, add new one to the list
|
||||||
|
lastChange = idx;
|
||||||
|
tags = tags + 1;
|
||||||
|
div.append(" <span>");
|
||||||
|
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ((slide["chords_text"] == data.results.slides[lastChange]["chords_text"]) &&
|
||||||
|
(data.results.slides.length > idx + (idx - lastChange))) {
|
||||||
|
// If the tag hasn't changed, check to see if the same verse
|
||||||
|
// has been repeated consecutively. Note the verse may have been
|
||||||
|
// split over several slides, so search through. If so, repeat the tag.
|
||||||
|
var match = true;
|
||||||
|
for (var idx2 = 0; idx2 < idx - lastChange; idx2++) {
|
||||||
|
if(data.results.slides[lastChange + idx2]["chords_text"] != data.results.slides[idx + idx2]["chords_text"]) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
lastChange = idx;
|
||||||
|
tags = tags + 1;
|
||||||
|
div.append(" <span>");
|
||||||
|
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenLP.currentTags[idx] = tags;
|
||||||
|
if (slide["selected"])
|
||||||
|
OpenLP.currentSlide = idx;
|
||||||
|
})
|
||||||
|
OpenLP.loadService();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSlide: function() {
|
||||||
|
// Show the current slide on top. Any trailing slides for the same verse
|
||||||
|
// are shown too underneath in grey.
|
||||||
|
// Then leave a blank line between following verses
|
||||||
|
var transposeValue = getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]);
|
||||||
|
var chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g;
|
||||||
|
var chordclassshow='class="chord"';
|
||||||
|
var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)(<span class="ws">.+?<\/span>)?([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
|
||||||
|
// NOTE: There is equivalent python code in openlp/core/lib/__init__.py, in the expand_and_align_chords_in_line function. Make sure to update both!
|
||||||
|
var replaceChords=function(mstr,$chord,$tail,$skips,$remainder,$end) {
|
||||||
|
var w='';
|
||||||
|
var $chordlen = 0;
|
||||||
|
var $taillen = 0;
|
||||||
|
var slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
|
||||||
|
// Transpose chord as dictated by the transpose value in local storage
|
||||||
|
if (transposeValue != 0) {
|
||||||
|
$chord = transposeChord($chord, transposeValue, OpenLP.chordNotation);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < $chord.length; i++) if (slimchars.indexOf($chord.charAt(i)) === -1) {$chordlen += 2;} else {$chordlen += 1;}
|
||||||
|
for (var i = 0; i < $tail.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||||
|
for (var i = 0; i < $remainder.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||||
|
if ($chordlen >= $taillen && !$end) {
|
||||||
|
if ($tail.length){
|
||||||
|
if (!$remainder.length) {
|
||||||
|
for (c = 0; c < Math.ceil(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||||
|
} else {
|
||||||
|
for (c = 0; c < $chordlen - $taillen + 2; c++) {w += ' ';}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$remainder.length) {
|
||||||
|
for (c = 0; c < Math.floor(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||||
|
} else {
|
||||||
|
for (c = 0; c < $chordlen - $taillen + 1; c++) {w += ' ';}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += ' ';}}
|
||||||
|
}
|
||||||
|
if (w!='') {
|
||||||
|
if (w[0] == '_') {
|
||||||
|
ws_length = w.length;
|
||||||
|
if (ws_length==1) {
|
||||||
|
w = '–';
|
||||||
|
} else {
|
||||||
|
wsl_mod = Math.floor(ws_length / 2);
|
||||||
|
ws_right = ws_left = new Array(wsl_mod + 1).join(' ');
|
||||||
|
w = ws_left + '–' + ws_right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w='<span class="ws">' + w + '</span>';
|
||||||
|
}
|
||||||
|
return $.grep(['<span class="chord"><span><strong>', $chord, '</strong></span></span>', $tail, w, $remainder, $end], Boolean).join('');
|
||||||
|
};
|
||||||
|
$("#verseorder span").removeClass("currenttag");
|
||||||
|
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
|
||||||
|
var slide = OpenLP.currentSlides[OpenLP.currentSlide];
|
||||||
|
var text = "";
|
||||||
|
// use title if available
|
||||||
|
if (slide["title"]) {
|
||||||
|
text = slide["title"];
|
||||||
|
} else {
|
||||||
|
text = slide["chords_text"];
|
||||||
|
if(OpenLP.showchords) {
|
||||||
|
text = text.replace(chordclass,chordclassshow);
|
||||||
|
text = text.replace(regchord, replaceChords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// use thumbnail if available
|
||||||
|
if (slide["img"]) {
|
||||||
|
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
|
||||||
|
}
|
||||||
|
// use notes if available
|
||||||
|
if (slide["slide_notes"]) {
|
||||||
|
text += '<br />' + slide["slide_notes"];
|
||||||
|
}
|
||||||
|
text = text.replace(/\n/g, "<br />");
|
||||||
|
$("#currentslide").html(text);
|
||||||
|
text = "";
|
||||||
|
if (OpenLP.currentSlide < OpenLP.currentSlides.length - 1) {
|
||||||
|
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
|
||||||
|
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||||
|
text = text + "<p class=\"nextslide\">";
|
||||||
|
if (OpenLP.currentSlides[idx]["title"]) {
|
||||||
|
text = text + OpenLP.currentSlides[idx]["title"];
|
||||||
|
} else {
|
||||||
|
text = text + OpenLP.currentSlides[idx]["chords_text"];
|
||||||
|
if(OpenLP.showchords) {
|
||||||
|
text = text.replace(chordclass,chordclassshow);
|
||||||
|
text = text.replace(regchord, replaceChords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||||
|
text = text + "</p>";
|
||||||
|
else
|
||||||
|
text = text + "<br />";
|
||||||
|
}
|
||||||
|
text = text.replace(/\n/g, "<br />");
|
||||||
|
$("#nextslide").html(text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text = "<p class=\"nextslide\">" + $("#next-text").val() + ": " + OpenLP.nextSong + "</p>";
|
||||||
|
$("#nextslide").html(text);
|
||||||
|
}
|
||||||
|
if(!OpenLP.showchords) {
|
||||||
|
$(".chordline").toggleClass('chordline1');
|
||||||
|
$(".chord").toggle();
|
||||||
|
$(".ws").toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateClock: function(data) {
|
||||||
|
var div = $("#clock");
|
||||||
|
var t = new Date();
|
||||||
|
var h = t.getHours();
|
||||||
|
if (data.results.twelve && h > 12)
|
||||||
|
h = h - 12;
|
||||||
|
if (h < 10) h = '0' + h + '';
|
||||||
|
var m = t.getMinutes();
|
||||||
|
if (m < 10)
|
||||||
|
m = '0' + m + '';
|
||||||
|
div.html(h + ":" + m);
|
||||||
|
},
|
||||||
|
pollServer: function () {
|
||||||
|
$.getJSON(
|
||||||
|
"/api/poll",
|
||||||
|
function (data, status) {
|
||||||
|
OpenLP.updateClock(data);
|
||||||
|
OpenLP.chordNotation = data.results.chordNotation;
|
||||||
|
if (OpenLP.currentItem != data.results.item || OpenLP.currentService != data.results.service) {
|
||||||
|
OpenLP.currentItem = data.results.item;
|
||||||
|
OpenLP.currentService = data.results.service;
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
}
|
||||||
|
else if (OpenLP.currentSlide != data.results.slide) {
|
||||||
|
OpenLP.currentSlide = parseInt(data.results.slide, 10);
|
||||||
|
OpenLP.updateSlide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.ajaxSetup({ cache: false });
|
||||||
|
setInterval("OpenLP.pollServer();", 500);
|
||||||
|
OpenLP.pollServer();
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#transposeup').click(function(e) {
|
||||||
|
$('#transposevalue').text(parseInt($('#transposevalue').text()) + 1);
|
||||||
|
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
$('#transposedown').click(function(e) {
|
||||||
|
$('#transposevalue').text(parseInt($('#transposevalue').text()) - 1);
|
||||||
|
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
$('#chords').click(function () {
|
||||||
|
OpenLP.showchords = OpenLP.showchords ? false : true;
|
||||||
|
OpenLP.loadSlides();
|
||||||
|
});
|
||||||
|
});
|
|
@ -152,6 +152,7 @@ class HttpRouter(RegistryProperties):
|
||||||
('^/$', {'function': self.serve_file, 'secure': False}),
|
('^/$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||||
|
('^/(chords)$', {'function': self.serve_file, 'secure': False}),
|
||||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||||
|
@ -318,10 +319,12 @@ class HttpRouter(RegistryProperties):
|
||||||
"""
|
"""
|
||||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||||
|
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||||
self.template_vars = {
|
self.template_vars = {
|
||||||
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
||||||
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
||||||
|
'chords_title': "{main} {chords}".format(main=UiStrings().OLPV2x, chords=chords),
|
||||||
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
||||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||||
|
@ -482,7 +485,8 @@ class HttpRouter(RegistryProperties):
|
||||||
'display': self.live_controller.desktop_screen.isChecked(),
|
'display': self.live_controller.desktop_screen.isChecked(),
|
||||||
'version': 2,
|
'version': 2,
|
||||||
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
||||||
'isAuthorised': self.authorised
|
'isAuthorised': self.authorised,
|
||||||
|
'chordNotation': Settings().value('songs/chord notation'),
|
||||||
}
|
}
|
||||||
self.do_json_header()
|
self.do_json_header()
|
||||||
return json.dumps({'results': result}).encode()
|
return json.dumps({'results': result}).encode()
|
||||||
|
@ -554,6 +558,7 @@ class HttpRouter(RegistryProperties):
|
||||||
item['tag'] = str(frame['verseTag'])
|
item['tag'] = str(frame['verseTag'])
|
||||||
else:
|
else:
|
||||||
item['tag'] = str(index + 1)
|
item['tag'] = str(index + 1)
|
||||||
|
item['chords_text'] = str(frame['chords_text'])
|
||||||
item['text'] = str(frame['text'])
|
item['text'] = str(frame['text'])
|
||||||
item['html'] = str(frame['html'])
|
item['html'] = str(frame['html'])
|
||||||
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
||||||
|
|
|
@ -81,6 +81,12 @@ class RemoteTab(SettingsTab):
|
||||||
self.stage_url.setObjectName('stage_url')
|
self.stage_url.setObjectName('stage_url')
|
||||||
self.stage_url.setOpenExternalLinks(True)
|
self.stage_url.setOpenExternalLinks(True)
|
||||||
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
||||||
|
self.chords_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
self.chords_url_label.setObjectName('chords_url_label')
|
||||||
|
self.chords_url = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
self.chords_url.setObjectName('chords_url')
|
||||||
|
self.chords_url.setOpenExternalLinks(True)
|
||||||
|
self.http_setting_layout.addRow(self.chords_url_label, self.chords_url)
|
||||||
self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
self.live_url_label.setObjectName('live_url_label')
|
self.live_url_label.setObjectName('live_url_label')
|
||||||
self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
|
self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
|
||||||
|
@ -148,6 +154,7 @@ class RemoteTab(SettingsTab):
|
||||||
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
||||||
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
||||||
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
||||||
|
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
|
||||||
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
||||||
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||||
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
||||||
|
|
|
@ -25,6 +25,7 @@ from PyQt5 import QtWidgets
|
||||||
from openlp.core.ui.lib import SpellTextEdit
|
from openlp.core.ui.lib import SpellTextEdit
|
||||||
from openlp.core.lib import build_icon, translate
|
from openlp.core.lib import build_icon, translate
|
||||||
from openlp.core.lib.ui import UiStrings, create_button_box
|
from openlp.core.lib.ui import UiStrings, create_button_box
|
||||||
|
from openlp.core.common import Settings
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +64,21 @@ class Ui_EditVerseDialog(object):
|
||||||
self.verse_type_layout.addWidget(self.insert_button)
|
self.verse_type_layout.addWidget(self.insert_button)
|
||||||
self.verse_type_layout.addStretch()
|
self.verse_type_layout.addStretch()
|
||||||
self.dialog_layout.addLayout(self.verse_type_layout)
|
self.dialog_layout.addLayout(self.verse_type_layout)
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.transpose_layout.setObjectName('transpose_layout')
|
||||||
|
self.transpose_label = QtWidgets.QLabel(edit_verse_dialog)
|
||||||
|
self.transpose_label.setObjectName('transpose_label')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_label)
|
||||||
|
self.transpose_up_button = QtWidgets.QPushButton(edit_verse_dialog)
|
||||||
|
self.transpose_up_button.setIcon(build_icon(':/services/service_up.png'))
|
||||||
|
self.transpose_up_button.setObjectName('transpose_up')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_up_button)
|
||||||
|
self.transpose_down_button = QtWidgets.QPushButton(edit_verse_dialog)
|
||||||
|
self.transpose_down_button.setIcon(build_icon(':/services/service_down.png'))
|
||||||
|
self.transpose_down_button.setObjectName('transpose_down')
|
||||||
|
self.transpose_layout.addWidget(self.transpose_down_button)
|
||||||
|
self.dialog_layout.addLayout(self.transpose_layout)
|
||||||
self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
|
self.button_box = create_button_box(edit_verse_dialog, 'button_box', ['cancel', 'ok'])
|
||||||
self.dialog_layout.addWidget(self.button_box)
|
self.dialog_layout.addWidget(self.button_box)
|
||||||
self.retranslateUi(edit_verse_dialog)
|
self.retranslateUi(edit_verse_dialog)
|
||||||
|
@ -82,3 +98,7 @@ class Ui_EditVerseDialog(object):
|
||||||
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
||||||
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
||||||
'Split a slide into two by inserting a verse splitter.'))
|
'Split a slide into two by inserting a verse splitter.'))
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_label.setText(translate('SongsPlugin.EditVerseForm', 'Transpose:'))
|
||||||
|
self.transpose_up_button.setText(translate('SongsPlugin.EditVerseForm', 'Up'))
|
||||||
|
self.transpose_down_button.setText(translate('SongsPlugin.EditVerseForm', 'Down'))
|
||||||
|
|
|
@ -25,7 +25,9 @@ import logging
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType, transpose_lyrics
|
||||||
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
|
from openlp.core.common import translate, Settings
|
||||||
from .editversedialog import Ui_EditVerseDialog
|
from .editversedialog import Ui_EditVerseDialog
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -48,6 +50,9 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||||
self.split_button.clicked.connect(self.on_split_button_clicked)
|
self.split_button.clicked.connect(self.on_split_button_clicked)
|
||||||
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
|
self.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_changed)
|
||||||
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
|
self.verse_type_combo_box.currentIndexChanged.connect(self.on_verse_type_combo_box_changed)
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
self.transpose_down_button.clicked.connect(self.on_transepose_down_button_clicked)
|
||||||
|
self.transpose_up_button.clicked.connect(self.on_transepose_up_button_clicked)
|
||||||
|
|
||||||
def insert_verse(self, verse_tag, verse_num=1):
|
def insert_verse(self, verse_tag, verse_num=1):
|
||||||
"""
|
"""
|
||||||
|
@ -95,6 +100,41 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||||
"""
|
"""
|
||||||
self.update_suggested_verse_number()
|
self.update_suggested_verse_number()
|
||||||
|
|
||||||
|
def on_transepose_up_button_clicked(self):
|
||||||
|
"""
|
||||||
|
The transpose up button clicked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
except ValueError as ve:
|
||||||
|
# Transposing failed
|
||||||
|
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'),
|
||||||
|
message=translate('SongsPlugin.EditVerseForm',
|
||||||
|
'Transposing failed because of invalid chord:\n{err_msg}'
|
||||||
|
.format(err_msg=ve)))
|
||||||
|
return
|
||||||
|
self.verse_text_edit.setFocus()
|
||||||
|
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||||
|
|
||||||
|
def on_transepose_down_button_clicked(self):
|
||||||
|
"""
|
||||||
|
The transpose down button clicked
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), -1)
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
except ValueError as ve:
|
||||||
|
# Transposing failed
|
||||||
|
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Transposing failed'),
|
||||||
|
message=translate('SongsPlugin.EditVerseForm',
|
||||||
|
'Transposing failed because of invalid chord:\n{err_msg}'
|
||||||
|
.format(err_msg=ve)))
|
||||||
|
return
|
||||||
|
self.verse_text_edit.setPlainText(transposed_lyrics)
|
||||||
|
self.verse_text_edit.setFocus()
|
||||||
|
self.verse_text_edit.moveCursor(QtGui.QTextCursor.End)
|
||||||
|
|
||||||
def update_suggested_verse_number(self):
|
def update_suggested_verse_number(self):
|
||||||
"""
|
"""
|
||||||
Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.
|
Adjusts the verse number SpinBox in regard to the selected verse type and the cursor's position.
|
||||||
|
@ -169,3 +209,20 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||||
if not text.startswith('---['):
|
if not text.startswith('---['):
|
||||||
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
|
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""
|
||||||
|
Test if any invalid chords has been entered before closing the verse editor
|
||||||
|
"""
|
||||||
|
if Settings().value('songs/enable chords'):
|
||||||
|
try:
|
||||||
|
transposed_lyrics = transpose_lyrics(self.verse_text_edit.toPlainText(), 1)
|
||||||
|
super(EditVerseForm, self).accept()
|
||||||
|
except ValueError as ve:
|
||||||
|
# Transposing failed
|
||||||
|
critical_error_message_box(title=translate('SongsPlugin.EditVerseForm', 'Invalid Chord'),
|
||||||
|
message=translate('SongsPlugin.EditVerseForm',
|
||||||
|
'An invalid chord was detected:\n{err_msg}'
|
||||||
|
.format(err_msg=ve)))
|
||||||
|
else:
|
||||||
|
super(EditVerseForm, self).accept()
|
||||||
|
|
|
@ -29,7 +29,7 @@ import re
|
||||||
|
|
||||||
from PyQt5 import QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
|
|
||||||
from openlp.core.common import AppLocation, CONTROL_CHARS
|
from openlp.core.common import AppLocation, CONTROL_CHARS, Settings
|
||||||
from openlp.core.lib import translate, clean_tags
|
from openlp.core.lib import translate, clean_tags
|
||||||
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
|
@ -380,7 +380,7 @@ def clean_song(manager, song):
|
||||||
if isinstance(song.lyrics, bytes):
|
if isinstance(song.lyrics, bytes):
|
||||||
song.lyrics = str(song.lyrics, encoding='utf8')
|
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||||
verses = SongXML().get_verses(song.lyrics)
|
verses = SongXML().get_verses(song.lyrics)
|
||||||
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1])) for verse in verses])
|
song.search_lyrics = ' '.join([clean_string(clean_tags(verse[1], True)) for verse in verses])
|
||||||
# The song does not have any author, add one.
|
# The song does not have any author, add one.
|
||||||
if not song.authors_songs:
|
if not song.authors_songs:
|
||||||
name = SongStrings.AuthorUnknown
|
name = SongStrings.AuthorUnknown
|
||||||
|
@ -541,3 +541,123 @@ def delete_song(song_id, song_plugin):
|
||||||
except OSError:
|
except OSError:
|
||||||
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
||||||
song_plugin.manager.delete_object(Song, song_id)
|
song_plugin.manager.delete_object(Song, song_id)
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_lyrics(lyrics, transepose_value):
|
||||||
|
"""
|
||||||
|
Transepose lyrics
|
||||||
|
|
||||||
|
:param lyrcs: The lyrics to be transposed
|
||||||
|
:param transepose_value: The value to transpose the lyrics with
|
||||||
|
:return: The transposed lyrics
|
||||||
|
"""
|
||||||
|
# Split text by verse delimiter - both normal and optional
|
||||||
|
verse_list = re.split('(---\[.+?:.+?\]---|\[---\])', lyrics)
|
||||||
|
transposed_lyrics = ''
|
||||||
|
notation = Settings().value('songs/chord notation')
|
||||||
|
for verse in verse_list:
|
||||||
|
if verse.startswith('---[') or verse == '[---]':
|
||||||
|
transposed_lyrics += verse
|
||||||
|
else:
|
||||||
|
transposed_lyrics += transpose_verse(verse, transepose_value, notation)
|
||||||
|
return transposed_lyrics
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_verse(verse_text, transepose_value, notation):
|
||||||
|
"""
|
||||||
|
Transepose lyrics
|
||||||
|
|
||||||
|
:param lyrcs: The lyrics to be transposed
|
||||||
|
:param transepose_value: The value to transpose the lyrics with
|
||||||
|
:return: The transposed lyrics
|
||||||
|
"""
|
||||||
|
if '[' not in verse_text:
|
||||||
|
return verse_text
|
||||||
|
# Split the lyrics based on chord tags
|
||||||
|
lyric_list = re.split('(\[|\]|/)', verse_text)
|
||||||
|
transposed_lyrics = ''
|
||||||
|
in_tag = False
|
||||||
|
for word in lyric_list:
|
||||||
|
if not in_tag:
|
||||||
|
transposed_lyrics += word
|
||||||
|
if word == '[':
|
||||||
|
in_tag = True
|
||||||
|
else:
|
||||||
|
if word == ']':
|
||||||
|
in_tag = False
|
||||||
|
transposed_lyrics += word
|
||||||
|
elif word == '/':
|
||||||
|
transposed_lyrics += word
|
||||||
|
else:
|
||||||
|
# This MUST be a chord
|
||||||
|
transposed_lyrics += transpose_chord(word, transepose_value, notation)
|
||||||
|
# If still inside a chord tag something is wrong!
|
||||||
|
if in_tag:
|
||||||
|
return verse_text
|
||||||
|
else:
|
||||||
|
return transposed_lyrics
|
||||||
|
|
||||||
|
|
||||||
|
def transpose_chord(chord, transpose_value, notation):
|
||||||
|
"""
|
||||||
|
Transpose chord according to the notation used.
|
||||||
|
NOTE: This function has a javascript equivalent in chords.js - make sure to update both!
|
||||||
|
|
||||||
|
:param chord: The chord to transpose.
|
||||||
|
:param transpose_value: The value the chord should be transposed.
|
||||||
|
:param notation: The notation to use when transposing.
|
||||||
|
:return: The transposed chord.
|
||||||
|
"""
|
||||||
|
# See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||||
|
notes_sharp_notation = {}
|
||||||
|
notes_flat_notation = {}
|
||||||
|
notes_sharp_notation['german'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']
|
||||||
|
notes_flat_notation['german'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']
|
||||||
|
notes_sharp_notation['english'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||||
|
notes_flat_notation['english'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
||||||
|
notes_sharp_notation['neo-latin'] = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si']
|
||||||
|
notes_flat_notation['neo-latin'] = ['Do', 'Reb', 'Re', 'Mib', 'Fab', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']
|
||||||
|
chord_split = chord.replace('♭', 'b').split('/')
|
||||||
|
transposed_chord = ''
|
||||||
|
last_chord = ''
|
||||||
|
notes_sharp = notes_sharp_notation[notation]
|
||||||
|
notes_flat = notes_flat_notation[notation]
|
||||||
|
notes_preferred = ['b', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#']
|
||||||
|
for i in range(0, len(chord_split)):
|
||||||
|
if i > 0:
|
||||||
|
transposed_chord += '/'
|
||||||
|
currentchord = chord_split[i]
|
||||||
|
if currentchord and currentchord[0] == '(':
|
||||||
|
transposed_chord += '('
|
||||||
|
if len(currentchord) > 1:
|
||||||
|
currentchord = currentchord[1:]
|
||||||
|
else:
|
||||||
|
currentchord = ''
|
||||||
|
if len(currentchord) > 0:
|
||||||
|
if len(currentchord) > 1:
|
||||||
|
if '#b'.find(currentchord[1]) == -1:
|
||||||
|
note = currentchord[0:1]
|
||||||
|
rest = currentchord[1:]
|
||||||
|
else:
|
||||||
|
note = currentchord[0:2]
|
||||||
|
rest = currentchord[2:]
|
||||||
|
else:
|
||||||
|
note = currentchord
|
||||||
|
rest = ''
|
||||||
|
notenumber = notes_flat.index(note) if note not in notes_sharp else notes_sharp.index(note)
|
||||||
|
notenumber += transpose_value
|
||||||
|
while notenumber > 11:
|
||||||
|
notenumber -= 12
|
||||||
|
while notenumber < 0:
|
||||||
|
notenumber += 12
|
||||||
|
if i == 0:
|
||||||
|
current_chord = notes_sharp[notenumber] if notes_preferred[notenumber] == '#' else notes_flat[
|
||||||
|
notenumber]
|
||||||
|
last_chord = current_chord
|
||||||
|
else:
|
||||||
|
current_chord = notes_flat[notenumber] if last_chord not in notes_sharp else notes_sharp[notenumber]
|
||||||
|
if not (note not in notes_flat and note not in notes_sharp):
|
||||||
|
transposed_chord += current_chord + rest
|
||||||
|
else:
|
||||||
|
transposed_chord += note + rest
|
||||||
|
return transposed_chord
|
||||||
|
|
|
@ -48,6 +48,7 @@ from .importers.powerpraise import PowerPraiseImport
|
||||||
from .importers.presentationmanager import PresentationManagerImport
|
from .importers.presentationmanager import PresentationManagerImport
|
||||||
from .importers.lyrix import LyrixImport
|
from .importers.lyrix import LyrixImport
|
||||||
from .importers.videopsalm import VideoPsalmImport
|
from .importers.videopsalm import VideoPsalmImport
|
||||||
|
from .importers.chordpro import ChordProImport
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -155,29 +156,30 @@ class SongFormat(object):
|
||||||
OpenLP2 = 1
|
OpenLP2 = 1
|
||||||
Generic = 2
|
Generic = 2
|
||||||
CCLI = 3
|
CCLI = 3
|
||||||
DreamBeam = 4
|
ChordPro = 4
|
||||||
EasySlides = 5
|
DreamBeam = 5
|
||||||
EasyWorshipDB = 6
|
EasySlides = 6
|
||||||
EasyWorshipService = 7
|
EasyWorshipDB = 7
|
||||||
FoilPresenter = 8
|
EasyWorshipService = 8
|
||||||
Lyrix = 9
|
FoilPresenter = 9
|
||||||
MediaShout = 10
|
Lyrix = 10
|
||||||
OpenSong = 11
|
MediaShout = 11
|
||||||
OPSPro = 12
|
OpenSong = 12
|
||||||
PowerPraise = 13
|
OPSPro = 13
|
||||||
PowerSong = 14
|
PowerPraise = 14
|
||||||
PresentationManager = 15
|
PowerSong = 15
|
||||||
ProPresenter = 16
|
PresentationManager = 16
|
||||||
SongBeamer = 17
|
ProPresenter = 17
|
||||||
SongPro = 18
|
SongBeamer = 18
|
||||||
SongShowPlus = 19
|
SongPro = 19
|
||||||
SongsOfFellowship = 20
|
SongShowPlus = 20
|
||||||
SundayPlus = 21
|
SongsOfFellowship = 21
|
||||||
VideoPsalm = 22
|
SundayPlus = 22
|
||||||
WordsOfWorship = 23
|
VideoPsalm = 23
|
||||||
WorshipAssistant = 24
|
WordsOfWorship = 24
|
||||||
WorshipCenterPro = 25
|
WorshipAssistant = 25
|
||||||
ZionWorx = 26
|
WorshipCenterPro = 26
|
||||||
|
ZionWorx = 27
|
||||||
|
|
||||||
# Set optional attribute defaults
|
# Set optional attribute defaults
|
||||||
__defaults__ = {
|
__defaults__ = {
|
||||||
|
@ -224,6 +226,13 @@ class SongFormat(object):
|
||||||
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||||
'CCLI SongSelect Files'))
|
'CCLI SongSelect Files'))
|
||||||
},
|
},
|
||||||
|
ChordPro: {
|
||||||
|
'class': ChordProImport,
|
||||||
|
'name': 'ChordPro',
|
||||||
|
'prefix': 'chordPro',
|
||||||
|
'filter': '{text} (*.cho *.crd *.chordpro *.chopro *.txt)'.format(
|
||||||
|
text=translate('SongsPlugin.ImportWizardForm', 'ChordPro Files'))
|
||||||
|
},
|
||||||
DreamBeam: {
|
DreamBeam: {
|
||||||
'class': DreamBeamImport,
|
'class': DreamBeamImport,
|
||||||
'name': 'DreamBeam',
|
'name': 'DreamBeam',
|
||||||
|
@ -427,6 +436,7 @@ class SongFormat(object):
|
||||||
SongFormat.OpenLP2,
|
SongFormat.OpenLP2,
|
||||||
SongFormat.Generic,
|
SongFormat.Generic,
|
||||||
SongFormat.CCLI,
|
SongFormat.CCLI,
|
||||||
|
SongFormat.ChordPro,
|
||||||
SongFormat.DreamBeam,
|
SongFormat.DreamBeam,
|
||||||
SongFormat.EasySlides,
|
SongFormat.EasySlides,
|
||||||
SongFormat.EasyWorshipDB,
|
SongFormat.EasyWorshipDB,
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2016 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; 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 #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`chordpro` module provides the functionality for importing
|
||||||
|
ChordPro files into the current database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from openlp.core.common import Settings
|
||||||
|
|
||||||
|
from .songimport import SongImport
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ChordProImport(SongImport):
|
||||||
|
"""
|
||||||
|
The :class:`ChordProImport` class provides OpenLP with the
|
||||||
|
ability to import ChordPro files.
|
||||||
|
This importer is based on the information available on these webpages:
|
||||||
|
http://webchord.sourceforge.net/tech.html
|
||||||
|
http://www.vromans.org/johan/projects/Chordii/chordpro/
|
||||||
|
http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
|
||||||
|
http://linkesoft.com/songbook/chordproformat.html
|
||||||
|
"""
|
||||||
|
def do_import(self):
|
||||||
|
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
|
||||||
|
for filename in self.import_source:
|
||||||
|
if self.stop_import_flag:
|
||||||
|
return
|
||||||
|
song_file = open(filename, 'rt')
|
||||||
|
self.do_import_file(song_file)
|
||||||
|
song_file.close()
|
||||||
|
|
||||||
|
def do_import_file(self, song_file):
|
||||||
|
"""
|
||||||
|
Imports the songs in the given file
|
||||||
|
:param song_file: The file object to be imported from.
|
||||||
|
"""
|
||||||
|
self.set_defaults()
|
||||||
|
# Loop over the lines of the file
|
||||||
|
file_content = song_file.read()
|
||||||
|
current_verse = ''
|
||||||
|
current_verse_type = 'v'
|
||||||
|
skip_block = False
|
||||||
|
for line in file_content.splitlines():
|
||||||
|
line = line.rstrip()
|
||||||
|
# Detect tags
|
||||||
|
if line.startswith('{'):
|
||||||
|
tag_name, tag_value = self.parse_tag(line)
|
||||||
|
# Detect which tag
|
||||||
|
if tag_name in ['title', 't']:
|
||||||
|
self.title = tag_value
|
||||||
|
elif tag_name in ['subtitle', 'su', 'st']:
|
||||||
|
self.alternate_title = tag_value
|
||||||
|
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
|
||||||
|
# Detect if the comment is used as a chorus repeat marker
|
||||||
|
if tag_value.lower().startswith('chorus'):
|
||||||
|
if current_verse.strip():
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
self.repeat_verse('c1')
|
||||||
|
else:
|
||||||
|
self.add_comment(tag_value)
|
||||||
|
elif tag_name in ['start_of_chorus', 'soc']:
|
||||||
|
current_verse_type = 'c'
|
||||||
|
elif tag_name in ['end_of_chorus', 'eoc']:
|
||||||
|
# Add collected chorus to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
elif tag_name in ['start_of_tab', 'sot']:
|
||||||
|
if current_verse.strip():
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
skip_block = True
|
||||||
|
elif tag_name in ['end_of_tab', 'eot']:
|
||||||
|
skip_block = False
|
||||||
|
elif tag_name in ['new_song', 'ns']:
|
||||||
|
# A new song starts below this tag
|
||||||
|
if self.verses and self.title:
|
||||||
|
if current_verse.strip():
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
if not self.finish():
|
||||||
|
self.log_error(song_file.name)
|
||||||
|
self.set_defaults()
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
else:
|
||||||
|
# Unsupported tag
|
||||||
|
log.debug('unsupported tag: %s' % line)
|
||||||
|
elif line.startswith('#'):
|
||||||
|
# Found a comment line, which is ignored...
|
||||||
|
continue
|
||||||
|
elif line == "['|]":
|
||||||
|
# Found a vertical bar
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if skip_block:
|
||||||
|
continue
|
||||||
|
elif line == '' and current_verse.strip() and current_verse_type != 'c':
|
||||||
|
# Add collected verse to the lyrics
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
current_verse_type = 'v'
|
||||||
|
current_verse = ''
|
||||||
|
else:
|
||||||
|
if current_verse.strip() == '':
|
||||||
|
current_verse = line + '\n'
|
||||||
|
else:
|
||||||
|
current_verse += line + '\n'
|
||||||
|
if current_verse.strip():
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||||
|
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||||
|
if not self.finish():
|
||||||
|
self.log_error(song_file.name)
|
||||||
|
|
||||||
|
def parse_tag(self, line):
|
||||||
|
"""
|
||||||
|
:param line: Line with the tag to be parsed
|
||||||
|
:return: A tuple with tag name and tag value (if any)
|
||||||
|
"""
|
||||||
|
# Strip the first '}'
|
||||||
|
line = line[1:].strip()
|
||||||
|
colon_idx = line.find(':')
|
||||||
|
# check if this is a tag without value
|
||||||
|
if colon_idx < 0:
|
||||||
|
# strip the final '}' and return the tag name
|
||||||
|
return line[:-1], None
|
||||||
|
tag_name = line[:colon_idx]
|
||||||
|
tag_value = line[colon_idx + 1:-1].strip()
|
||||||
|
return tag_name, tag_value
|
|
@ -26,7 +26,7 @@ import re
|
||||||
from lxml import objectify
|
from lxml import objectify
|
||||||
from lxml.etree import Error, LxmlError
|
from lxml.etree import Error, LxmlError
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
from openlp.plugins.songs.lib.ui import SongStrings
|
from openlp.plugins.songs.lib.ui import SongStrings
|
||||||
|
@ -87,7 +87,7 @@ 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 preceded 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. For example::
|
||||||
|
|
||||||
. A7 Bm
|
. A7 Bm
|
||||||
1 Some____ Words
|
1 Some____ Words
|
||||||
|
@ -195,14 +195,34 @@ class OpenSongImport(SongImport):
|
||||||
lyrics = str(root.lyrics)
|
lyrics = str(root.lyrics)
|
||||||
else:
|
else:
|
||||||
lyrics = ''
|
lyrics = ''
|
||||||
|
chords = []
|
||||||
for this_line in lyrics.split('\n'):
|
for this_line in lyrics.split('\n'):
|
||||||
if not this_line.strip():
|
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(';'):
|
||||||
continue
|
continue
|
||||||
# skip guitar chords and page and column breaks
|
# skip page and column breaks
|
||||||
if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
|
if this_line.startswith('---') or this_line.startswith('-!!'):
|
||||||
|
continue
|
||||||
|
# guitar chords marker
|
||||||
|
if this_line.startswith('.'):
|
||||||
|
# Find the position of the chords so they can be inserted in the lyrics
|
||||||
|
chords = []
|
||||||
|
this_line = this_line[1:]
|
||||||
|
chord = ''
|
||||||
|
i = 0
|
||||||
|
while i < len(this_line):
|
||||||
|
if this_line[i] != ' ':
|
||||||
|
chord_pos = i
|
||||||
|
chord += this_line[i]
|
||||||
|
i += 1
|
||||||
|
while i < len(this_line) and this_line[i] != ' ':
|
||||||
|
chord += this_line[i]
|
||||||
|
i += 1
|
||||||
|
chords.append((chord_pos, chord))
|
||||||
|
chord = ''
|
||||||
|
i += 1
|
||||||
continue
|
continue
|
||||||
# verse/chorus/etc. marker
|
# verse/chorus/etc. marker
|
||||||
if this_line.startswith('['):
|
if this_line.startswith('['):
|
||||||
|
@ -228,12 +248,20 @@ class OpenSongImport(SongImport):
|
||||||
# number at start of line.. it's verse number
|
# number at start of line.. it's verse number
|
||||||
if this_line[0].isdigit():
|
if this_line[0].isdigit():
|
||||||
verse_num = this_line[0]
|
verse_num = this_line[0]
|
||||||
this_line = this_line[1:].strip()
|
this_line = this_line[1:]
|
||||||
verses.setdefault(verse_tag, {})
|
verses.setdefault(verse_tag, {})
|
||||||
verses[verse_tag].setdefault(verse_num, {})
|
verses[verse_tag].setdefault(verse_num, {})
|
||||||
if inst not in verses[verse_tag][verse_num]:
|
if inst not in verses[verse_tag][verse_num]:
|
||||||
verses[verse_tag][verse_num][inst] = []
|
verses[verse_tag][verse_num][inst] = []
|
||||||
our_verse_order.append([verse_tag, verse_num, inst])
|
our_verse_order.append([verse_tag, verse_num, inst])
|
||||||
|
# If chords exists insert them
|
||||||
|
if chords and Settings().value('songs/enable chords') and not Settings().value(
|
||||||
|
'songs/disable chords import'):
|
||||||
|
offset = 0
|
||||||
|
for (column, chord) in chords:
|
||||||
|
this_line = '{pre}[{chord}]{post}'.format(pre=this_line[:offset + column], chord=chord,
|
||||||
|
post=this_line[offset + column:])
|
||||||
|
offset += len(chord) + 2
|
||||||
# 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('_', '')
|
||||||
|
|
|
@ -25,10 +25,12 @@ The :mod:`songbeamer` module provides the functionality for importing SongBeamer
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import base64
|
||||||
|
import math
|
||||||
|
|
||||||
from openlp.core.common import get_file_encoding
|
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
|
from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -60,6 +62,13 @@ class SongBeamerTypes(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VerseTagMode(object):
|
||||||
|
Unknown = 0
|
||||||
|
ContainsTags = 1
|
||||||
|
ContainsNoTags = 2
|
||||||
|
ContainsNoTagsRestart = 3
|
||||||
|
|
||||||
|
|
||||||
class SongBeamerImport(SongImport):
|
class SongBeamerImport(SongImport):
|
||||||
"""
|
"""
|
||||||
Import Song Beamer files(s). Song Beamer file format is text based in the beginning are one or more control tags
|
Import Song Beamer files(s). Song Beamer file format is text based in the beginning are one or more control tags
|
||||||
|
@ -109,7 +118,7 @@ class SongBeamerImport(SongImport):
|
||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
self.current_verse = ''
|
self.current_verse = ''
|
||||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||||
read_verses = False
|
self.chord_table = None
|
||||||
file_name = os.path.split(import_file)[1]
|
file_name = os.path.split(import_file)[1]
|
||||||
if os.path.isfile(import_file):
|
if os.path.isfile(import_file):
|
||||||
# Detect the encoding
|
# Detect the encoding
|
||||||
|
@ -125,33 +134,103 @@ class SongBeamerImport(SongImport):
|
||||||
continue
|
continue
|
||||||
self.title = file_name.split('.sng')[0]
|
self.title = file_name.split('.sng')[0]
|
||||||
read_verses = False
|
read_verses = False
|
||||||
for line in song_data:
|
# The first verse separator doesn't count, but the others does, so line count starts at -1
|
||||||
# Just make sure that the line is of the type 'Unicode'.
|
line_number = -1
|
||||||
line = str(line).strip()
|
verse_tags_mode = VerseTagMode.Unknown
|
||||||
|
first_verse = True
|
||||||
|
idx = -1
|
||||||
|
while idx + 1 < len(song_data):
|
||||||
|
idx = idx + 1
|
||||||
|
line = song_data[idx].rstrip()
|
||||||
|
stripped_line = line.strip()
|
||||||
if line.startswith('#') and not read_verses:
|
if line.startswith('#') and not read_verses:
|
||||||
self.parseTags(line)
|
self.parse_tags(line)
|
||||||
elif line.startswith('--'):
|
elif stripped_line.startswith('---'):
|
||||||
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
|
# '---' is a verse breaker
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
self.current_verse = ''
|
self.current_verse = ''
|
||||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||||
|
first_verse = False
|
||||||
read_verses = True
|
read_verses = True
|
||||||
verse_start = True
|
verse_start = True
|
||||||
|
# Songbeamer allows chord on line "-1", meaning the first line has only chords
|
||||||
|
if line_number == -1:
|
||||||
|
first_line = self.insert_chords(line_number, '')
|
||||||
|
if first_line:
|
||||||
|
self.current_verse = first_line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
|
elif stripped_line.startswith('--'):
|
||||||
|
# '--' is a page breaker, we convert to optional page break
|
||||||
|
self.current_verse += '[---]\n'
|
||||||
|
line_number += 1
|
||||||
elif read_verses:
|
elif read_verses:
|
||||||
if verse_start:
|
if verse_start:
|
||||||
verse_start = False
|
verse_start = False
|
||||||
if not self.check_verse_marks(line):
|
verse_mark = self.check_verse_marks(line)
|
||||||
self.current_verse = line + '\n'
|
# To ensure that linenumbers are mapped correctly when inserting chords, we attempt to detect
|
||||||
|
# if verse tags are inserted manually or by SongBeamer. If they are inserted manually the lines
|
||||||
|
# should be counted, otherwise not. If all verses start with a tag we assume it is inserted by
|
||||||
|
# SongBeamer.
|
||||||
|
if first_verse and verse_tags_mode == VerseTagMode.Unknown:
|
||||||
|
if verse_mark:
|
||||||
|
verse_tags_mode = VerseTagMode.ContainsTags
|
||||||
else:
|
else:
|
||||||
self.current_verse += line + '\n'
|
verse_tags_mode = VerseTagMode.ContainsNoTags
|
||||||
|
elif verse_tags_mode != VerseTagMode.ContainsNoTagsRestart:
|
||||||
|
if not verse_mark and verse_tags_mode == VerseTagMode.ContainsTags:
|
||||||
|
# A verse mark was expected but not found, which means that verse marks has not been
|
||||||
|
# inserted by songbeamer, but are manually added headings. So restart the loop, and
|
||||||
|
# count tags as lines.
|
||||||
|
self.set_defaults()
|
||||||
|
self.title = file_name.split('.sng')[0]
|
||||||
|
verse_tags_mode = VerseTagMode.ContainsNoTagsRestart
|
||||||
|
read_verses = False
|
||||||
|
# The first verseseparator doesn't count, but the others does, so linecount starts at -1
|
||||||
|
line_number = -1
|
||||||
|
first_verse = True
|
||||||
|
idx = -1
|
||||||
|
continue
|
||||||
|
if not verse_mark:
|
||||||
|
line = self.insert_chords(line_number, line)
|
||||||
|
self.current_verse += line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
|
elif verse_tags_mode in [VerseTagMode.ContainsNoTags, VerseTagMode.ContainsNoTagsRestart]:
|
||||||
|
line_number += 1
|
||||||
|
else:
|
||||||
|
line = self.insert_chords(line_number, line)
|
||||||
|
self.current_verse += line.strip() + '\n'
|
||||||
|
line_number += 1
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
if not self.finish():
|
if not self.finish():
|
||||||
self.log_error(import_file)
|
self.log_error(import_file)
|
||||||
|
|
||||||
|
def insert_chords(self, line_number, line):
|
||||||
|
"""
|
||||||
|
Insert chords into text if any exists and chords import is enabled
|
||||||
|
|
||||||
|
:param linenumber: Number of the current line
|
||||||
|
:param line: The line of lyrics to insert chords
|
||||||
|
"""
|
||||||
|
if self.chord_table and Settings().value('songs/enable chords') and not Settings().value(
|
||||||
|
'songs/disable chords import') and line_number in self.chord_table:
|
||||||
|
line_idx = sorted(self.chord_table[line_number].keys(), reverse=True)
|
||||||
|
for idx in line_idx:
|
||||||
|
# In SongBeamer the column position of the chord can be a decimal, we just round it up.
|
||||||
|
int_idx = int(math.ceil(idx))
|
||||||
|
if int_idx < 0:
|
||||||
|
int_idx = 0
|
||||||
|
elif int_idx > len(line):
|
||||||
|
# If a chord is placed beyond the current end of the line, extend the line with spaces.
|
||||||
|
line += ' ' * (int_idx - len(line))
|
||||||
|
chord = self.chord_table[line_number][idx]
|
||||||
|
chord = chord.replace('<', '♭')
|
||||||
|
line = line[:int_idx] + '[' + chord + ']' + line[int_idx:]
|
||||||
|
return line
|
||||||
|
|
||||||
def replace_html_tags(self):
|
def replace_html_tags(self):
|
||||||
"""
|
"""
|
||||||
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
|
This can be called to replace SongBeamer's specific (html) tags with OpenLP's specific (html) tags.
|
||||||
|
@ -159,7 +238,7 @@ class SongBeamerImport(SongImport):
|
||||||
for pair in SongBeamerImport.HTML_TAG_PAIRS:
|
for pair in SongBeamerImport.HTML_TAG_PAIRS:
|
||||||
self.current_verse = pair[0].sub(pair[1], self.current_verse)
|
self.current_verse = pair[0].sub(pair[1], self.current_verse)
|
||||||
|
|
||||||
def parseTags(self, line):
|
def parse_tags(self, line):
|
||||||
"""
|
"""
|
||||||
Parses a meta data line.
|
Parses a meta data line.
|
||||||
|
|
||||||
|
@ -176,8 +255,10 @@ class SongBeamerImport(SongImport):
|
||||||
self.add_copyright(tag_val[1])
|
self.add_copyright(tag_val[1])
|
||||||
elif tag_val[0] == '#AddCopyrightInfo':
|
elif tag_val[0] == '#AddCopyrightInfo':
|
||||||
pass
|
pass
|
||||||
|
elif tag_val[0] == '#AudioFile':
|
||||||
|
self.parse_audio_file(tag_val[1])
|
||||||
elif tag_val[0] == '#Author':
|
elif tag_val[0] == '#Author':
|
||||||
self.parse_author(tag_val[1])
|
self.parse_author(tag_val[1], 'words')
|
||||||
elif tag_val[0] == '#BackgroundImage':
|
elif tag_val[0] == '#BackgroundImage':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Bible':
|
elif tag_val[0] == '#Bible':
|
||||||
|
@ -187,12 +268,15 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#CCLI':
|
elif tag_val[0] == '#CCLI':
|
||||||
self.ccli_number = tag_val[1]
|
self.ccli_number = tag_val[1]
|
||||||
elif tag_val[0] == '#Chords':
|
elif tag_val[0] == '#Chords':
|
||||||
pass
|
self.chord_table = self.parse_chords(tag_val[1])
|
||||||
elif tag_val[0] == '#ChurchSongID':
|
elif tag_val[0] == '#ChurchSongID':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#ColorChords':
|
elif tag_val[0] == '#ColorChords':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Comments':
|
elif tag_val[0] == '#Comments':
|
||||||
|
try:
|
||||||
|
self.comments = base64.b64decode(tag_val[1]).decode(self.input_file_encoding)
|
||||||
|
except ValueError:
|
||||||
self.comments = tag_val[1]
|
self.comments = tag_val[1]
|
||||||
elif tag_val[0] == '#Editor':
|
elif tag_val[0] == '#Editor':
|
||||||
pass
|
pass
|
||||||
|
@ -217,7 +301,7 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#LangCount':
|
elif tag_val[0] == '#LangCount':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Melody':
|
elif tag_val[0] == '#Melody':
|
||||||
self.parse_author(tag_val[1])
|
self.parse_author(tag_val[1], 'music')
|
||||||
elif tag_val[0] == '#NatCopyright':
|
elif tag_val[0] == '#NatCopyright':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#OTitle':
|
elif tag_val[0] == '#OTitle':
|
||||||
|
@ -243,7 +327,7 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#TextAlign':
|
elif tag_val[0] == '#TextAlign':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#Title':
|
elif tag_val[0] == '#Title':
|
||||||
self.title = str(tag_val[1]).strip()
|
self.title = tag_val[1].strip()
|
||||||
elif tag_val[0] == '#TitleAlign':
|
elif tag_val[0] == '#TitleAlign':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#TitleFontSize':
|
elif tag_val[0] == '#TitleFontSize':
|
||||||
|
@ -263,25 +347,80 @@ class SongBeamerImport(SongImport):
|
||||||
elif tag_val[0] == '#Version':
|
elif tag_val[0] == '#Version':
|
||||||
pass
|
pass
|
||||||
elif tag_val[0] == '#VerseOrder':
|
elif tag_val[0] == '#VerseOrder':
|
||||||
# TODO: add the verse order.
|
verse_order = tag_val[1].strip()
|
||||||
pass
|
for verse_mark in verse_order.split(','):
|
||||||
|
new_verse_mark = self.convert_verse_marks(verse_mark)
|
||||||
|
if new_verse_mark:
|
||||||
|
self.verse_order_list.append(new_verse_mark)
|
||||||
|
|
||||||
def check_verse_marks(self, line):
|
def check_verse_marks(self, line):
|
||||||
"""
|
"""
|
||||||
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
||||||
``False``.
|
``False``.
|
||||||
|
|
||||||
:param line: The line to check for marks (unicode).
|
:param line: The line to check for marks.
|
||||||
"""
|
"""
|
||||||
marks = line.split(' ')
|
new_verse_mark = self.convert_verse_marks(line)
|
||||||
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
if new_verse_mark:
|
||||||
self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
self.current_verse_type = new_verse_mark
|
||||||
if len(marks) == 2:
|
|
||||||
# If we have a digit, we append it to current_verse_type.
|
|
||||||
if marks[1].isdigit():
|
|
||||||
self.current_verse_type += marks[1]
|
|
||||||
return True
|
|
||||||
elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
|
|
||||||
self.current_verse_type = SongBeamerTypes.MarkTypes['$$m=']
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def convert_verse_marks(self, line):
|
||||||
|
"""
|
||||||
|
Convert the verse's MarkType. Returns the OpenLP versemark if the given line contains a correct SongBeamer verse
|
||||||
|
mark otherwise ``None``.
|
||||||
|
|
||||||
|
:param line: The line to check for marks.
|
||||||
|
"""
|
||||||
|
new_verse_mark = None
|
||||||
|
marks = line.split(' ')
|
||||||
|
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
||||||
|
new_verse_mark = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
||||||
|
if len(marks) == 2:
|
||||||
|
# If we have a digit, we append it to the converted verse mark
|
||||||
|
if marks[1].isdigit():
|
||||||
|
new_verse_mark += marks[1]
|
||||||
|
elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
|
||||||
|
new_verse_mark = SongBeamerTypes.MarkTypes['$$m=']
|
||||||
|
return new_verse_mark
|
||||||
|
|
||||||
|
def parse_chords(self, chords):
|
||||||
|
"""
|
||||||
|
Parse chords. The chords are in a base64 encode string. The decoded string is an index of chord placement
|
||||||
|
separated by "\r", like this: "<linecolumn>,<linenumber>,<chord>\r"
|
||||||
|
|
||||||
|
:param chords: Chords in a base64 encoded string
|
||||||
|
"""
|
||||||
|
chord_list = base64.b64decode(chords).decode(self.input_file_encoding).split('\r')
|
||||||
|
chord_table = {}
|
||||||
|
for chord_index in chord_list:
|
||||||
|
if not chord_index:
|
||||||
|
continue
|
||||||
|
[col_str, line_str, chord] = chord_index.split(',')
|
||||||
|
col = float(col_str)
|
||||||
|
line = int(line_str)
|
||||||
|
if line not in chord_table:
|
||||||
|
chord_table[line] = {}
|
||||||
|
chord_table[line][col] = chord
|
||||||
|
return chord_table
|
||||||
|
|
||||||
|
def parse_audio_file(self, audio_file_path):
|
||||||
|
"""
|
||||||
|
Parse audio file. The path is relative to the SongsBeamer Songs folder.
|
||||||
|
|
||||||
|
:param audio_file_path: Path to the audio file
|
||||||
|
"""
|
||||||
|
# The path is relative to SongBeamers Song folder
|
||||||
|
if is_win():
|
||||||
|
user_doc_folder = os.path.expandvars('$DOCUMENTS')
|
||||||
|
elif is_macosx():
|
||||||
|
user_doc_folder = os.path.join(os.path.expanduser('~'), 'Documents')
|
||||||
|
else:
|
||||||
|
# SongBeamer only runs on mac and win...
|
||||||
|
return
|
||||||
|
audio_file_path = os.path.normpath(os.path.join(user_doc_folder, 'SongBeamer', 'Songs', audio_file_path))
|
||||||
|
if os.path.isfile(audio_file_path):
|
||||||
|
self.add_media_file(audio_file_path)
|
||||||
|
else:
|
||||||
|
log.debug('Could not import mediafile "%s" since it does not exists!' % audio_file_path)
|
||||||
|
|
|
@ -242,7 +242,7 @@ class SongImport(QtCore.QObject):
|
||||||
self.copyright += ' '
|
self.copyright += ' '
|
||||||
self.copyright += copyright
|
self.copyright += copyright
|
||||||
|
|
||||||
def parse_author(self, text):
|
def parse_author(self, text, type=None):
|
||||||
"""
|
"""
|
||||||
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
|
Add the author. OpenLP stores them individually so split by 'and', '&' and comma. However need to check
|
||||||
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
||||||
|
@ -256,6 +256,9 @@ class SongImport(QtCore.QObject):
|
||||||
if author2.endswith('.'):
|
if author2.endswith('.'):
|
||||||
author2 = author2[:-1]
|
author2 = author2[:-1]
|
||||||
if author2:
|
if author2:
|
||||||
|
if type:
|
||||||
|
self.add_author(author2, type)
|
||||||
|
else:
|
||||||
self.add_author(author2)
|
self.add_author(author2)
|
||||||
|
|
||||||
def add_author(self, author, type=None):
|
def add_author(self, author, type=None):
|
||||||
|
@ -304,11 +307,22 @@ class SongImport(QtCore.QObject):
|
||||||
if verse_def not in self.verse_order_list_generated:
|
if verse_def not in self.verse_order_list_generated:
|
||||||
self.verse_order_list_generated.append(verse_def)
|
self.verse_order_list_generated.append(verse_def)
|
||||||
|
|
||||||
def repeat_verse(self):
|
def repeat_verse(self, verse_def=None):
|
||||||
"""
|
"""
|
||||||
Repeat the previous verse in the verse order
|
Repeat the verse with the given verse_def or default to repeating the previous verse in the verse order
|
||||||
|
|
||||||
|
:param verse_def: verse_def of the verse to be repeated
|
||||||
"""
|
"""
|
||||||
if self.verse_order_list_generated:
|
if self.verse_order_list_generated:
|
||||||
|
if verse_def:
|
||||||
|
# If the given verse_def is only one char (like 'v' or 'c'), postfix it with '1'
|
||||||
|
if len(verse_def) == 1:
|
||||||
|
verse_def += '1'
|
||||||
|
if verse_def in self.verse_order_list_generated:
|
||||||
|
self.verse_order_list_generated.append(verse_def)
|
||||||
|
else:
|
||||||
|
log.warning('Trying to add unknown verse_def "%s"' % verse_def)
|
||||||
|
else:
|
||||||
self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
|
self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
|
||||||
self.verse_order_list_generated_useful = True
|
self.verse_order_list_generated_useful = True
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,9 @@ exproted from Lyrix."""
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||||
from openlp.plugins.songs.lib.db import AuthorType
|
from openlp.plugins.songs.lib.db import AuthorType
|
||||||
|
|
||||||
|
@ -123,7 +124,11 @@ class VideoPsalmImport(SongImport):
|
||||||
for verse in song['Verses']:
|
for verse in song['Verses']:
|
||||||
if 'Text' not in verse:
|
if 'Text' not in verse:
|
||||||
continue
|
continue
|
||||||
self.add_verse(verse['Text'], 'v')
|
verse_text = verse['Text']
|
||||||
|
# Strip out chords if set up to
|
||||||
|
if not Settings().value('songs/enable chords') or Settings().value('songs/disable chords import'):
|
||||||
|
verse_text = re.sub(r'\[.*?\]', '', verse_text)
|
||||||
|
self.add_verse(verse_text, 'v')
|
||||||
if not self.finish():
|
if not self.finish():
|
||||||
self.log_error('Could not import {title}'.format(title=self.title))
|
self.log_error('Could not import {title}'.format(title=self.title))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -61,7 +61,7 @@ import re
|
||||||
|
|
||||||
from lxml import etree, objectify
|
from lxml import etree, objectify
|
||||||
|
|
||||||
from openlp.core.common import translate
|
from openlp.core.common import translate, Settings
|
||||||
from openlp.core.common.versionchecker import get_application_version
|
from openlp.core.common.versionchecker import get_application_version
|
||||||
from openlp.core.lib import FormattingTags
|
from openlp.core.lib import FormattingTags
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||||
|
@ -154,7 +154,7 @@ class OpenLyrics(object):
|
||||||
OpenLP does not support the attribute *lang*.
|
OpenLP does not support the attribute *lang*.
|
||||||
|
|
||||||
``<chord>``
|
``<chord>``
|
||||||
This property is not supported.
|
This property is fully supported.
|
||||||
|
|
||||||
``<comments>``
|
``<comments>``
|
||||||
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
The ``<comments>`` property is fully supported. But comments in lyrics are not supported.
|
||||||
|
@ -323,7 +323,19 @@ class OpenLyrics(object):
|
||||||
# Do not add the break attribute to the last lines element.
|
# Do not add the break attribute to the last lines element.
|
||||||
if index < len(optional_verses) - 1:
|
if index < len(optional_verses) - 1:
|
||||||
lines_element.set('break', 'optional')
|
lines_element.set('break', 'optional')
|
||||||
return self._extract_xml(song_xml).decode()
|
xml_text = self._extract_xml(song_xml).decode()
|
||||||
|
return self._chordpro_to_openlyrics(xml_text)
|
||||||
|
|
||||||
|
def _chordpro_to_openlyrics(self, text):
|
||||||
|
"""
|
||||||
|
Convert chords from Chord Pro format to Open Lyrics format
|
||||||
|
|
||||||
|
:param text: the lyric with chords
|
||||||
|
:return: the lyrics with the converted chords
|
||||||
|
"""
|
||||||
|
# Process chords.
|
||||||
|
new_text = re.sub(r'\[(\w.*?)\]', r'<chord name="\1"/>', text)
|
||||||
|
return new_text
|
||||||
|
|
||||||
def _get_missing_tags(self, text):
|
def _get_missing_tags(self, text):
|
||||||
"""
|
"""
|
||||||
|
@ -595,8 +607,7 @@ class OpenLyrics(object):
|
||||||
|
|
||||||
def _process_lines_mixed_content(self, element, newlines=True):
|
def _process_lines_mixed_content(self, element, newlines=True):
|
||||||
"""
|
"""
|
||||||
Converts the xml text with mixed content to OpenLP representation. Chords are skipped and formatting tags are
|
Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
|
||||||
converted.
|
|
||||||
|
|
||||||
:param element: The property object (lxml.etree.Element).
|
:param element: The property object (lxml.etree.Element).
|
||||||
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
:param newlines: The switch to enable/disable processing of line breaks <br/>. The <br/> is used since
|
||||||
|
@ -608,12 +619,14 @@ class OpenLyrics(object):
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
if element.tag == NSMAP % 'comment':
|
if element.tag == NSMAP % 'comment':
|
||||||
if element.tail:
|
if element.tail:
|
||||||
# Append tail text at chord element.
|
# Append tail text at comment element.
|
||||||
text += element.tail
|
text += element.tail
|
||||||
return text
|
return text
|
||||||
# Skip <chord> element - not yet supported.
|
# Convert chords to ChordPro format which OpenLP uses internally
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
elif element.tag == NSMAP % 'chord':
|
elif element.tag == NSMAP % 'chord':
|
||||||
|
if Settings().value('songs/enable chords') and not Settings().value('songs/disable chords import'):
|
||||||
|
text += '[{chord}]'.format(chord=element.get('name'))
|
||||||
if element.tail:
|
if element.tail:
|
||||||
# Append tail text at chord element.
|
# Append tail text at chord element.
|
||||||
text += element.tail
|
text += element.tail
|
||||||
|
@ -666,7 +679,7 @@ class OpenLyrics(object):
|
||||||
text = self._process_lines_mixed_content(element)
|
text = self._process_lines_mixed_content(element)
|
||||||
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
||||||
else:
|
else:
|
||||||
# Loop over the "line" elements removing comments and chords.
|
# Loop over the "line" elements removing comments
|
||||||
for line in element:
|
for line in element:
|
||||||
# Skip comment lines.
|
# Skip comment lines.
|
||||||
# TODO: Verify format() with template variables
|
# TODO: Verify format() with template variables
|
||||||
|
|
|
@ -60,6 +60,35 @@ class SongsTab(SettingsTab):
|
||||||
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
||||||
self.mode_layout.addWidget(self.display_copyright_check_box)
|
self.mode_layout.addWidget(self.display_copyright_check_box)
|
||||||
self.left_layout.addWidget(self.mode_group_box)
|
self.left_layout.addWidget(self.mode_group_box)
|
||||||
|
# Chords group box
|
||||||
|
self.chords_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||||
|
self.chords_group_box.setObjectName('chords_group_box')
|
||||||
|
self.chords_group_box.setCheckable(True)
|
||||||
|
self.chords_layout = QtWidgets.QVBoxLayout(self.chords_group_box)
|
||||||
|
self.chords_layout.setObjectName('chords_layout')
|
||||||
|
self.chords_info_label = QtWidgets.QLabel(self.chords_group_box)
|
||||||
|
self.chords_info_label.setWordWrap(True)
|
||||||
|
self.chords_layout.addWidget(self.chords_info_label)
|
||||||
|
self.mainview_chords_check_box = QtWidgets.QCheckBox(self.mode_group_box)
|
||||||
|
self.mainview_chords_check_box.setObjectName('mainview_chords_check_box')
|
||||||
|
self.chords_layout.addWidget(self.mainview_chords_check_box)
|
||||||
|
self.disable_chords_import_check_box = QtWidgets.QCheckBox(self.mode_group_box)
|
||||||
|
self.disable_chords_import_check_box.setObjectName('disable_chords_import_check_box')
|
||||||
|
self.chords_layout.addWidget(self.disable_chords_import_check_box)
|
||||||
|
# Chords notation group box
|
||||||
|
self.chord_notation_label = QtWidgets.QLabel(self.chords_group_box)
|
||||||
|
self.chord_notation_label.setWordWrap(True)
|
||||||
|
self.chords_layout.addWidget(self.chord_notation_label)
|
||||||
|
self.english_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.english_notation_radio_button.setObjectName('english_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.english_notation_radio_button)
|
||||||
|
self.german_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.german_notation_radio_button.setObjectName('german_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.german_notation_radio_button)
|
||||||
|
self.neolatin_notation_radio_button = QtWidgets.QRadioButton(self.chords_group_box)
|
||||||
|
self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
|
||||||
|
self.chords_layout.addWidget(self.neolatin_notation_radio_button)
|
||||||
|
self.left_layout.addWidget(self.chords_group_box)
|
||||||
self.left_layout.addStretch()
|
self.left_layout.addStretch()
|
||||||
self.right_layout.addStretch()
|
self.right_layout.addStretch()
|
||||||
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
||||||
|
@ -68,6 +97,11 @@ class SongsTab(SettingsTab):
|
||||||
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
|
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
|
||||||
self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
|
self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
|
||||||
self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
|
self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
|
||||||
|
self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)
|
||||||
|
self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
|
||||||
|
self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)
|
||||||
|
self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked)
|
||||||
|
self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked)
|
||||||
|
|
||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
|
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
|
||||||
|
@ -82,6 +116,17 @@ class SongsTab(SettingsTab):
|
||||||
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||||
'Display "{symbol}" symbol before copyright '
|
'Display "{symbol}" symbol before copyright '
|
||||||
'info').format(symbol=SongStrings.CopyrightSymbol))
|
'info').format(symbol=SongStrings.CopyrightSymbol))
|
||||||
|
self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
|
||||||
|
'be regarded as chords.'))
|
||||||
|
self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
|
||||||
|
self.mainview_chords_check_box.setText(translate('SongsPlugin.SongsTab', 'Display chords in the main view'))
|
||||||
|
self.disable_chords_import_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||||
|
'Ignore chords when importing songs'))
|
||||||
|
self.chord_notation_label.setText(translate('SongsPlugin.SongsTab', 'Chord notation to use:'))
|
||||||
|
self.english_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'English') + ' (C-D-E-F-G-A-B)')
|
||||||
|
self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
|
||||||
|
self.neolatin_notation_radio_button.setText(
|
||||||
|
translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)')
|
||||||
|
|
||||||
def on_search_as_type_check_box_changed(self, check_state):
|
def on_search_as_type_check_box_changed(self, check_state):
|
||||||
self.song_search = (check_state == QtCore.Qt.Checked)
|
self.song_search = (check_state == QtCore.Qt.Checked)
|
||||||
|
@ -104,6 +149,21 @@ class SongsTab(SettingsTab):
|
||||||
def on_copyright_check_box_changed(self, check_state):
|
def on_copyright_check_box_changed(self, check_state):
|
||||||
self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
|
self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_mainview_chords_check_box_changed(self, check_state):
|
||||||
|
self.mainview_chords = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_disable_chords_import_check_box_changed(self, check_state):
|
||||||
|
self.disable_chords_import = (check_state == QtCore.Qt.Checked)
|
||||||
|
|
||||||
|
def on_english_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'english'
|
||||||
|
|
||||||
|
def on_german_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'german'
|
||||||
|
|
||||||
|
def on_neolatin_notation_button_clicked(self):
|
||||||
|
self.chord_notation = 'neo-latin'
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
settings.beginGroup(self.settings_section)
|
settings.beginGroup(self.settings_section)
|
||||||
|
@ -113,12 +173,25 @@ class SongsTab(SettingsTab):
|
||||||
self.display_songbook = settings.value('display songbook')
|
self.display_songbook = settings.value('display songbook')
|
||||||
self.display_written_by = settings.value('display written by')
|
self.display_written_by = settings.value('display written by')
|
||||||
self.display_copyright_symbol = settings.value('display copyright symbol')
|
self.display_copyright_symbol = settings.value('display copyright symbol')
|
||||||
|
self.enable_chords = settings.value('enable chords')
|
||||||
|
self.chord_notation = settings.value('chord notation')
|
||||||
|
self.mainview_chords = settings.value('mainview chords')
|
||||||
|
self.disable_chords_import = settings.value('disable chords import')
|
||||||
self.tool_bar_active_check_box.setChecked(self.tool_bar)
|
self.tool_bar_active_check_box.setChecked(self.tool_bar)
|
||||||
self.update_on_edit_check_box.setChecked(self.update_edit)
|
self.update_on_edit_check_box.setChecked(self.update_edit)
|
||||||
self.add_from_service_check_box.setChecked(self.update_load)
|
self.add_from_service_check_box.setChecked(self.update_load)
|
||||||
self.display_songbook_check_box.setChecked(self.display_songbook)
|
self.display_songbook_check_box.setChecked(self.display_songbook)
|
||||||
self.display_written_by_check_box.setChecked(self.display_written_by)
|
self.display_written_by_check_box.setChecked(self.display_written_by)
|
||||||
self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
|
self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
|
||||||
|
self.chords_group_box.setChecked(self.enable_chords)
|
||||||
|
self.mainview_chords_check_box.setChecked(self.mainview_chords)
|
||||||
|
self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
|
||||||
|
if self.chord_notation == 'german':
|
||||||
|
self.german_notation_radio_button.setChecked(True)
|
||||||
|
elif self.chord_notation == 'neo-latin':
|
||||||
|
self.neolatin_notation_radio_button.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.english_notation_radio_button.setChecked(True)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
@ -130,6 +203,10 @@ class SongsTab(SettingsTab):
|
||||||
settings.setValue('display songbook', self.display_songbook)
|
settings.setValue('display songbook', self.display_songbook)
|
||||||
settings.setValue('display written by', self.display_written_by)
|
settings.setValue('display written by', self.display_written_by)
|
||||||
settings.setValue('display copyright symbol', self.display_copyright_symbol)
|
settings.setValue('display copyright symbol', self.display_copyright_symbol)
|
||||||
|
settings.setValue('enable chords', self.chords_group_box.isChecked())
|
||||||
|
settings.setValue('mainview chords', self.mainview_chords)
|
||||||
|
settings.setValue('disable chords import', self.disable_chords_import)
|
||||||
|
settings.setValue('chord notation', self.chord_notation)
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
if self.tab_visited:
|
if self.tab_visited:
|
||||||
self.settings_form.register_post_process('songs_config_updated')
|
self.settings_form.register_post_process('songs_config_updated')
|
||||||
|
|
|
@ -66,7 +66,11 @@ __default_settings__ = {
|
||||||
'songs/last directory export': '',
|
'songs/last directory export': '',
|
||||||
'songs/songselect username': '',
|
'songs/songselect username': '',
|
||||||
'songs/songselect password': '',
|
'songs/songselect password': '',
|
||||||
'songs/songselect searches': ''
|
'songs/songselect searches': '',
|
||||||
|
'songs/enable chords': True,
|
||||||
|
'songs/chord notation': 'english', # Can be english, german or neo-latin
|
||||||
|
'songs/mainview chords': False,
|
||||||
|
'songs/disable chords import': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -250,5 +250,6 @@ def main():
|
||||||
print_qt_image_formats()
|
print_qt_image_formats()
|
||||||
print_enchant_backends_and_languages()
|
print_enchant_backends_and_languages()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -217,5 +217,6 @@ def main():
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
[pep8]
|
[pep8]
|
||||||
exclude=resources.py,vlc.py
|
exclude=resources.py,vlc.py
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
ignore = E402
|
ignore = E402,E722
|
||||||
|
|
|
@ -121,11 +121,11 @@ class TestCategoryActionList(TestCase):
|
||||||
self.list.add(self.action2)
|
self.list.add(self.action2)
|
||||||
|
|
||||||
# WHEN: Iterating over the list
|
# WHEN: Iterating over the list
|
||||||
l = [a for a in self.list]
|
list = [a for a in self.list]
|
||||||
# THEN: Make sure they are returned in correct order
|
# THEN: Make sure they are returned in correct order
|
||||||
self.assertEquals(len(self.list), 2)
|
self.assertEquals(len(self.list), 2)
|
||||||
self.assertIs(l[0], self.action1)
|
self.assertIs(list[0], self.action1)
|
||||||
self.assertIs(l[1], self.action2)
|
self.assertIs(list[1], self.action2)
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,11 +22,13 @@
|
||||||
"""
|
"""
|
||||||
Functional tests to test the AppLocation class and related methods.
|
Functional tests to test the AppLocation class and related methods.
|
||||||
"""
|
"""
|
||||||
|
from pathlib import Path
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, call, patch
|
||||||
|
|
||||||
from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \
|
from openlp.core import common
|
||||||
is_linux, clean_button_text
|
from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
|
||||||
|
is_linux, is_win, path_to_module, trace_error_handler, translate
|
||||||
|
|
||||||
|
|
||||||
class TestCommonFunctions(TestCase):
|
class TestCommonFunctions(TestCase):
|
||||||
|
@ -72,6 +74,72 @@ class TestCommonFunctions(TestCase):
|
||||||
mocked_exists.assert_called_with(directory_to_check)
|
mocked_exists.assert_called_with(directory_to_check)
|
||||||
self.assertRaises(ValueError, check_directory_exists, directory_to_check)
|
self.assertRaises(ValueError, check_directory_exists, directory_to_check)
|
||||||
|
|
||||||
|
def test_extension_loader_no_files_found(self):
|
||||||
|
"""
|
||||||
|
Test the `extension_loader` function when no files are found
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked `Path.glob` method which does not match any files
|
||||||
|
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||||
|
patch.object(common.Path, 'glob', return_value=[]), \
|
||||||
|
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
|
||||||
|
|
||||||
|
# WHEN: Calling `extension_loader`
|
||||||
|
extension_loader('glob', ['file2.py', 'file3.py'])
|
||||||
|
|
||||||
|
# THEN: `extension_loader` should not try to import any files
|
||||||
|
self.assertFalse(mocked_import_module.called)
|
||||||
|
|
||||||
|
def test_extension_loader_files_found(self):
|
||||||
|
"""
|
||||||
|
Test the `extension_loader` function when it successfully finds and loads some files
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked `Path.glob` method which returns a list of files
|
||||||
|
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||||
|
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py'),
|
||||||
|
Path('/app/dir/openlp/import_dir/file2.py'),
|
||||||
|
Path('/app/dir/openlp/import_dir/file3.py'),
|
||||||
|
Path('/app/dir/openlp/import_dir/file4.py')]), \
|
||||||
|
patch('openlp.core.common.importlib.import_module') as mocked_import_module:
|
||||||
|
|
||||||
|
# WHEN: Calling `extension_loader` with a list of files to exclude
|
||||||
|
extension_loader('glob', ['file2.py', 'file3.py'])
|
||||||
|
|
||||||
|
# THEN: `extension_loader` should only try to import the files that are matched by the blob, excluding the
|
||||||
|
# files listed in the `excluded_files` argument
|
||||||
|
mocked_import_module.assert_has_calls([call('openlp.import_dir.file1'), call('openlp.import_dir.file4')])
|
||||||
|
|
||||||
|
def test_extension_loader_import_error(self):
|
||||||
|
"""
|
||||||
|
Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked `import_module` which raises an `ImportError`
|
||||||
|
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||||
|
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
|
||||||
|
patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \
|
||||||
|
patch('openlp.core.common.log') as mocked_logger:
|
||||||
|
|
||||||
|
# WHEN: Calling `extension_loader`
|
||||||
|
extension_loader('glob')
|
||||||
|
|
||||||
|
# THEN: The `ImportError` should be caught and logged
|
||||||
|
self.assertTrue(mocked_logger.warning.called)
|
||||||
|
|
||||||
|
def test_extension_loader_os_error(self):
|
||||||
|
"""
|
||||||
|
Test the `extension_loader` function when `import_module` raises a `ImportError`
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked `SourceFileLoader` which raises an `OSError`
|
||||||
|
with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
|
||||||
|
patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
|
||||||
|
patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \
|
||||||
|
patch('openlp.core.common.log') as mocked_logger:
|
||||||
|
|
||||||
|
# WHEN: Calling `extension_loader`
|
||||||
|
extension_loader('glob')
|
||||||
|
|
||||||
|
# THEN: The `OSError` should be caught and logged
|
||||||
|
self.assertTrue(mocked_logger.warning.called)
|
||||||
|
|
||||||
def test_de_hump_conversion(self):
|
def test_de_hump_conversion(self):
|
||||||
"""
|
"""
|
||||||
Test the de_hump function with a class name
|
Test the de_hump function with a class name
|
||||||
|
@ -83,7 +151,7 @@ class TestCommonFunctions(TestCase):
|
||||||
new_string = de_hump(string)
|
new_string = de_hump(string)
|
||||||
|
|
||||||
# THEN: the new string should be converted to python format
|
# THEN: the new string should be converted to python format
|
||||||
self.assertTrue(new_string == "my_class", 'The class name should have been converted')
|
self.assertEqual(new_string, "my_class", 'The class name should have been converted')
|
||||||
|
|
||||||
def test_de_hump_static(self):
|
def test_de_hump_static(self):
|
||||||
"""
|
"""
|
||||||
|
@ -96,7 +164,20 @@ class TestCommonFunctions(TestCase):
|
||||||
new_string = de_hump(string)
|
new_string = de_hump(string)
|
||||||
|
|
||||||
# THEN: the new string should be converted to python format
|
# THEN: the new string should be converted to python format
|
||||||
self.assertTrue(new_string == "my_class", 'The class name should have been preserved')
|
self.assertEqual(new_string, "my_class", 'The class name should have been preserved')
|
||||||
|
|
||||||
|
def test_path_to_module(self):
|
||||||
|
"""
|
||||||
|
Test `path_to_module` when supplied with a `Path` object
|
||||||
|
"""
|
||||||
|
# GIVEN: A `Path` object
|
||||||
|
path = Path('openlp/core/ui/media/webkitplayer.py')
|
||||||
|
|
||||||
|
# WHEN: Calling path_to_module with the `Path` object
|
||||||
|
result = path_to_module(path)
|
||||||
|
|
||||||
|
# THEN: path_to_module should return the module name
|
||||||
|
self.assertEqual(result, 'openlp.core.ui.media.webkitplayer')
|
||||||
|
|
||||||
def test_trace_error_handler(self):
|
def test_trace_error_handler(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -8,7 +8,7 @@ from PyQt5 import QtCore, QtWebKit
|
||||||
|
|
||||||
from openlp.core.common import Settings
|
from openlp.core.common import Settings
|
||||||
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
|
from openlp.core.lib.htmlbuilder import build_html, build_background_css, build_lyrics_css, build_lyrics_outline_css, \
|
||||||
build_lyrics_format_css, build_footer_css, webkit_version
|
build_lyrics_format_css, build_footer_css, webkit_version, build_chords_css
|
||||||
from openlp.core.lib.theme import HorizontalType, VerticalType
|
from openlp.core.lib.theme import HorizontalType, VerticalType
|
||||||
|
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
@ -60,6 +60,29 @@ HTML = """
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
}
|
}
|
||||||
|
/* Chords css */
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 1.0em;
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var timer = null;
|
var timer = null;
|
||||||
|
@ -211,6 +234,34 @@ FOOTER_CSS_BASE = """
|
||||||
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
||||||
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
||||||
FOOTER_CSS_INVALID = ''
|
FOOTER_CSS_INVALID = ''
|
||||||
|
CHORD_CSS_ENABLED = """
|
||||||
|
.chordline {
|
||||||
|
line-height: 2.0em;
|
||||||
|
}
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.8em;
|
||||||
|
left: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.firstchordline {
|
||||||
|
line-height: 2.1em;
|
||||||
|
}
|
||||||
|
.ws {
|
||||||
|
display: inline;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
__default_settings__ = {
|
||||||
|
'songs/mainview chords': False,
|
||||||
|
'songs/enable chords': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Htmbuilder(TestCase, TestMixin):
|
class Htmbuilder(TestCase, TestMixin):
|
||||||
|
@ -222,6 +273,7 @@ class Htmbuilder(TestCase, TestMixin):
|
||||||
Create the UI
|
Create the UI
|
||||||
"""
|
"""
|
||||||
self.build_settings()
|
self.build_settings()
|
||||||
|
Settings().extend_default_settings(__default_settings__)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""
|
"""
|
||||||
|
@ -403,3 +455,17 @@ class Htmbuilder(TestCase, TestMixin):
|
||||||
# WHEN: Retrieving the webkit version
|
# WHEN: Retrieving the webkit version
|
||||||
# THEN: Webkit versions should match
|
# THEN: Webkit versions should match
|
||||||
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
|
self.assertEquals(webkit_version(), webkit_ver, "The returned webkit version doesn't match the installed one")
|
||||||
|
|
||||||
|
def test_build_chords_css(self):
|
||||||
|
"""
|
||||||
|
Test the build_chords_css() function
|
||||||
|
"""
|
||||||
|
# GIVEN: A setting that activates chords on the mainview
|
||||||
|
Settings().setValue('songs/enable chords', True)
|
||||||
|
Settings().setValue('songs/mainview chords', True)
|
||||||
|
|
||||||
|
# WHEN: Building the chord CSS
|
||||||
|
chord_css = build_chords_css()
|
||||||
|
|
||||||
|
# THEN: The build css should look as expected
|
||||||
|
self.assertEqual(CHORD_CSS_ENABLED, chord_css, 'The chord CSS should look as expected')
|
||||||
|
|
|
@ -29,8 +29,10 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import FormattingTags, expand_chords_for_printing
|
||||||
from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
|
from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
|
||||||
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
|
expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb, expand_chords, \
|
||||||
|
compare_chord_lyric, find_formatting_tags
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
||||||
|
|
||||||
|
@ -745,3 +747,116 @@ class TestLib(TestCase):
|
||||||
# THEN: We should have "Author 1, Author 2 and Author 3"
|
# THEN: We should have "Author 1, Author 2 and Author 3"
|
||||||
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
|
self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
|
||||||
'Author 2, and Author 3".')
|
'Author 2, and Author 3".')
|
||||||
|
|
||||||
|
def test_expand_chords(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords works as expected.
|
||||||
|
"""
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = 'H[C]alleluya.[F] [G]'
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_chords(text_with_chords)
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<span class="chordline firstchordline">H<span class="chord"><span><strong>C</strong></span>' \
|
||||||
|
'</span>alleluya.<span class="chord"><span><strong>F</strong></span></span><span class="ws">' \
|
||||||
|
' </span> <span class="chord"><span><strong>G</strong></span></span></span>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
||||||
|
def test_expand_chords2(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords works as expected when special chars are involved.
|
||||||
|
"""
|
||||||
|
import html
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = "I[D]'M NOT MOVED BY WHAT I SEE HALLE[F]LUJA[C]H"
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_tags(text_with_chords, True)
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<span class="chordline firstchordline">I<span class="chord"><span><strong>D</strong></span>' \
|
||||||
|
'</span>'M NOT MOVED BY WHAT I SEE HALLE<span class="chord"><span><strong>F</strong>' \
|
||||||
|
'</span></span>LUJA<span class="chord"><span><strong>C</strong></span></span>H</span>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
||||||
|
def test_compare_chord_lyric_short_chord(self):
|
||||||
|
"""
|
||||||
|
Test that the chord/lyric comparing works.
|
||||||
|
"""
|
||||||
|
# GIVEN: A chord and some lyric
|
||||||
|
chord = 'C'
|
||||||
|
lyrics = 'alleluya'
|
||||||
|
|
||||||
|
# WHEN: Comparing the chord and lyrics
|
||||||
|
ret = compare_chord_lyric(chord, lyrics)
|
||||||
|
|
||||||
|
# THEN: The returned value should 0 because the lyric is longer than the chord
|
||||||
|
self.assertEquals(0, ret, 'The returned value should 0 because the lyric is longer than the chord')
|
||||||
|
|
||||||
|
def test_compare_chord_lyric_long_chord(self):
|
||||||
|
"""
|
||||||
|
Test that the chord/lyric comparing works.
|
||||||
|
"""
|
||||||
|
# GIVEN: A chord and some lyric
|
||||||
|
chord = 'Gsus'
|
||||||
|
lyrics = 'me'
|
||||||
|
|
||||||
|
# WHEN: Comparing the chord and lyrics
|
||||||
|
ret = compare_chord_lyric(chord, lyrics)
|
||||||
|
|
||||||
|
# THEN: The returned value should 4 because the chord is longer than the lyric
|
||||||
|
self.assertEquals(4, ret, 'The returned value should 4 because the chord is longer than the lyric')
|
||||||
|
|
||||||
|
def test_find_formatting_tags(self):
|
||||||
|
"""
|
||||||
|
Test that find_formatting_tags works as expected
|
||||||
|
"""
|
||||||
|
# GIVEN: Lyrics with formatting tags and a empty list of formatting tags
|
||||||
|
lyrics = '{st}Amazing {r}grace{/r} how sweet the sound'
|
||||||
|
tags = []
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
|
# WHEN: Detecting active formatting tags
|
||||||
|
active_tags = find_formatting_tags(lyrics, tags)
|
||||||
|
|
||||||
|
# THEN: The list of active tags should contain only 'st'
|
||||||
|
self.assertListEqual(['st'], active_tags, 'The list of active tags should contain only "st"')
|
||||||
|
|
||||||
|
def test_expand_chords_for_printing(self):
|
||||||
|
"""
|
||||||
|
Test that the expanding of chords for printing works as expected.
|
||||||
|
"""
|
||||||
|
# GIVEN: A lyrics-line with chords
|
||||||
|
text_with_chords = '{st}[D]Amazing {r}gr[D7]ace{/r} how [G]sweet the [D]sound [F]{/st}'
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
|
# WHEN: Expanding the chords
|
||||||
|
text_with_expanded_chords = expand_chords_for_printing(text_with_chords, '{br}')
|
||||||
|
|
||||||
|
# THEN: We should get html that looks like below
|
||||||
|
expected_html = '<table class="line" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><table ' \
|
||||||
|
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
|
||||||
|
'<td class="chord"> </td><td class="chord">D</td></tr><tr><td class="lyrics">{st}{/st}' \
|
||||||
|
'</td><td class="lyrics">{st}Amazing {/st}</td></tr></table><table class="segment" ' \
|
||||||
|
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">' \
|
||||||
|
'<td class="chord"> </td><td class="chord">D7</td></tr><tr><td class="lyrics">{st}{r}gr' \
|
||||||
|
'{/r}{/st}</td><td class="lyrics">{r}{st}ace{/r} {/st}</td></tr></table><table ' \
|
||||||
|
'class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow">'\
|
||||||
|
'<td class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
|
||||||
|
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
|
||||||
|
'class="chordrow"><td class="chord"> </td></tr><tr><td class="lyrics">{st}how {/st}' \
|
||||||
|
'</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" border="0" ' \
|
||||||
|
'align="left"><tr class="chordrow"><td class="chord">G</td></tr><tr><td class="lyrics">{st}' \
|
||||||
|
'sweet {/st}</td></tr></table><table class="segment" cellpadding="0" cellspacing="0" ' \
|
||||||
|
'border="0" align="left"><tr class="chordrow"><td class="chord"> </td></tr><tr><td ' \
|
||||||
|
'class="lyrics">{st}the {/st}</td></tr></table><table class="segment" cellpadding="0" ' \
|
||||||
|
'cellspacing="0" border="0" align="left"><tr class="chordrow"><td class="chord">D</td></tr>' \
|
||||||
|
'<tr><td class="lyrics">{st}sound {/st}</td></tr></table><table class="segment" ' \
|
||||||
|
'cellpadding="0" cellspacing="0" border="0" align="left"><tr class="chordrow"><td ' \
|
||||||
|
'class="chord"> </td></tr><tr><td class="lyrics">{st} {/st}</td></tr></table>' \
|
||||||
|
'<table class="segment" cellpadding="0" cellspacing="0" border="0" align="left"><tr ' \
|
||||||
|
'class="chordrow"><td class="chord">F</td></tr><tr><td class="lyrics">{st}{/st} </td>' \
|
||||||
|
'</tr></table></td></tr></table>'
|
||||||
|
self.assertEqual(expected_html, text_with_expanded_chords, 'The expanded chords should look as expected!')
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2015 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; 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 #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
Package to test the openlp.core.lib.projector.constants package.
|
||||||
|
"""
|
||||||
|
from unittest import TestCase, skip
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectorConstants(TestCase):
|
||||||
|
"""
|
||||||
|
Test specific functions in the projector constants module.
|
||||||
|
"""
|
||||||
|
def build_pjlink_video_label_test(self):
|
||||||
|
"""
|
||||||
|
Test building PJLINK_DEFAULT_CODES dictionary
|
||||||
|
"""
|
||||||
|
# GIVEN: Test data
|
||||||
|
from tests.resources.projector.data import TEST_VIDEO_CODES
|
||||||
|
|
||||||
|
# WHEN: Import projector PJLINK_DEFAULT_CODES
|
||||||
|
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
|
||||||
|
|
||||||
|
# THEN: Verify dictionary was build correctly
|
||||||
|
self.assertEquals(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match')
|
|
@ -23,14 +23,15 @@
|
||||||
Package to test the openlp.core.lib.projector.pjlink1 package.
|
Package to test the openlp.core.lib.projector.pjlink1 package.
|
||||||
"""
|
"""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import call, patch, MagicMock
|
||||||
|
|
||||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
from openlp.core.lib.projector.pjlink1 import PJLink
|
||||||
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, PJLINK_POWR_STATUS
|
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, \
|
||||||
|
PJLINK_POWR_STATUS, S_CONNECTED
|
||||||
|
|
||||||
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH
|
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH
|
||||||
|
|
||||||
pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
|
pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)
|
||||||
|
|
||||||
|
|
||||||
class TestPJLink(TestCase):
|
class TestPJLink(TestCase):
|
||||||
|
@ -163,7 +164,13 @@ class TestPJLink(TestCase):
|
||||||
'Lamp 3 hours should have been set to 33333')
|
'Lamp 3 hours should have been set to 33333')
|
||||||
|
|
||||||
@patch.object(pjlink_test, 'projectorReceivedData')
|
@patch.object(pjlink_test, 'projectorReceivedData')
|
||||||
def test_projector_process_power_on(self, mock_projectorReceivedData):
|
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||||
|
@patch.object(pjlink_test, 'send_command')
|
||||||
|
@patch.object(pjlink_test, 'change_status')
|
||||||
|
def test_projector_process_power_on(self, mock_change_status,
|
||||||
|
mock_send_command,
|
||||||
|
mock_UpdateIcons,
|
||||||
|
mock_ReceivedData):
|
||||||
"""
|
"""
|
||||||
Test status power to ON
|
Test status power to ON
|
||||||
"""
|
"""
|
||||||
|
@ -176,9 +183,17 @@ class TestPJLink(TestCase):
|
||||||
|
|
||||||
# THEN: Power should be set to ON
|
# THEN: Power should be set to ON
|
||||||
self.assertEquals(pjlink.power, S_ON, 'Power should have been set to ON')
|
self.assertEquals(pjlink.power, S_ON, 'Power should have been set to ON')
|
||||||
|
mock_send_command.assert_called_once_with('INST')
|
||||||
|
self.assertEquals(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
|
||||||
|
|
||||||
@patch.object(pjlink_test, 'projectorReceivedData')
|
@patch.object(pjlink_test, 'projectorReceivedData')
|
||||||
def test_projector_process_power_off(self, mock_projectorReceivedData):
|
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||||
|
@patch.object(pjlink_test, 'send_command')
|
||||||
|
@patch.object(pjlink_test, 'change_status')
|
||||||
|
def test_projector_process_power_off(self, mock_change_status,
|
||||||
|
mock_send_command,
|
||||||
|
mock_UpdateIcons,
|
||||||
|
mock_ReceivedData):
|
||||||
"""
|
"""
|
||||||
Test status power to STANDBY
|
Test status power to STANDBY
|
||||||
"""
|
"""
|
||||||
|
@ -191,6 +206,8 @@ class TestPJLink(TestCase):
|
||||||
|
|
||||||
# THEN: Power should be set to STANDBY
|
# THEN: Power should be set to STANDBY
|
||||||
self.assertEquals(pjlink.power, S_STANDBY, 'Power should have been set to STANDBY')
|
self.assertEquals(pjlink.power, S_STANDBY, 'Power should have been set to STANDBY')
|
||||||
|
self.assertEquals(mock_send_command.called, False, 'send_command should not have been called')
|
||||||
|
self.assertEquals(mock_UpdateIcons.emit.called, True, 'projectorUpdateIcons should have been called')
|
||||||
|
|
||||||
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
@patch.object(pjlink_test, 'projectorUpdateIcons')
|
||||||
def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
|
def test_projector_process_avmt_closed_unmuted(self, mock_projectorReceivedData):
|
||||||
|
@ -366,3 +383,95 @@ class TestPJLink(TestCase):
|
||||||
# THEN: send_command should have the proper authentication
|
# THEN: send_command should have the proper authentication
|
||||||
self.assertEquals("{test}".format(test=mock_send_command.call_args),
|
self.assertEquals("{test}".format(test=mock_send_command.call_args),
|
||||||
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
|
"call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH))
|
||||||
|
|
||||||
|
@patch.object(pjlink_test, '_not_implemented')
|
||||||
|
def not_implemented_test(self, mock_not_implemented):
|
||||||
|
"""
|
||||||
|
Test PJLink._not_implemented method being called
|
||||||
|
"""
|
||||||
|
# GIVEN: test object
|
||||||
|
pjlink = pjlink_test
|
||||||
|
test_cmd = 'TESTMEONLY'
|
||||||
|
|
||||||
|
# WHEN: A future command is called that is not implemented yet
|
||||||
|
pjlink.process_command(test_cmd, "Garbage data for test only")
|
||||||
|
|
||||||
|
# THEN: PJLink.__not_implemented should have been called with test_cmd
|
||||||
|
mock_not_implemented.assert_called_with(test_cmd)
|
||||||
|
|
||||||
|
@patch.object(pjlink_test, 'disconnect_from_host')
|
||||||
|
def socket_abort_test(self, mock_disconnect):
|
||||||
|
"""
|
||||||
|
Test PJLink.socket_abort calls disconnect_from_host
|
||||||
|
"""
|
||||||
|
# GIVEN: Test object
|
||||||
|
pjlink = pjlink_test
|
||||||
|
|
||||||
|
# WHEN: Calling socket_abort
|
||||||
|
pjlink.socket_abort()
|
||||||
|
|
||||||
|
# THEN: disconnect_from_host should be called
|
||||||
|
self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host')
|
||||||
|
|
||||||
|
def poll_loop_not_connected_test(self):
|
||||||
|
"""
|
||||||
|
Test PJLink.poll_loop not connected return
|
||||||
|
"""
|
||||||
|
# GIVEN: Test object and mocks
|
||||||
|
pjlink = pjlink_test
|
||||||
|
pjlink.state = MagicMock()
|
||||||
|
pjlink.timer = MagicMock()
|
||||||
|
pjlink.state.return_value = False
|
||||||
|
pjlink.ConnectedState = True
|
||||||
|
|
||||||
|
# WHEN: PJLink.poll_loop called
|
||||||
|
pjlink.poll_loop()
|
||||||
|
|
||||||
|
# THEN: poll_loop should exit without calling any other method
|
||||||
|
self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method')
|
||||||
|
|
||||||
|
@patch.object(pjlink_test, 'send_command')
|
||||||
|
def poll_loop_start_test(self, mock_send_command):
|
||||||
|
"""
|
||||||
|
Test PJLink.poll_loop makes correct calls
|
||||||
|
"""
|
||||||
|
# GIVEN: test object and test data
|
||||||
|
pjlink = pjlink_test
|
||||||
|
pjlink.state = MagicMock()
|
||||||
|
pjlink.timer = MagicMock()
|
||||||
|
pjlink.timer.interval = MagicMock()
|
||||||
|
pjlink.timer.setInterval = MagicMock()
|
||||||
|
pjlink.timer.start = MagicMock()
|
||||||
|
pjlink.poll_time = 20
|
||||||
|
pjlink.power = S_ON
|
||||||
|
pjlink.source_available = None
|
||||||
|
pjlink.other_info = None
|
||||||
|
pjlink.manufacturer = None
|
||||||
|
pjlink.model = None
|
||||||
|
pjlink.pjlink_name = None
|
||||||
|
pjlink.ConnectedState = S_CONNECTED
|
||||||
|
pjlink.timer.interval.return_value = 10
|
||||||
|
pjlink.state.return_value = S_CONNECTED
|
||||||
|
call_list = [
|
||||||
|
call('POWR', queue=True),
|
||||||
|
call('ERST', queue=True),
|
||||||
|
call('LAMP', queue=True),
|
||||||
|
call('AVMT', queue=True),
|
||||||
|
call('INPT', queue=True),
|
||||||
|
call('INST', queue=True),
|
||||||
|
call('INFO', queue=True),
|
||||||
|
call('INF1', queue=True),
|
||||||
|
call('INF2', queue=True),
|
||||||
|
call('NAME', queue=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
# WHEN: PJLink.poll_loop is called
|
||||||
|
pjlink.poll_loop()
|
||||||
|
|
||||||
|
# THEN: proper calls were made to retrieve projector data
|
||||||
|
# First, call to update the timer with the next interval
|
||||||
|
self.assertTrue(pjlink.timer.setInterval.called, 'Should have updated the timer')
|
||||||
|
# Next, should have called the timer to start
|
||||||
|
self.assertTrue(pjlink.timer.start.called, 'Should have started the timer')
|
||||||
|
# Finally, should have called send_command with a list of projetctor status checks
|
||||||
|
mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries')
|
||||||
|
|
|
@ -26,13 +26,15 @@ record functions.
|
||||||
PREREQUISITE: add_record() and get_all() functions validated.
|
PREREQUISITE: add_record() and get_all() functions validated.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
|
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
|
||||||
from openlp.core.lib.projector.constants import PJLINK_PORT
|
from openlp.core.lib.projector.constants import PJLINK_PORT
|
||||||
|
|
||||||
from tests.resources.projector.data import TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
|
from tests.resources.projector.data import TEST_DB_PJLINK1, TEST_DB, TEST1_DATA, TEST2_DATA, TEST3_DATA
|
||||||
|
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||||
|
|
||||||
|
|
||||||
def compare_data(one, two):
|
def compare_data(one, two):
|
||||||
|
@ -45,7 +47,11 @@ def compare_data(one, two):
|
||||||
one.port == two.port and \
|
one.port == two.port and \
|
||||||
one.name == two.name and \
|
one.name == two.name and \
|
||||||
one.location == two.location and \
|
one.location == two.location and \
|
||||||
one.notes == two.notes
|
one.notes == two.notes and \
|
||||||
|
one.sw_version == two.sw_version and \
|
||||||
|
one.serial_no == two.serial_no and \
|
||||||
|
one.model_filter == two.model_filter and \
|
||||||
|
one.model_lamp == two.model_lamp
|
||||||
|
|
||||||
|
|
||||||
def compare_source(one, two):
|
def compare_source(one, two):
|
||||||
|
@ -168,6 +174,10 @@ class TestProjectorDB(TestCase):
|
||||||
record.name = TEST3_DATA['name']
|
record.name = TEST3_DATA['name']
|
||||||
record.location = TEST3_DATA['location']
|
record.location = TEST3_DATA['location']
|
||||||
record.notes = TEST3_DATA['notes']
|
record.notes = TEST3_DATA['notes']
|
||||||
|
record.sw_version = TEST3_DATA['sw_version']
|
||||||
|
record.serial_no = TEST3_DATA['serial_no']
|
||||||
|
record.model_filter = TEST3_DATA['model_filter']
|
||||||
|
record.model_lamp = TEST3_DATA['model_lamp']
|
||||||
updated = self.projector.update_projector(record)
|
updated = self.projector.update_projector(record)
|
||||||
self.assertTrue(updated, 'Save updated record should have returned True')
|
self.assertTrue(updated, 'Save updated record should have returned True')
|
||||||
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
|
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
|
||||||
|
@ -246,7 +256,8 @@ class TestProjectorDB(TestCase):
|
||||||
projector = Projector()
|
projector = Projector()
|
||||||
|
|
||||||
# WHEN: projector() is populated
|
# WHEN: projector() is populated
|
||||||
# NOTE: projector.pin, projector.other, projector.sources should all return None
|
# NOTE: projector.[pin, other, sources, sw_version, serial_no, sw_version, model_lamp, model_filter]
|
||||||
|
# should all return None.
|
||||||
# projector.source_list should return an empty list
|
# projector.source_list should return an empty list
|
||||||
projector.id = 0
|
projector.id = 0
|
||||||
projector.ip = '127.0.0.1'
|
projector.ip = '127.0.0.1'
|
||||||
|
@ -262,8 +273,9 @@ class TestProjectorDB(TestCase):
|
||||||
self.assertEqual(str(projector),
|
self.assertEqual(str(projector),
|
||||||
'< Projector(id="0", ip="127.0.0.1", port="4352", pin="None", name="Test One", '
|
'< Projector(id="0", ip="127.0.0.1", port="4352", pin="None", name="Test One", '
|
||||||
'location="Somewhere over the rainbow", notes="Not again", pjlink_name="TEST", '
|
'location="Somewhere over the rainbow", notes="Not again", pjlink_name="TEST", '
|
||||||
'manufacturer="IN YOUR DREAMS", model="OpenLP", other="None", sources="None", '
|
'manufacturer="IN YOUR DREAMS", model="OpenLP", serial_no="None", other="None", '
|
||||||
'source_list="[]") >',
|
'sources="None", source_list="[]", model_filter="None", model_lamp="None", '
|
||||||
|
'sw_version="None") >',
|
||||||
'Projector.__repr__() should have returned a proper representation string')
|
'Projector.__repr__() should have returned a proper representation string')
|
||||||
|
|
||||||
def test_projectorsource_repr(self):
|
def test_projectorsource_repr(self):
|
||||||
|
|
|
@ -27,7 +27,7 @@ from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from openlp.core.common import Registry, md5_hash
|
from openlp.core.common import Registry, md5_hash
|
||||||
from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType
|
from openlp.core.lib import ItemCapabilities, ServiceItem, ServiceItemType, FormattingTags
|
||||||
|
|
||||||
from tests.utils import assert_length, convert_file_service_item
|
from tests.utils import assert_length, convert_file_service_item
|
||||||
|
|
||||||
|
@ -38,6 +38,23 @@ VERSE = 'The Lord said to {r}Noah{/r}: \n'\
|
||||||
'Get those children out of the muddy, muddy \n'\
|
'Get those children out of the muddy, muddy \n'\
|
||||||
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
|
'{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}'\
|
||||||
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
|
||||||
|
CLEANED_VERSE = 'The Lord said to Noah: \n'\
|
||||||
|
'There\'s gonna be a floody, floody\n'\
|
||||||
|
'The Lord said to Noah:\n'\
|
||||||
|
'There\'s gonna be a floody, floody\n'\
|
||||||
|
'Get those children out of the muddy, muddy \n'\
|
||||||
|
'Children of the Lord\n'
|
||||||
|
RENDERED_VERSE = 'The Lord said to <span style="-webkit-text-fill-color:red">Noah</span>: \n'\
|
||||||
|
'There's gonna be a <sup>floody</sup>, <sub>floody</sub>\n'\
|
||||||
|
'The Lord said to <span style="-webkit-text-fill-color:green">Noah</span>:\n'\
|
||||||
|
'There's gonna be a <strong>floody</strong>, <em>floody</em>\n'\
|
||||||
|
'Get those children out of the muddy, muddy \n'\
|
||||||
|
'<span style="-webkit-text-fill-color:red">C</span><span style="-webkit-text-fill-color:black">h' \
|
||||||
|
'</span><span style="-webkit-text-fill-color:blue">i</span>'\
|
||||||
|
'<span style="-webkit-text-fill-color:yellow">l</span><span style="-webkit-text-fill-color:green">d'\
|
||||||
|
'</span><span style="-webkit-text-fill-color:#FFC0CB">r</span>'\
|
||||||
|
'<span style="-webkit-text-fill-color:#FFA500">e</span><span style="-webkit-text-fill-color:#800080">'\
|
||||||
|
'n</span> of the Lord\n'
|
||||||
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
|
FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'service'))
|
||||||
|
|
||||||
|
@ -74,6 +91,7 @@ class TestServiceItem(TestCase):
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
service_item.add_icon = MagicMock()
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
# WHEN: We add a custom from a saved service
|
# WHEN: We add a custom from a saved service
|
||||||
line = convert_file_service_item(TEST_PATH, 'serviceitem_custom_1.osj')
|
line = convert_file_service_item(TEST_PATH, 'serviceitem_custom_1.osj')
|
||||||
|
@ -89,9 +107,9 @@ class TestServiceItem(TestCase):
|
||||||
|
|
||||||
# THEN: The frames should also be valid
|
# THEN: The frames should also be valid
|
||||||
self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"')
|
self.assertEqual('Test Custom', service_item.get_display_title(), 'The title should be "Test Custom"')
|
||||||
self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'],
|
self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'],
|
||||||
'The returned text matches the input, except the last line feed')
|
'The returned text matches the input, except the last line feed')
|
||||||
self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
||||||
'The first line has been returned')
|
'The first line has been returned')
|
||||||
self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title')
|
self.assertEqual('Slide 1', service_item.get_frame_title(0), '"Slide 1" has been returned as the title')
|
||||||
self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title')
|
self.assertEqual('Slide 2', service_item.get_frame_title(1), '"Slide 2" has been returned as the title')
|
||||||
|
@ -300,6 +318,7 @@ class TestServiceItem(TestCase):
|
||||||
# GIVEN: A new service item and a mocked add icon function
|
# GIVEN: A new service item and a mocked add icon function
|
||||||
service_item = ServiceItem(None)
|
service_item = ServiceItem(None)
|
||||||
service_item.add_icon = MagicMock()
|
service_item.add_icon = MagicMock()
|
||||||
|
FormattingTags.load_tags()
|
||||||
|
|
||||||
# WHEN: We add a custom from a saved service
|
# WHEN: We add a custom from a saved service
|
||||||
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
||||||
|
@ -315,9 +334,9 @@ class TestServiceItem(TestCase):
|
||||||
|
|
||||||
# THEN: The frames should also be valid
|
# THEN: The frames should also be valid
|
||||||
self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"')
|
self.assertEqual('Amazing Grace', service_item.get_display_title(), 'The title should be "Amazing Grace"')
|
||||||
self.assertEqual(VERSE[:-1], service_item.get_frames()[0]['text'],
|
self.assertEqual(CLEANED_VERSE[:-1], service_item.get_frames()[0]['text'],
|
||||||
'The returned text matches the input, except the last line feed')
|
'The returned text matches the input, except the last line feed')
|
||||||
self.assertEqual(VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
self.assertEqual(RENDERED_VERSE.split('\n', 1)[0], service_item.get_rendered_frame(1),
|
||||||
'The first line has been returned')
|
'The first line has been returned')
|
||||||
self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0),
|
self.assertEqual('Amazing Grace! how sweet the s', service_item.get_frame_title(0),
|
||||||
'"Amazing Grace! how sweet the s" has been returned as the title')
|
'"Amazing Grace! how sweet the s" has been returned as the title')
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; 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 tests.helpers.songfileimport import SongImportTestHelper
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
TEST_PATH = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'chordprosongs'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestChordProFileImport(SongImportTestHelper):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.importer_class_name = 'ChordProImport'
|
||||||
|
self.importer_module_name = 'chordpro'
|
||||||
|
super(TestChordProFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@patch('openlp.plugins.songs.lib.importers.chordpro.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
|
"""
|
||||||
|
Test that loading an ChordPro file works correctly on various files
|
||||||
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
# Do the test import
|
||||||
|
self.file_import([os.path.join(TEST_PATH, 'swing-low.chordpro')],
|
||||||
|
self.load_external_result_data(os.path.join(TEST_PATH, 'swing-low.json')))
|
|
@ -48,7 +48,8 @@ class TestDB(TestCase):
|
||||||
"""
|
"""
|
||||||
Clean up after tests
|
Clean up after tests
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(self.tmp_folder)
|
# Ignore errors since windows can have problems with locked files
|
||||||
|
shutil.rmtree(self.tmp_folder, ignore_errors=True)
|
||||||
|
|
||||||
def test_add_author(self):
|
def test_add_author(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -114,6 +114,7 @@ class TestFieldDesc:
|
||||||
self.field_type = field_type
|
self.field_type = field_type
|
||||||
self.size = size
|
self.size = size
|
||||||
|
|
||||||
|
|
||||||
TEST_DATA_ENCODING = 'cp1252'
|
TEST_DATA_ENCODING = 'cp1252'
|
||||||
CODE_PAGE_MAPPINGS = [
|
CODE_PAGE_MAPPINGS = [
|
||||||
(852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'),
|
(852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'),
|
||||||
|
|
|
@ -25,7 +25,7 @@ This module contains tests for the lib submodule of the Songs plugin.
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch, MagicMock, PropertyMock
|
from unittest.mock import patch, MagicMock, PropertyMock
|
||||||
|
|
||||||
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf
|
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title, strip_rtf, transpose_chord, transpose_lyrics
|
||||||
from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
|
from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
|
||||||
|
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ class TestLib(TestCase):
|
||||||
assert result[0][3] == 0, 'The start indices should be kept.'
|
assert result[0][3] == 0, 'The start indices should be kept.'
|
||||||
assert result[0][4] == 21, 'The stop indices should be kept.'
|
assert result[0][4] == 21, 'The stop indices should be kept.'
|
||||||
|
|
||||||
def test_remove_typos_beginning_negated(self):
|
def test_remove_typos_middle_negated(self):
|
||||||
"""
|
"""
|
||||||
Test the _remove_typos function with a large difference in the middle.
|
Test the _remove_typos function with a large difference in the middle.
|
||||||
"""
|
"""
|
||||||
|
@ -264,6 +264,85 @@ class TestLib(TestCase):
|
||||||
# THEN: The stripped text matches thed expected result
|
# THEN: The stripped text matches thed expected result
|
||||||
assert result == exp_result, 'The result should be %s' % exp_result
|
assert result == exp_result, 'The result should be %s' % exp_result
|
||||||
|
|
||||||
|
def test_transpose_chord_up(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() method works when transposing up
|
||||||
|
"""
|
||||||
|
# GIVEN: A Chord
|
||||||
|
chord = 'C'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 up
|
||||||
|
new_chord = transpose_chord(chord, 1, 'english')
|
||||||
|
|
||||||
|
# THEN: The chord should be transposed up one note
|
||||||
|
self.assertEqual(new_chord, 'C#', 'The chord should be transposed up.')
|
||||||
|
|
||||||
|
def test_transpose_chord_up_adv(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() method works when transposing up an advanced chord
|
||||||
|
"""
|
||||||
|
# GIVEN: An advanced Chord
|
||||||
|
chord = '(C/D#)'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 up
|
||||||
|
new_chord = transpose_chord(chord, 1, 'english')
|
||||||
|
|
||||||
|
# THEN: The chord should be transposed up one note
|
||||||
|
self.assertEqual(new_chord, '(C#/E)', 'The chord should be transposed up.')
|
||||||
|
|
||||||
|
def test_transpose_chord_down(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() method works when transposing down
|
||||||
|
"""
|
||||||
|
# GIVEN: A Chord
|
||||||
|
chord = 'C'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 down
|
||||||
|
new_chord = transpose_chord(chord, -1, 'english')
|
||||||
|
|
||||||
|
# THEN: The chord should be transposed down one note
|
||||||
|
self.assertEqual(new_chord, 'B', 'The chord should be transposed down.')
|
||||||
|
|
||||||
|
def test_transpose_chord_error(self):
|
||||||
|
"""
|
||||||
|
Test that the transpose_chord() raises exception on invalid chord
|
||||||
|
"""
|
||||||
|
# GIVEN: A invalid Chord
|
||||||
|
chord = 'T'
|
||||||
|
|
||||||
|
# WHEN: Transposing it 1 down
|
||||||
|
# THEN: An exception should be raised
|
||||||
|
with self.assertRaises(ValueError) as err:
|
||||||
|
new_chord = transpose_chord(chord, -1, 'english')
|
||||||
|
self.assertEqual(err.exception.args[0], '\'T\' is not in list',
|
||||||
|
'ValueError exception should have been thrown for invalid chord')
|
||||||
|
|
||||||
|
@patch('openlp.plugins.songs.lib.transpose_verse')
|
||||||
|
@patch('openlp.plugins.songs.lib.Settings')
|
||||||
|
def test_transpose_lyrics(self, mocked_settings, mocked_transpose_verse):
|
||||||
|
"""
|
||||||
|
Test that the transpose_lyrics() splits verses correctly
|
||||||
|
"""
|
||||||
|
# GIVEN: Lyrics with verse splitters and a mocked settings
|
||||||
|
lyrics = '---[Verse:1]---\n'\
|
||||||
|
'Amazing grace how sweet the sound\n'\
|
||||||
|
'[---]\n'\
|
||||||
|
'That saved a wretch like me.\n'\
|
||||||
|
'---[Verse:2]---\n'\
|
||||||
|
'I once was lost but now I\'m found.'
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.return_value = 'english'
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
|
||||||
|
# WHEN: Transposing the lyrics
|
||||||
|
transpose_lyrics(lyrics, 1)
|
||||||
|
|
||||||
|
# THEN: transpose_verse should have been called
|
||||||
|
mocked_transpose_verse.assert_any_call('', 1, 'english')
|
||||||
|
mocked_transpose_verse.assert_any_call('\nAmazing grace how sweet the sound\n', 1, 'english')
|
||||||
|
mocked_transpose_verse.assert_any_call('\nThat saved a wretch like me.\n', 1, 'english')
|
||||||
|
mocked_transpose_verse.assert_any_call('\nI once was lost but now I\'m found.', 1, 'english')
|
||||||
|
|
||||||
|
|
||||||
class TestVerseType(TestCase):
|
class TestVerseType(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -42,10 +42,16 @@ class TestOpenSongFileImport(SongImportTestHelper):
|
||||||
self.importer_module_name = 'opensong'
|
self.importer_module_name = 'opensong'
|
||||||
super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
|
super(TestOpenSongFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
@patch('openlp.plugins.songs.lib.importers.opensong.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
"""
|
"""
|
||||||
Test that loading an OpenSong file works correctly on various files
|
Test that loading an OpenSong file works correctly on various files
|
||||||
"""
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
# Do the test import
|
||||||
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace')],
|
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace')],
|
||||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
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.file_import([os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer')],
|
||||||
|
|
|
@ -42,12 +42,21 @@ class TestSongBeamerFileImport(SongImportTestHelper):
|
||||||
self.importer_module_name = 'songbeamer'
|
self.importer_module_name = 'songbeamer'
|
||||||
super(TestSongBeamerFileImport, self).__init__(*args, **kwargs)
|
super(TestSongBeamerFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
@patch('openlp.plugins.songs.lib.importers.songbeamer.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
"""
|
"""
|
||||||
Test that loading an OpenSong file works correctly on various files
|
Test that loading an SongBeamer file works correctly on various files
|
||||||
"""
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.sng')],
|
||||||
|
self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||||
self.file_import([os.path.join(TEST_PATH, 'Lobsinget dem Herrn.sng')],
|
self.file_import([os.path.join(TEST_PATH, 'Lobsinget dem Herrn.sng')],
|
||||||
self.load_external_result_data(os.path.join(TEST_PATH, 'Lobsinget dem Herrn.json')))
|
self.load_external_result_data(os.path.join(TEST_PATH, 'Lobsinget dem Herrn.json')))
|
||||||
|
self.file_import([os.path.join(TEST_PATH, 'When I Call On You.sng')],
|
||||||
|
self.load_external_result_data(os.path.join(TEST_PATH, 'When I Call On You.json')))
|
||||||
|
|
||||||
def test_cp1252_encoded_file(self):
|
def test_cp1252_encoded_file(self):
|
||||||
"""
|
"""
|
||||||
|
@ -66,6 +75,16 @@ class TestSongBeamerImport(TestCase):
|
||||||
Create the registry
|
Create the registry
|
||||||
"""
|
"""
|
||||||
Registry.create()
|
Registry.create()
|
||||||
|
self.song_import_patcher = patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport')
|
||||||
|
self.song_import_patcher.start()
|
||||||
|
mocked_manager = MagicMock()
|
||||||
|
self.importer = SongBeamerImport(mocked_manager, filenames=[])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""
|
||||||
|
Clean up
|
||||||
|
"""
|
||||||
|
self.song_import_patcher.stop()
|
||||||
|
|
||||||
def test_create_importer(self):
|
def test_create_importer(self):
|
||||||
"""
|
"""
|
||||||
|
@ -85,20 +104,18 @@ class TestSongBeamerImport(TestCase):
|
||||||
"""
|
"""
|
||||||
Test SongBeamerImport.do_import handles different invalid import_source values
|
Test SongBeamerImport.do_import handles different invalid import_source values
|
||||||
"""
|
"""
|
||||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
# GIVEN: A mocked out import wizard
|
||||||
with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
|
|
||||||
mocked_manager = MagicMock()
|
|
||||||
mocked_import_wizard = MagicMock()
|
mocked_import_wizard = MagicMock()
|
||||||
importer = SongBeamerImport(mocked_manager, filenames=[])
|
self.importer.import_wizard = mocked_import_wizard
|
||||||
importer.import_wizard = mocked_import_wizard
|
self.importer.stop_import_flag = True
|
||||||
importer.stop_import_flag = True
|
|
||||||
|
|
||||||
# WHEN: Import source is not a list
|
# WHEN: Import source is not a list
|
||||||
for source in ['not a list', 0]:
|
for source in ['not a list', 0]:
|
||||||
importer.import_source = source
|
self.importer.import_source = source
|
||||||
|
|
||||||
# THEN: do_import should return none and the progress bar maximum should not be set.
|
# 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.assertIsNone(self.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,
|
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
||||||
'setMaxium on import_wizard.progress_bar should not have been called')
|
'setMaxium on import_wizard.progress_bar should not have been called')
|
||||||
|
|
||||||
|
@ -106,22 +123,19 @@ class TestSongBeamerImport(TestCase):
|
||||||
"""
|
"""
|
||||||
Test SongBeamerImport.do_import handles different invalid import_source values
|
Test SongBeamerImport.do_import handles different invalid import_source values
|
||||||
"""
|
"""
|
||||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
# GIVEN: A mocked out import wizard
|
||||||
with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
|
|
||||||
mocked_manager = MagicMock()
|
|
||||||
mocked_import_wizard = MagicMock()
|
mocked_import_wizard = MagicMock()
|
||||||
importer = SongBeamerImport(mocked_manager, filenames=[])
|
self.importer.import_wizard = mocked_import_wizard
|
||||||
importer.import_wizard = mocked_import_wizard
|
self.importer.stop_import_flag = True
|
||||||
importer.stop_import_flag = True
|
|
||||||
|
|
||||||
# WHEN: Import source is a list
|
# WHEN: Import source is a list
|
||||||
importer.import_source = ['List', 'of', 'files']
|
self.importer.import_source = ['List', 'of', 'files']
|
||||||
|
|
||||||
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
|
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
|
||||||
# import_source.
|
# import_source.
|
||||||
self.assertIsNone(importer.do_import(),
|
self.assertIsNone(self.importer.do_import(),
|
||||||
'do_import should return None when import_source is a list and stop_import_flag is True')
|
'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))
|
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(self.importer.import_source))
|
||||||
|
|
||||||
def test_check_verse_marks(self):
|
def test_check_verse_marks(self):
|
||||||
"""
|
"""
|
||||||
|
@ -130,75 +144,76 @@ class TestSongBeamerImport(TestCase):
|
||||||
|
|
||||||
# GIVEN: line with unnumbered verse-type
|
# GIVEN: line with unnumbered verse-type
|
||||||
line = 'Refrain'
|
line = 'Refrain'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and c as self.current_verse_type
|
# THEN: we should get back true and c as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <Refrain> should be found, value true')
|
self.assertTrue(result, 'Versemark for <Refrain> should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
|
self.assertEqual(self.importer.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
|
||||||
|
|
||||||
# GIVEN: line with unnumbered verse-type and trailing space
|
# GIVEN: line with unnumbered verse-type and trailing space
|
||||||
line = 'ReFrain '
|
line = 'ReFrain '
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and c as self.current_verse_type
|
# THEN: we should get back true and c as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
|
self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
|
self.assertEqual(self.importer.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
|
||||||
|
|
||||||
# GIVEN: line with numbered verse-type
|
# GIVEN: line with numbered verse-type
|
||||||
line = 'VersE 1'
|
line = 'VersE 1'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and v1 as self.current_verse_type
|
# THEN: we should get back true and v1 as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
|
self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
|
self.assertEqual(self.importer.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
|
||||||
|
|
||||||
# GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
|
# GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
|
||||||
line = '$$M=special'
|
line = '$$M=special'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back true and o as self.current_verse_type
|
# THEN: we should get back true and o as self.importer.current_verse_type
|
||||||
self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true')
|
self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true')
|
||||||
self.assertEqual(self.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
|
self.assertEqual(self.importer.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
|
||||||
|
|
||||||
# GIVEN: line with song-text with 3 words
|
# GIVEN: line with song-text with 3 words
|
||||||
line = 'Jesus my saviour'
|
line = 'Jesus my saviour'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for <Jesus my saviour> should be found, value false')
|
self.assertFalse(result, 'No versemark for <Jesus my saviour> should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '<Jesus my saviour> should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type,
|
||||||
|
'<Jesus my saviour> should be interpreted as none versemark')
|
||||||
|
|
||||||
# GIVEN: line with song-text with 2 words
|
# GIVEN: line with song-text with 2 words
|
||||||
line = 'Praise him'
|
line = 'Praise him'
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for <Praise him> should be found, value false')
|
self.assertFalse(result, 'No versemark for <Praise him> should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '<Praise him> should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type, '<Praise him> should be interpreted as none versemark')
|
||||||
|
|
||||||
# GIVEN: line with only a space (could occur, nothing regular)
|
# GIVEN: line with only a space (could occur, nothing regular)
|
||||||
line = ' '
|
line = ' '
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for < > should be found, value false')
|
self.assertFalse(result, 'No versemark for < > should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '< > should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type, '< > should be interpreted as none versemark')
|
||||||
|
|
||||||
# GIVEN: blank line (could occur, nothing regular)
|
# GIVEN: blank line (could occur, nothing regular)
|
||||||
line = ''
|
line = ''
|
||||||
self.current_verse_type = None
|
self.importer.current_verse_type = None
|
||||||
# WHEN: line is being checked for verse marks
|
# WHEN: line is being checked for verse marks
|
||||||
result = SongBeamerImport.check_verse_marks(self, line)
|
result = self.importer.check_verse_marks(line)
|
||||||
# THEN: we should get back false and none as self.current_verse_type
|
# THEN: we should get back false and none as self.importer.current_verse_type
|
||||||
self.assertFalse(result, 'No versemark for <> should be found, value false')
|
self.assertFalse(result, 'No versemark for <> should be found, value false')
|
||||||
self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark')
|
self.assertIsNone(self.importer.current_verse_type, '<> should be interpreted as none versemark')
|
||||||
|
|
||||||
def test_verse_marks_defined_in_lowercase(self):
|
def test_verse_marks_defined_in_lowercase(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,6 +25,7 @@ This module contains tests for the VideoPsalm song importer.
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from tests.helpers.songfileimport import SongImportTestHelper
|
from tests.helpers.songfileimport import SongImportTestHelper
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(
|
TEST_PATH = os.path.abspath(
|
||||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs'))
|
||||||
|
@ -37,10 +38,16 @@ class TestVideoPsalmFileImport(SongImportTestHelper):
|
||||||
self.importer_module_name = 'videopsalm'
|
self.importer_module_name = 'videopsalm'
|
||||||
super(TestVideoPsalmFileImport, self).__init__(*args, **kwargs)
|
super(TestVideoPsalmFileImport, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def test_song_import(self):
|
@patch('openlp.plugins.songs.lib.importers.videopsalm.Settings')
|
||||||
|
def test_song_import(self, mocked_settings):
|
||||||
"""
|
"""
|
||||||
Test that loading an VideoPsalm file works correctly on various files
|
Test that loading an VideoPsalm file works correctly on various files
|
||||||
"""
|
"""
|
||||||
|
# Mock out the settings - always return False
|
||||||
|
mocked_returned_settings = MagicMock()
|
||||||
|
mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
|
||||||
|
mocked_settings.return_value = mocked_returned_settings
|
||||||
|
# Do the test import
|
||||||
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'),
|
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold.json'),
|
||||||
self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json')))
|
self.load_external_result_data(os.path.join(TEST_PATH, 'as-safe-a-stronghold.json')))
|
||||||
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold2.json'),
|
self.file_import(os.path.join(TEST_PATH, 'videopsalm-as-safe-a-stronghold2.json'),
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
{title:Swing Low Sweet Chariot}
|
||||||
|
{st:Traditional}
|
||||||
|
|
||||||
|
{start_of_chorus}
|
||||||
|
Swing [D]low, sweet [G]chari[D]ot,
|
||||||
|
Comin' for to carry me [A7]home.
|
||||||
|
Swing [D7]low, sweet [G]chari[D]ot,
|
||||||
|
Comin' for to [A7]carry me [D]home.
|
||||||
|
{end_of_chorus}
|
||||||
|
|
||||||
|
I looked over Jordan, and what did I see,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
A band of angels comin' after me,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
|
||||||
|
{c:Chorus}
|
||||||
|
|
||||||
|
If you get there before I do,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
Just tell my friends that I'm a comin' too.
|
||||||
|
Comin' for to carry me home.
|
||||||
|
|
||||||
|
{c:Chorus}
|
||||||
|
|
||||||
|
I'm sometimes up and sometimes down,
|
||||||
|
Comin' for to carry me home.
|
||||||
|
But still my soul feels heavenly bound.
|
||||||
|
Comin' for to carry me home.
|
||||||
|
{c:Chorus}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"title": "Swing Low Sweet Chariot",
|
||||||
|
"alternative_title": "Traditional",
|
||||||
|
"verses": [
|
||||||
|
[
|
||||||
|
"Swing [D]low, sweet [G]chari[D]ot,\nComin' for to carry me [A7]home.\nSwing [D7]low, sweet [G]chari[D]ot,\nComin' for to [A7]carry me [D]home.",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"I looked over Jordan, and what did I see,\n Comin' for to carry me home.\nA band of angels comin' after me,\n Comin' for to carry me home.",
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"If you get there before I do,\n Comin' for to carry me home.\nJust tell my friends that I'm a comin' too.\n Comin' for to carry me home.",
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"I'm sometimes up and sometimes down,\n Comin' for to carry me home.\nBut still my soul feels heavenly bound.\n Comin' for to carry me home.",
|
||||||
|
"v"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
|
@ -19,23 +19,23 @@
|
||||||
"verse_order_list": [],
|
"verse_order_list": [],
|
||||||
"verses": [
|
"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.",
|
"A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
|
||||||
"v1"
|
"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.",
|
"'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
|
||||||
"v2"
|
"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.",
|
"The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
|
||||||
"v3"
|
"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.",
|
"Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
|
||||||
"v4"
|
"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.",
|
"When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
|
||||||
"v5"
|
"v5"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -19,23 +19,23 @@
|
||||||
"verse_order_list": [],
|
"verse_order_list": [],
|
||||||
"verses": [
|
"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.",
|
"A[D]mazing [D7]grace! How [G]sweet the [D]sound!\nThat [Bm]saved a [E]wretch like [A]me![A7]\nI [D]once was [D7]lost, but [G]now am [D]found;\nWas [Bm]blind, but [A]now I [G]see.[D]",
|
||||||
"v1"
|
"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.",
|
"'Twas [D]grace that [D7]taught my [G]heart to [D]fear,\nAnd [Bm]grace my [E]fears re[A]lieved.[A7]\nHow [D]precious [D7]did that [G]grace ap[D]pear,\nThe [Bm]hour I [A]first be[G]lieved.[D]",
|
||||||
"v2"
|
"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.",
|
"The [D]Lord has [D7]promised [G]good to [D]me,\nHis [Bm]Word my [E]hope se[A]cures.[A7]\nHe [D]will my [D7]shield and [G]portion [D]be\nAs [Bm]long as [A]life en[G]dures.[D]",
|
||||||
"v3"
|
"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.",
|
"Thro' [D]many [D7]dangers, [G]toils and [D]snares\nI [Bm]have al[E]ready [A]come.[A7]\n'Tis [D]grace that [D7]brought me [G]safe thus [D]far,\nAnd [Bm]grace will [A]lead me [G]home.[D]",
|
||||||
"v4"
|
"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.",
|
"When [D]we've been [D7]there ten [G]thousand [D]years,\nBright [Bm]shining [E]as the [A]sun,[A7]\nWe've [D]no less [D7]days to [G]sing God's [D]praise,\nThan [Bm]when we [A]first be[G]gun.[D]",
|
||||||
"v5"
|
"v5"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -27,6 +27,8 @@ import os
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
|
|
||||||
# Test data
|
# Test data
|
||||||
|
TEST_DB_PJLINK1 = 'projector_pjlink1.sqlite'
|
||||||
|
|
||||||
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
|
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
|
||||||
|
|
||||||
TEST_SALT = '498e4a67'
|
TEST_SALT = '498e4a67'
|
||||||
|
@ -44,18 +46,243 @@ TEST1_DATA = dict(ip='111.111.111.111',
|
||||||
pin='1111',
|
pin='1111',
|
||||||
name='___TEST_ONE___',
|
name='___TEST_ONE___',
|
||||||
location='location one',
|
location='location one',
|
||||||
notes='notes one')
|
notes='notes one',
|
||||||
|
serial_no='Serial Number 1',
|
||||||
|
sw_version='Version 1',
|
||||||
|
model_filter='Filter type 1',
|
||||||
|
model_lamp='Lamp type 1')
|
||||||
|
|
||||||
TEST2_DATA = dict(ip='222.222.222.222',
|
TEST2_DATA = dict(ip='222.222.222.222',
|
||||||
port='2222',
|
port='2222',
|
||||||
pin='2222',
|
pin='2222',
|
||||||
name='___TEST_TWO___',
|
name='___TEST_TWO___',
|
||||||
location='location two',
|
location='location two',
|
||||||
notes='notes two')
|
notes='notes one',
|
||||||
|
serial_no='Serial Number 2',
|
||||||
|
sw_version='Version 2',
|
||||||
|
model_filter='Filter type 2',
|
||||||
|
model_lamp='Lamp type 2')
|
||||||
|
|
||||||
TEST3_DATA = dict(ip='333.333.333.333',
|
TEST3_DATA = dict(ip='333.333.333.333',
|
||||||
port='3333',
|
port='3333',
|
||||||
pin='3333',
|
pin='3333',
|
||||||
name='___TEST_THREE___',
|
name='___TEST_THREE___',
|
||||||
location='location three',
|
location='location three',
|
||||||
notes='notes three')
|
notes='notes one',
|
||||||
|
serial_no='Serial Number 3',
|
||||||
|
sw_version='Version 3',
|
||||||
|
model_filter='Filter type 3',
|
||||||
|
model_lamp='Lamp type 3')
|
||||||
|
|
||||||
|
TEST_VIDEO_CODES = {
|
||||||
|
'11': 'RGB 1',
|
||||||
|
'12': 'RGB 2',
|
||||||
|
'13': 'RGB 3',
|
||||||
|
'14': 'RGB 4',
|
||||||
|
'15': 'RGB 5',
|
||||||
|
'16': 'RGB 6',
|
||||||
|
'17': 'RGB 7',
|
||||||
|
'18': 'RGB 8',
|
||||||
|
'19': 'RGB 9',
|
||||||
|
'1A': 'RGB A',
|
||||||
|
'1B': 'RGB B',
|
||||||
|
'1C': 'RGB C',
|
||||||
|
'1D': 'RGB D',
|
||||||
|
'1E': 'RGB E',
|
||||||
|
'1F': 'RGB F',
|
||||||
|
'1G': 'RGB G',
|
||||||
|
'1H': 'RGB H',
|
||||||
|
'1I': 'RGB I',
|
||||||
|
'1J': 'RGB J',
|
||||||
|
'1K': 'RGB K',
|
||||||
|
'1L': 'RGB L',
|
||||||
|
'1M': 'RGB M',
|
||||||
|
'1N': 'RGB N',
|
||||||
|
'1O': 'RGB O',
|
||||||
|
'1P': 'RGB P',
|
||||||
|
'1Q': 'RGB Q',
|
||||||
|
'1R': 'RGB R',
|
||||||
|
'1S': 'RGB S',
|
||||||
|
'1T': 'RGB T',
|
||||||
|
'1U': 'RGB U',
|
||||||
|
'1V': 'RGB V',
|
||||||
|
'1W': 'RGB W',
|
||||||
|
'1X': 'RGB X',
|
||||||
|
'1Y': 'RGB Y',
|
||||||
|
'1Z': 'RGB Z',
|
||||||
|
'21': 'Video 1',
|
||||||
|
'22': 'Video 2',
|
||||||
|
'23': 'Video 3',
|
||||||
|
'24': 'Video 4',
|
||||||
|
'25': 'Video 5',
|
||||||
|
'26': 'Video 6',
|
||||||
|
'27': 'Video 7',
|
||||||
|
'28': 'Video 8',
|
||||||
|
'29': 'Video 9',
|
||||||
|
'2A': 'Video A',
|
||||||
|
'2B': 'Video B',
|
||||||
|
'2C': 'Video C',
|
||||||
|
'2D': 'Video D',
|
||||||
|
'2E': 'Video E',
|
||||||
|
'2F': 'Video F',
|
||||||
|
'2G': 'Video G',
|
||||||
|
'2H': 'Video H',
|
||||||
|
'2I': 'Video I',
|
||||||
|
'2J': 'Video J',
|
||||||
|
'2K': 'Video K',
|
||||||
|
'2L': 'Video L',
|
||||||
|
'2M': 'Video M',
|
||||||
|
'2N': 'Video N',
|
||||||
|
'2O': 'Video O',
|
||||||
|
'2P': 'Video P',
|
||||||
|
'2Q': 'Video Q',
|
||||||
|
'2R': 'Video R',
|
||||||
|
'2S': 'Video S',
|
||||||
|
'2T': 'Video T',
|
||||||
|
'2U': 'Video U',
|
||||||
|
'2V': 'Video V',
|
||||||
|
'2W': 'Video W',
|
||||||
|
'2X': 'Video X',
|
||||||
|
'2Y': 'Video Y',
|
||||||
|
'2Z': 'Video Z',
|
||||||
|
'31': 'Digital 1',
|
||||||
|
'32': 'Digital 2',
|
||||||
|
'33': 'Digital 3',
|
||||||
|
'34': 'Digital 4',
|
||||||
|
'35': 'Digital 5',
|
||||||
|
'36': 'Digital 6',
|
||||||
|
'37': 'Digital 7',
|
||||||
|
'38': 'Digital 8',
|
||||||
|
'39': 'Digital 9',
|
||||||
|
'3A': 'Digital A',
|
||||||
|
'3B': 'Digital B',
|
||||||
|
'3C': 'Digital C',
|
||||||
|
'3D': 'Digital D',
|
||||||
|
'3E': 'Digital E',
|
||||||
|
'3F': 'Digital F',
|
||||||
|
'3G': 'Digital G',
|
||||||
|
'3H': 'Digital H',
|
||||||
|
'3I': 'Digital I',
|
||||||
|
'3J': 'Digital J',
|
||||||
|
'3K': 'Digital K',
|
||||||
|
'3L': 'Digital L',
|
||||||
|
'3M': 'Digital M',
|
||||||
|
'3N': 'Digital N',
|
||||||
|
'3O': 'Digital O',
|
||||||
|
'3P': 'Digital P',
|
||||||
|
'3Q': 'Digital Q',
|
||||||
|
'3R': 'Digital R',
|
||||||
|
'3S': 'Digital S',
|
||||||
|
'3T': 'Digital T',
|
||||||
|
'3U': 'Digital U',
|
||||||
|
'3V': 'Digital V',
|
||||||
|
'3W': 'Digital W',
|
||||||
|
'3X': 'Digital X',
|
||||||
|
'3Y': 'Digital Y',
|
||||||
|
'3Z': 'Digital Z',
|
||||||
|
'41': 'Storage 1',
|
||||||
|
'42': 'Storage 2',
|
||||||
|
'43': 'Storage 3',
|
||||||
|
'44': 'Storage 4',
|
||||||
|
'45': 'Storage 5',
|
||||||
|
'46': 'Storage 6',
|
||||||
|
'47': 'Storage 7',
|
||||||
|
'48': 'Storage 8',
|
||||||
|
'49': 'Storage 9',
|
||||||
|
'4A': 'Storage A',
|
||||||
|
'4B': 'Storage B',
|
||||||
|
'4C': 'Storage C',
|
||||||
|
'4D': 'Storage D',
|
||||||
|
'4E': 'Storage E',
|
||||||
|
'4F': 'Storage F',
|
||||||
|
'4G': 'Storage G',
|
||||||
|
'4H': 'Storage H',
|
||||||
|
'4I': 'Storage I',
|
||||||
|
'4J': 'Storage J',
|
||||||
|
'4K': 'Storage K',
|
||||||
|
'4L': 'Storage L',
|
||||||
|
'4M': 'Storage M',
|
||||||
|
'4N': 'Storage N',
|
||||||
|
'4O': 'Storage O',
|
||||||
|
'4P': 'Storage P',
|
||||||
|
'4Q': 'Storage Q',
|
||||||
|
'4R': 'Storage R',
|
||||||
|
'4S': 'Storage S',
|
||||||
|
'4T': 'Storage T',
|
||||||
|
'4U': 'Storage U',
|
||||||
|
'4V': 'Storage V',
|
||||||
|
'4W': 'Storage W',
|
||||||
|
'4X': 'Storage X',
|
||||||
|
'4Y': 'Storage Y',
|
||||||
|
'4Z': 'Storage Z',
|
||||||
|
'51': 'Network 1',
|
||||||
|
'52': 'Network 2',
|
||||||
|
'53': 'Network 3',
|
||||||
|
'54': 'Network 4',
|
||||||
|
'55': 'Network 5',
|
||||||
|
'56': 'Network 6',
|
||||||
|
'57': 'Network 7',
|
||||||
|
'58': 'Network 8',
|
||||||
|
'59': 'Network 9',
|
||||||
|
'5A': 'Network A',
|
||||||
|
'5B': 'Network B',
|
||||||
|
'5C': 'Network C',
|
||||||
|
'5D': 'Network D',
|
||||||
|
'5E': 'Network E',
|
||||||
|
'5F': 'Network F',
|
||||||
|
'5G': 'Network G',
|
||||||
|
'5H': 'Network H',
|
||||||
|
'5I': 'Network I',
|
||||||
|
'5J': 'Network J',
|
||||||
|
'5K': 'Network K',
|
||||||
|
'5L': 'Network L',
|
||||||
|
'5M': 'Network M',
|
||||||
|
'5N': 'Network N',
|
||||||
|
'5O': 'Network O',
|
||||||
|
'5P': 'Network P',
|
||||||
|
'5Q': 'Network Q',
|
||||||
|
'5R': 'Network R',
|
||||||
|
'5S': 'Network S',
|
||||||
|
'5T': 'Network T',
|
||||||
|
'5U': 'Network U',
|
||||||
|
'5V': 'Network V',
|
||||||
|
'5W': 'Network W',
|
||||||
|
'5X': 'Network X',
|
||||||
|
'5Y': 'Network Y',
|
||||||
|
'5Z': 'Network Z',
|
||||||
|
'61': 'Internal 1',
|
||||||
|
'62': 'Internal 2',
|
||||||
|
'63': 'Internal 3',
|
||||||
|
'64': 'Internal 4',
|
||||||
|
'65': 'Internal 5',
|
||||||
|
'66': 'Internal 6',
|
||||||
|
'67': 'Internal 7',
|
||||||
|
'68': 'Internal 8',
|
||||||
|
'69': 'Internal 9',
|
||||||
|
'6A': 'Internal A',
|
||||||
|
'6B': 'Internal B',
|
||||||
|
'6C': 'Internal C',
|
||||||
|
'6D': 'Internal D',
|
||||||
|
'6E': 'Internal E',
|
||||||
|
'6F': 'Internal F',
|
||||||
|
'6G': 'Internal G',
|
||||||
|
'6H': 'Internal H',
|
||||||
|
'6I': 'Internal I',
|
||||||
|
'6J': 'Internal J',
|
||||||
|
'6K': 'Internal K',
|
||||||
|
'6L': 'Internal L',
|
||||||
|
'6M': 'Internal M',
|
||||||
|
'6N': 'Internal N',
|
||||||
|
'6O': 'Internal O',
|
||||||
|
'6P': 'Internal P',
|
||||||
|
'6Q': 'Internal Q',
|
||||||
|
'6R': 'Internal R',
|
||||||
|
'6S': 'Internal S',
|
||||||
|
'6T': 'Internal T',
|
||||||
|
'6U': 'Internal U',
|
||||||
|
'6V': 'Internal V',
|
||||||
|
'6W': 'Internal W',
|
||||||
|
'6X': 'Internal X',
|
||||||
|
'6Y': 'Internal Y',
|
||||||
|
'6Z': 'Internal Z'
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
["John Newton", "words"]
|
||||||
|
],
|
||||||
|
"title": "Amazing grace",
|
||||||
|
"verse_order_list": ["v1", "v2", "v3", "v4", "v5"],
|
||||||
|
"verses": [
|
||||||
|
[
|
||||||
|
"[D]Amazing gr[D7]ace how [G]sweet the [D]sound\nThat saved a [E7]wretch like [A7]me\nI [D]once was [D7]lost but [G]now im [D]found\nWas b[E7]lind but no[A7]w i [D]see [A7]\n",
|
||||||
|
"v1"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"T'was [D]grace that [D7]taught my [G]heart to [D]fear\nAnd grace my [E7]fears [A7]relieved\nHow [D]precious [D7]did that [G]Grace [D]appear\nThe [E7]hour I [A7]first bel[D]ieved.[A7]\n",
|
||||||
|
"v2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Through [D]many [D7]dangers, [G]toils and [D]snares\nI have [E7]already [A7]come;\n'Tis [D]Grace that [D7]brought me [G]safe thus [D]far\nand [E7]Grace will [A7]lead me [D]home. [A7]\n",
|
||||||
|
"v3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"When we[D]'ve been here[D7] ten thous[G]and y[D]ears\nBright shining [E7]as the [A7]sun.\nWe've [D]no less [D7]days to s[G]ing God's p[D]raise\nThan w[E7]hen we've [A7]first [D]begun.[A7]\n",
|
||||||
|
"v4"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"[D]Amazing gr[D7]ace how [G]sweet the [D]sound\nThat saved a [E7]wretch like [A7]me\nI [D]once was [D7]lost but [G]now im [D]found\nWas b[E7]lind but no[A7]w i [D]see [A]\n",
|
||||||
|
"v5"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
#LangCount=1
|
||||||
|
#Title=Amazing grace
|
||||||
|
#Chords=MCwwLEQNMTAsMCxENw0xOCwwLEcNMjgsMCxEDTEzLDEsRTcNMjUsMSxBNw0yLDIsRA0xMSwyLEQ3DTIwLDIsRw0yNywyLEQNNSwzLEU3DTE2LDMsQTcNMjAsMyxEDTI0LDMsQTcNNiw1LEQNMTcsNSxENw0yNyw1LEcNMzYsNSxEDTEzLDYsRTcNMTksNixBNw00LDcsRA0xMyw3LEQ3DTIyLDcsRw0yOCw3LEQNNCw4LEU3DTExLDgsQTcNMjAsOCxEDTI2LDgsQTcNOCwxMCxEDTEzLDEwLEQ3DTIyLDEwLEcNMzIsMTAsRA03LDExLEU3DTE1LDExLEE3DTUsMTIsRA0xNiwxMixENw0yNywxMixHDTM3LDEyLEQNNCwxMyxFNw0xNSwxMyxBNw0yMywxMyxEDTI5LDEzLEE3DTcsMTUsRA0yMCwxNSxENw0zMCwxNSxHDTM1LDE1LEQNMTUsMTYsRTcNMjIsMTYsQTcNNiwxNyxEDTE0LDE3LEQ3DTIzLDE3LEcNMzQsMTcsRA02LDE4LEU3DTE2LDE4LEE3DTIyLDE4LEQNMjgsMTgsQTcNMCwyMCxEDTEwLDIwLEQ3DTE4LDIwLEcNMjgsMjAsRA0xMywyMSxFNw0yNSwyMSxBNw0yLDIyLEQNMTEsMjIsRDcNMjAsMjIsRw0yNywyMixEDTUsMjMsRTcNMTYsMjMsQTcNMjAsMjMsRA0yNCwyMyxBDQ==
|
||||||
|
#Author=John Newton
|
||||||
|
#Editor=SongBeamer 4.37a
|
||||||
|
#Version=3
|
||||||
|
#VerseOrder=Verse 1,Verse 2,Verse 3,Verse 4,Verse 5
|
||||||
|
---
|
||||||
|
Verse 1
|
||||||
|
Amazing grace how sweet the sound
|
||||||
|
That saved a wretch like me
|
||||||
|
I once was lost but now im found
|
||||||
|
Was blind but now i see
|
||||||
|
---
|
||||||
|
Verse 2
|
||||||
|
T'was grace that taught my heart to fear
|
||||||
|
And grace my fears relieved
|
||||||
|
How precious did that Grace appear
|
||||||
|
The hour I first believed.
|
||||||
|
---
|
||||||
|
Verse 3
|
||||||
|
Through many dangers, toils and snares
|
||||||
|
I have already come;
|
||||||
|
'Tis Grace that brought me safe thus far
|
||||||
|
and Grace will lead me home.
|
||||||
|
---
|
||||||
|
Verse 4
|
||||||
|
When we've been here ten thousand years
|
||||||
|
Bright shining as the sun.
|
||||||
|
We've no less days to sing God's praise
|
||||||
|
Than when we've first begun.
|
||||||
|
---
|
||||||
|
Verse 5
|
||||||
|
Amazing grace how sweet the sound
|
||||||
|
That saved a wretch like me
|
||||||
|
I once was lost but now im found
|
||||||
|
Was blind but now i see
|
|
@ -8,5 +8,8 @@
|
||||||
],
|
],
|
||||||
"song_book_name": "Glaubenslieder I",
|
"song_book_name": "Glaubenslieder I",
|
||||||
"song_number": "1",
|
"song_number": "1",
|
||||||
"authors": ["Carl Brockhaus", "Johann Jakob Vetter"]
|
"authors": [
|
||||||
|
["Carl Brockhaus", "words"],
|
||||||
|
["Johann Jakob Vetter", "music"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"title": "When I Call On You",
|
||||||
|
"verse_order_list": [],
|
||||||
|
"verses": [
|
||||||
|
[
|
||||||
|
"[G]When I call on Y[Em]ou,\n[G]You are always [D]there,\n[G]When I call on Y[Em]ou.\n[G]You are always [D]there. [D]\n",
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"[G]Oh it [Em]makes me [G]feel like [D]dancing,\n[G]Oh it [Em]makes me [G]feel like [D]dancing,\n[Em]Oh - oh -[D]oh - oh -[G]oh.\n",
|
||||||
|
"v"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
#LangCount=1
|
||||||
|
#Title=When I Call On You
|
||||||
|
#Chords=MCwwLEcNMTUuNSwwLEVtDTAsMSxHDTE1LDEsRA0wLDIsRw0xNS41LDIsRW0NMCwzLEcNMTUsMyxEDTIzLjUsMyxEDTAsNSxHDTYsNSxFbQ0xNSw1LEcNMjUsNSxEDTAsNixHDTYsNixFbQ0xNSw2LEcNMjUsNixEDTAsNyxFbQ05LDcsRA0xOCw3LEcN
|
||||||
|
#Editor=SongBeamer 4.47
|
||||||
|
#Version=3
|
||||||
|
---
|
||||||
|
When I call on You,
|
||||||
|
You are always there,
|
||||||
|
When I call on You.
|
||||||
|
You are always there.
|
||||||
|
---
|
||||||
|
Oh it makes me feel like dancing,
|
||||||
|
Oh it makes me feel like dancing,
|
||||||
|
Oh - oh -oh - oh -oh.
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"title": "Some Song",
|
"title": "Some Song",
|
||||||
"authors": ["Author"],
|
"authors": [
|
||||||
|
["Author", "words"]
|
||||||
|
],
|
||||||
"verses" : [
|
"verses" : [
|
||||||
["Here are a couple of \"weird\" chars’’’.\n", "v"],
|
["Here are a couple of \"weird\" chars’’’.\n", "v"],
|
||||||
["Here is another one….\n\n", "v"]
|
["Here is another one….\n\n", "v"]
|
||||||
|
|
Loading…
Reference in New Issue