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)
|
||||
# If data_version is different from the current version ask if we should backup the data folder
|
||||
elif data_version != openlp_version:
|
||||
if self.splash.isVisible():
|
||||
if can_show_splash and self.splash.isVisible():
|
||||
self.splash.hide()
|
||||
if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
|
||||
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.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -32,6 +32,7 @@ import sys
|
||||
import traceback
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
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')
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
return True
|
||||
|
||||
|
||||
__all__ = ['get_web_page']
|
||||
|
@ -23,10 +23,11 @@
|
||||
The :mod:`lib` module contains most of the components and libraries that make
|
||||
OpenLP work.
|
||||
"""
|
||||
|
||||
import html
|
||||
import logging
|
||||
import os
|
||||
from distutils.version import LooseVersion
|
||||
import re
|
||||
import math
|
||||
|
||||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||
|
||||
@ -34,6 +35,8 @@ from openlp.core.common import translate
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
SLIMCHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
|
||||
|
||||
|
||||
class ServiceItemContext(object):
|
||||
"""
|
||||
@ -281,11 +284,12 @@ def check_item_selected(list_widget, message):
|
||||
return True
|
||||
|
||||
|
||||
def clean_tags(text):
|
||||
def clean_tags(text, remove_chords=False):
|
||||
"""
|
||||
Remove Tags from text for display
|
||||
|
||||
:param text: Text to be cleaned
|
||||
:param remove_chords: Clean ChordPro tags
|
||||
"""
|
||||
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():
|
||||
text = text.replace(tag['start tag'], '')
|
||||
text = text.replace(tag['end tag'], '')
|
||||
# Remove ChordPro tags
|
||||
if remove_chords:
|
||||
text = re.sub(r'\[.+?\]', r'', text)
|
||||
return text
|
||||
|
||||
|
||||
def expand_tags(text):
|
||||
def expand_tags(text, expand_chord_tags=False, for_printing=False):
|
||||
"""
|
||||
Expand tags HTML for display
|
||||
|
||||
: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():
|
||||
text = text.replace(tag['start tag'], tag['start html'])
|
||||
text = text.replace(tag['end tag'], tag['end html'])
|
||||
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):
|
||||
"""
|
||||
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 .settingstab import SettingsTab
|
||||
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 .renderer import Renderer
|
||||
from .mediamanageritem import MediaManagerItem
|
||||
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
|
||||
|
@ -172,6 +172,7 @@ def upgrade_db(url, upgrade):
|
||||
else:
|
||||
version = int(version_meta.value)
|
||||
if version > upgrade.__version__:
|
||||
session.remove()
|
||||
return version, upgrade.__version__
|
||||
version += 1
|
||||
try:
|
||||
@ -194,7 +195,7 @@ def upgrade_db(url, upgrade):
|
||||
session.commit()
|
||||
upgrade_version = upgrade.__version__
|
||||
version = int(version_meta.value)
|
||||
session.close()
|
||||
session.remove()
|
||||
return version, upgrade_version
|
||||
|
||||
|
||||
|
@ -124,6 +124,25 @@ is the function which has to be called from outside. The generated and returned
|
||||
position: relative;
|
||||
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>
|
||||
<script>
|
||||
var timer = null;
|
||||
@ -444,6 +463,7 @@ HTML_SRC = Template("""
|
||||
position: relative;
|
||||
top: -0.3em;
|
||||
}
|
||||
/* Chords css */${chords_css}
|
||||
</style>
|
||||
<script>
|
||||
var timer = null;
|
||||
@ -592,6 +612,30 @@ LYRICS_FORMAT_SRC = Template("""
|
||||
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):
|
||||
"""
|
||||
@ -636,7 +680,8 @@ def build_html(item, screen, is_live, background, image=None, plugins=None):
|
||||
js_additions=js_additions,
|
||||
bg_image=bgimage_src,
|
||||
image=image_src,
|
||||
html_additions=html_additions)
|
||||
html_additions=html_additions,
|
||||
chords_css=build_chords_css())
|
||||
|
||||
|
||||
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(),
|
||||
family=theme.font_footer_name, size=theme.font_footer_size,
|
||||
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
|
||||
"""
|
||||
import os
|
||||
import imp
|
||||
|
||||
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):
|
||||
@ -70,32 +69,8 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
"""
|
||||
Scan a directory for objects inheriting from the ``Plugin`` class.
|
||||
"""
|
||||
start_depth = len(os.path.abspath(self.base_path).split(os.sep))
|
||||
present_plugin_dir = os.path.join(self.base_path, 'presentations')
|
||||
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]))
|
||||
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
|
||||
extension_loader(glob_pattern)
|
||||
plugin_classes = Plugin.__subclasses__()
|
||||
plugin_objects = []
|
||||
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',
|
||||
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
||||
'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.
|
||||
CR = chr(0x0D) # \r
|
||||
@ -56,20 +57,35 @@ LF = chr(0x0A) # \n
|
||||
PJLINK_PORT = 4352
|
||||
TIMEOUT = 30.0
|
||||
PJLINK_MAX_PACKET = 136
|
||||
PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection
|
||||
'POWR', # Power option
|
||||
'INPT', # Video sources option
|
||||
'AVMT', # Shutter option
|
||||
'ERST', # Error status option
|
||||
'LAMP', # Lamp(s) query (Includes fans)
|
||||
'INST', # Input sources available query
|
||||
'NAME', # Projector name query
|
||||
'INF1', # Manufacturer name query
|
||||
'INF2', # Product name query
|
||||
'INFO', # Other information query
|
||||
'CLSS' # PJLink class support query
|
||||
]}
|
||||
|
||||
# NOTE: Change format to account for some commands are both class 1 and 2
|
||||
PJLINK_VALID_CMD = {
|
||||
'ACKN': ['2', ], # UDP Reply to 'SRCH'
|
||||
'AVMT': ['1', ], # Shutter option
|
||||
'CLSS': ['1', ], # PJLink class support query
|
||||
'ERST': ['1', '2'], # Error status option
|
||||
'FILT': ['2', ], # Get current filter usage time
|
||||
'FREZ': ['2', ], # Set freeze/unfreeze picture being projected
|
||||
'INF1': ['1', ], # Manufacturer name query
|
||||
'INF2': ['1', ], # Product name query
|
||||
'INFO': ['1', ], # Other information 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
|
||||
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.
|
||||
@ -321,53 +337,54 @@ PJLINK_DEFAULT_SOURCES = {
|
||||
'2': translate('OpenLP.DB', 'Video'),
|
||||
'3': translate('OpenLP.DB', 'Digital'),
|
||||
'4': translate('OpenLP.DB', 'Storage'),
|
||||
'5': translate('OpenLP.DB', 'Network')
|
||||
'5': translate('OpenLP.DB', 'Network'),
|
||||
'6': translate('OpenLP.DB', 'Internal')
|
||||
}
|
||||
|
||||
PJLINK_DEFAULT_CODES = {
|
||||
'11': translate('OpenLP.DB', 'RGB 1'),
|
||||
'12': translate('OpenLP.DB', 'RGB 2'),
|
||||
'13': translate('OpenLP.DB', 'RGB 3'),
|
||||
'14': translate('OpenLP.DB', 'RGB 4'),
|
||||
'15': translate('OpenLP.DB', 'RGB 5'),
|
||||
'16': translate('OpenLP.DB', 'RGB 6'),
|
||||
'17': translate('OpenLP.DB', 'RGB 7'),
|
||||
'18': translate('OpenLP.DB', 'RGB 8'),
|
||||
'19': translate('OpenLP.DB', 'RGB 9'),
|
||||
'21': translate('OpenLP.DB', 'Video 1'),
|
||||
'22': translate('OpenLP.DB', 'Video 2'),
|
||||
'23': translate('OpenLP.DB', 'Video 3'),
|
||||
'24': translate('OpenLP.DB', 'Video 4'),
|
||||
'25': translate('OpenLP.DB', 'Video 5'),
|
||||
'26': translate('OpenLP.DB', 'Video 6'),
|
||||
'27': translate('OpenLP.DB', 'Video 7'),
|
||||
'28': translate('OpenLP.DB', 'Video 8'),
|
||||
'29': translate('OpenLP.DB', 'Video 9'),
|
||||
'31': translate('OpenLP.DB', 'Digital 1'),
|
||||
'32': translate('OpenLP.DB', 'Digital 2'),
|
||||
'33': translate('OpenLP.DB', 'Digital 3'),
|
||||
'34': translate('OpenLP.DB', 'Digital 4'),
|
||||
'35': translate('OpenLP.DB', 'Digital 5'),
|
||||
'36': translate('OpenLP.DB', 'Digital 6'),
|
||||
'37': translate('OpenLP.DB', 'Digital 7'),
|
||||
'38': translate('OpenLP.DB', 'Digital 8'),
|
||||
'39': translate('OpenLP.DB', 'Digital 9'),
|
||||
'41': translate('OpenLP.DB', 'Storage 1'),
|
||||
'42': translate('OpenLP.DB', 'Storage 2'),
|
||||
'43': translate('OpenLP.DB', 'Storage 3'),
|
||||
'44': translate('OpenLP.DB', 'Storage 4'),
|
||||
'45': translate('OpenLP.DB', 'Storage 5'),
|
||||
'46': translate('OpenLP.DB', 'Storage 6'),
|
||||
'47': translate('OpenLP.DB', 'Storage 7'),
|
||||
'48': translate('OpenLP.DB', 'Storage 8'),
|
||||
'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')
|
||||
PJLINK_DEFAULT_ITEMS = {
|
||||
'1': translate('OpenLP.DB', '1'),
|
||||
'2': translate('OpenLP.DB', '2'),
|
||||
'3': translate('OpenLP.DB', '3'),
|
||||
'4': translate('OpenLP.DB', '4'),
|
||||
'5': translate('OpenLP.DB', '5'),
|
||||
'6': translate('OpenLP.DB', '6'),
|
||||
'7': translate('OpenLP.DB', '7'),
|
||||
'8': translate('OpenLP.DB', '8'),
|
||||
'9': translate('OpenLP.DB', '9'),
|
||||
'A': translate('OpenLP.DB', 'A'),
|
||||
'B': translate('OpenLP.DB', 'B'),
|
||||
'C': translate('OpenLP.DB', 'C'),
|
||||
'D': translate('OpenLP.DB', 'D'),
|
||||
'E': translate('OpenLP.DB', 'E'),
|
||||
'F': translate('OpenLP.DB', 'F'),
|
||||
'G': translate('OpenLP.DB', 'G'),
|
||||
'H': translate('OpenLP.DB', 'H'),
|
||||
'I': translate('OpenLP.DB', 'I'),
|
||||
'J': translate('OpenLP.DB', 'J'),
|
||||
'K': translate('OpenLP.DB', 'K'),
|
||||
'L': translate('OpenLP.DB', 'L'),
|
||||
'M': translate('OpenLP.DB', 'M'),
|
||||
'N': translate('OpenLP.DB', 'N'),
|
||||
'O': translate('OpenLP.DB', 'O'),
|
||||
'P': translate('OpenLP.DB', 'P'),
|
||||
'Q': translate('OpenLP.DB', 'Q'),
|
||||
'R': translate('OpenLP.DB', 'R'),
|
||||
'S': translate('OpenLP.DB', 'S'),
|
||||
'T': translate('OpenLP.DB', 'T'),
|
||||
'U': translate('OpenLP.DB', 'U'),
|
||||
'V': translate('OpenLP.DB', 'V'),
|
||||
'W': translate('OpenLP.DB', 'W'),
|
||||
'X': translate('OpenLP.DB', 'X'),
|
||||
'Y': translate('OpenLP.DB', 'Y'),
|
||||
'Z': translate('OpenLP.DB', 'Z')
|
||||
}
|
||||
|
||||
# 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))
|
||||
location: Column(String(30))
|
||||
notes: Column(String(200))
|
||||
pjlink_name: Column(String(128)) # From projector (future)
|
||||
manufacturer: Column(String(128)) # From projector (future)
|
||||
model: Column(String(128)) # From projector (future)
|
||||
other: Column(String(128)) # From projector (future)
|
||||
sources: Column(String(128)) # From projector (future)
|
||||
pjlink_name: Column(String(128)) # From projector
|
||||
manufacturer: Column(String(128)) # From projector
|
||||
model: Column(String(128)) # From projector
|
||||
other: Column(String(128)) # From projector
|
||||
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
|
||||
"""
|
||||
@ -164,20 +168,25 @@ class Projector(CommonBase, Base):
|
||||
"""
|
||||
return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
|
||||
'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
|
||||
'manufacturer="{manufacturer}", model="{model}", other="{other}", ' \
|
||||
'sources="{sources}", source_list="{source_list}") >'.format(data=self.id,
|
||||
ip=self.ip,
|
||||
port=self.port,
|
||||
pin=self.pin,
|
||||
name=self.name,
|
||||
location=self.location,
|
||||
notes=self.notes,
|
||||
pjlink_name=self.pjlink_name,
|
||||
manufacturer=self.manufacturer,
|
||||
model=self.model,
|
||||
other=self.other,
|
||||
sources=self.sources,
|
||||
source_list=self.source_list)
|
||||
'manufacturer="{manufacturer}", model="{model}", serial_no="{serial}", other="{other}", ' \
|
||||
'sources="{sources}", source_list="{source_list}", model_filter="{mfilter}", ' \
|
||||
'model_lamp="{mlamp}", sw_version="{sw_ver}") >'.format(data=self.id,
|
||||
ip=self.ip,
|
||||
port=self.port,
|
||||
pin=self.pin,
|
||||
name=self.name,
|
||||
location=self.location,
|
||||
notes=self.notes,
|
||||
pjlink_name=self.pjlink_name,
|
||||
manufacturer=self.manufacturer,
|
||||
model=self.model,
|
||||
other=self.other,
|
||||
sources=self.sources,
|
||||
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))
|
||||
port = Column(String(8))
|
||||
pin = Column(String(20))
|
||||
@ -189,6 +198,10 @@ class Projector(CommonBase, Base):
|
||||
model = Column(String(128))
|
||||
other = 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',
|
||||
order_by='ProjectorSource.code',
|
||||
backref='projector',
|
||||
@ -359,6 +372,10 @@ class ProjectorDB(Manager):
|
||||
old_projector.model = projector.model
|
||||
old_projector.other = projector.other
|
||||
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)
|
||||
|
||||
def delete_projector(self, projector):
|
||||
|
@ -42,7 +42,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
log.debug('pjlink1 loaded')
|
||||
|
||||
__all__ = ['PJLink1']
|
||||
__all__ = ['PJLink']
|
||||
|
||||
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_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, \
|
||||
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, S_NOT_CONNECTED, \
|
||||
S_OFF, S_OK, S_ON, S_STATUS
|
||||
PJLINK_DEFAULT_CODES, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
|
||||
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
|
||||
|
||||
# Shortcuts
|
||||
SocketError = QtNetwork.QAbstractSocket.SocketError
|
||||
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
||||
|
||||
PJLINK_PREFIX = '%'
|
||||
PJLINK_CLASS = '1'
|
||||
PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJLINK_CLASS)
|
||||
PJLINK_CLASS = '1' # Default to class 1 until we query the projector
|
||||
# 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
|
||||
|
||||
|
||||
class PJLink1(QtNetwork.QTcpSocket):
|
||||
class PJLink(QtNetwork.QTcpSocket):
|
||||
"""
|
||||
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
|
||||
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
||||
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):
|
||||
"""
|
||||
@ -100,7 +129,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
self.pin = pin
|
||||
super(PJLink1, self).__init__()
|
||||
super(PJLink, self).__init__()
|
||||
self.dbid = None
|
||||
self.location = None
|
||||
self.notes = None
|
||||
@ -133,7 +162,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
# Socket timer for some possible brain-dead projectors or network cable pulled
|
||||
self.socket_timer = None
|
||||
# Map command to function
|
||||
self.pjlink1_functions = {
|
||||
self.pjlink_functions = {
|
||||
'AVMT': self.process_avmt,
|
||||
'CLSS': self.process_clss,
|
||||
'ERST': self.process_erst,
|
||||
@ -244,8 +273,6 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
self.send_command('INF2', queue=True)
|
||||
if self.pjlink_name is None:
|
||||
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):
|
||||
"""
|
||||
@ -259,7 +286,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
elif status in STATUS_STRING:
|
||||
return STATUS_STRING[status], ERROR_MSG[status]
|
||||
else:
|
||||
return status, translate('OpenLP.PJLink1', 'Unknown status')
|
||||
return status, translate('OpenLP.PJLink', 'Unknown status')
|
||||
|
||||
def change_status(self, status, msg=None):
|
||||
"""
|
||||
@ -269,7 +296,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
:param status: Status code
|
||||
: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)
|
||||
if msg is not None:
|
||||
message = msg
|
||||
@ -322,7 +349,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
elif len(read) < 8:
|
||||
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
||||
return
|
||||
data = decode(read, 'ascii')
|
||||
data = decode(read, 'utf-8')
|
||||
# Possibility of extraneous data on input when reading.
|
||||
# Clean out extraneous characters in buffer.
|
||||
dontcare = self.readLine(self.max_size)
|
||||
@ -403,25 +430,24 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
return
|
||||
self.socket_timer.stop()
|
||||
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()
|
||||
if len(data) < 7:
|
||||
# Not enough data for a packet
|
||||
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
||||
self.send_busy = False
|
||||
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()
|
||||
self.receive_data_signal()
|
||||
return
|
||||
elif '=' not in data:
|
||||
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
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
|
||||
data_split = data.split('=')
|
||||
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(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
|
||||
self.change_status(E_INVALID_DATA)
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
return
|
||||
|
||||
if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
|
||||
if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]):
|
||||
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
||||
self.send_busy = False
|
||||
self.projectorReceivedData.emit()
|
||||
self.receive_data_signal()
|
||||
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)
|
||||
|
||||
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
||||
@ -487,8 +513,10 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
data=opts,
|
||||
salt='' if salt is None
|
||||
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,
|
||||
header=PJLINK_HEADER,
|
||||
header=header,
|
||||
command=cmd,
|
||||
options=opts,
|
||||
suffix=CR)
|
||||
@ -510,11 +538,12 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
self._send_command()
|
||||
|
||||
@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.
|
||||
|
||||
: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(): 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))
|
||||
self.socket_timer.start()
|
||||
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
|
||||
if sent == -1:
|
||||
# Network error?
|
||||
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):
|
||||
"""
|
||||
@ -556,7 +585,13 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
:param cmd: Command to process
|
||||
: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:
|
||||
# Oops - projector error
|
||||
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
||||
@ -568,9 +603,8 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
self.projectorAuthentication.emit(self.name)
|
||||
elif data.upper() == 'ERR1':
|
||||
# Undefined command
|
||||
self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1',
|
||||
'Undefined command:'),
|
||||
data=cmd))
|
||||
self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
|
||||
data=cmd))
|
||||
elif data.upper() == 'ERR2':
|
||||
# Invalid parameter
|
||||
self.change_status(E_PARAMETER)
|
||||
@ -591,8 +625,9 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
self.projectorReceivedData.emit()
|
||||
return
|
||||
|
||||
if cmd in self.pjlink1_functions:
|
||||
self.pjlink1_functions[cmd](data)
|
||||
if cmd in self.pjlink_functions:
|
||||
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
|
||||
self.pjlink_functions[cmd](data)
|
||||
else:
|
||||
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
||||
self.send_busy = False
|
||||
@ -628,6 +663,7 @@ class PJLink1(QtNetwork.QTcpSocket):
|
||||
|
||||
:param data: Power status
|
||||
"""
|
||||
log.debug('({ip}: Processing POWR command'.format(ip=self.ip))
|
||||
if data in PJLINK_POWR_STATUS:
|
||||
power = PJLINK_POWR_STATUS[data]
|
||||
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))
|
||||
self.send_command(cmd='AVMT', opts='10')
|
||||
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.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.ui import MainDisplay
|
||||
|
||||
@ -383,13 +383,14 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
</script>
|
||||
<style>
|
||||
*{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>
|
||||
<body><div id="main"></div></body></html>""")
|
||||
self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
|
||||
self.page_width,
|
||||
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()
|
||||
|
||||
def _paginate_slide(self, lines, line_end):
|
||||
|
@ -34,7 +34,7 @@ import ntpath
|
||||
from PyQt5 import QtGui
|
||||
|
||||
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__)
|
||||
|
||||
@ -117,7 +117,6 @@ class ItemCapabilities(object):
|
||||
|
||||
``HasThumbnails``
|
||||
The item has related thumbnails available
|
||||
|
||||
"""
|
||||
CanPreview = 1
|
||||
CanEdit = 2
|
||||
@ -247,6 +246,8 @@ class ServiceItem(RegistryProperties):
|
||||
self.renderer.set_item_theme(self.theme)
|
||||
self.theme_data, self.main, self.footer = self.renderer.pre_render()
|
||||
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))
|
||||
# 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.
|
||||
@ -260,13 +261,16 @@ class ServiceItem(RegistryProperties):
|
||||
previous_pages[verse_tag] = (slide['raw_slide'], pages)
|
||||
for page in pages:
|
||||
page = page.replace('<br>', '{br}')
|
||||
html_data = expand_tags(html.escape(page.rstrip()))
|
||||
self._display_frames.append({
|
||||
html_data = expand_tags(page.rstrip(), expand_chord_tags)
|
||||
new_frame = {
|
||||
'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;', ' '),
|
||||
'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:
|
||||
pass
|
||||
else:
|
||||
|
@ -143,6 +143,7 @@ def format_milliseconds(milliseconds):
|
||||
seconds=seconds,
|
||||
millis=millis)
|
||||
|
||||
|
||||
from .mediacontroller import MediaController
|
||||
from .playertab import PlayerTab
|
||||
|
||||
|
@ -28,7 +28,8 @@ import os
|
||||
import datetime
|
||||
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.ui import critical_error_message_box
|
||||
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
|
||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
TICK_TIME = 200
|
||||
@ -172,19 +174,9 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
Check to see if we have any media Player's available.
|
||||
"""
|
||||
log.debug('_check_available_media_players')
|
||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'ui', 'media')
|
||||
for filename in os.listdir(controller_dir):
|
||||
if filename.endswith('player.py') and filename != '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)
|
||||
controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
|
||||
glob_pattern = os.path.join(controller_dir, '*player.py')
|
||||
extension_loader(glob_pattern, ['mediaplayer.py'])
|
||||
player_classes = MediaPlayer.__subclasses__()
|
||||
for player_class in player_classes:
|
||||
self.register_players(player_class(self))
|
||||
|
@ -95,7 +95,7 @@ class Ui_PrintServiceDialog(object):
|
||||
self.main_layout.addWidget(self.preview_widget)
|
||||
self.options_widget = QtWidgets.QWidget(print_service_dialog)
|
||||
self.options_widget.hide()
|
||||
self.options_widget.resize(400, 300)
|
||||
self.options_widget.resize(400, 350)
|
||||
self.options_widget.setAutoFillBackground(True)
|
||||
self.options_layout = QtWidgets.QVBoxLayout(self.options_widget)
|
||||
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.meta_data_check_box = QtWidgets.QCheckBox()
|
||||
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.options_group_box.setLayout(self.group_layout)
|
||||
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.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.show_chords_check_box.setText(translate('OpenLP.PrintServiceForm', 'Show chords'))
|
||||
self.title_line_edit.setText(translate('OpenLP.PrintServiceForm', 'Service Sheet'))
|
||||
# Do not change the order.
|
||||
self.zoom_combo_box.addItems([
|
||||
|
@ -37,7 +37,7 @@ from openlp.core.common import AppLocation
|
||||
DEFAULT_CSS = """/*
|
||||
Edit this file to customize the service order print. Note, that not all CSS
|
||||
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 {
|
||||
@ -101,6 +101,19 @@ http://doc.trolltech.com/4.7/richtext-html-subset.html#css-properties
|
||||
.newPage {
|
||||
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')
|
||||
for index, item in enumerate(self.service_manager.service_items):
|
||||
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:
|
||||
if self.footer_text_edit.toPlainText():
|
||||
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_html = None
|
||||
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')
|
||||
else:
|
||||
elif 'chordspacing' not in slide['printing_html']:
|
||||
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_html = slide['html']
|
||||
verse_html = slide['printing_html']
|
||||
# Break the page before the div element.
|
||||
if index != 0 and self.page_break_after_text.isChecked():
|
||||
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, \
|
||||
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.pjlink1 import PJLink1
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink
|
||||
from openlp.core.ui.projector.editform import ProjectorEditForm
|
||||
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
||||
|
||||
@ -690,19 +690,19 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
|
||||
Helper app to build a projector instance
|
||||
|
||||
:param projector: Dict of projector database information
|
||||
:returns: PJLink1() instance
|
||||
:returns: PJLink() instance
|
||||
"""
|
||||
log.debug('_add_projector()')
|
||||
return PJLink1(dbid=projector.id,
|
||||
ip=projector.ip,
|
||||
port=int(projector.port),
|
||||
name=projector.name,
|
||||
location=projector.location,
|
||||
notes=projector.notes,
|
||||
pin=None if projector.pin == '' else projector.pin,
|
||||
poll_time=self.poll_time,
|
||||
socket_timeout=self.socket_timeout
|
||||
)
|
||||
return PJLink(dbid=projector.id,
|
||||
ip=projector.ip,
|
||||
port=int(projector.port),
|
||||
name=projector.name,
|
||||
location=projector.location,
|
||||
notes=projector.notes,
|
||||
pin=None if projector.pin == '' else projector.pin,
|
||||
poll_time=self.poll_time,
|
||||
socket_timeout=self.socket_timeout
|
||||
)
|
||||
|
||||
def add_projector(self, projector, start=False):
|
||||
"""
|
||||
@ -961,7 +961,7 @@ class ProjectorItem(QtCore.QObject):
|
||||
"""
|
||||
Initialization for ProjectorItem instance
|
||||
|
||||
:param link: PJLink1 instance for QListWidgetItem
|
||||
:param link: PJLink instance for QListWidgetItem
|
||||
"""
|
||||
self.link = link
|
||||
self.thread = None
|
||||
|
@ -429,4 +429,5 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
for bible in self.db_cache:
|
||||
self.db_cache[bible].finalise()
|
||||
|
||||
|
||||
__all__ = ['BibleFormat']
|
||||
|
@ -58,7 +58,8 @@ from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.lib import ScreenList
|
||||
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__)
|
||||
|
@ -29,7 +29,7 @@ from subprocess import check_output, CalledProcessError
|
||||
from openlp.core.common import AppLocation, check_binary_exists
|
||||
from openlp.core.common import Settings, is_win
|
||||
from openlp.core.lib import ScreenList
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
if is_win():
|
||||
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
|
||||
|
@ -43,7 +43,7 @@ if is_win():
|
||||
from openlp.core.lib import ScreenList
|
||||
from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate
|
||||
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__)
|
||||
|
||||
|
@ -35,7 +35,7 @@ if is_win():
|
||||
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.lib import ScreenList
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -197,6 +197,7 @@ class PPTViewer(QtWidgets.QWidget):
|
||||
def openDialog(self):
|
||||
self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pptdll = cdll.LoadLibrary(r'pptviewlib.dll')
|
||||
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.ui import critical_error_message_box
|
||||
from openlp.core.ui.lib import PathEdit
|
||||
from .pdfcontroller import PdfController
|
||||
from openlp.plugins.presentations.lib.pdfcontroller import PdfController
|
||||
|
||||
|
||||
class PresentationTab(SettingsTab):
|
||||
|
@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
@ -20,19 +20,18 @@
|
||||
# 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
|
||||
formats.
|
||||
The :mod:`openlp.plugins.presentations.presentationplugin` module provides the ability for OpenLP to display
|
||||
presentations from a variety of document formats.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
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.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
log.debug('check_pre_conditions')
|
||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'presentations', 'lib')
|
||||
for filename in os.listdir(controller_dir):
|
||||
if filename.endswith('controller.py') and filename != '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_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
|
||||
glob_pattern = os.path.join(controller_dir, '*controller.py')
|
||||
extension_loader(glob_pattern, ['presentationcontroller.py'])
|
||||
controller_classes = PresentationController.__subclasses__()
|
||||
for controller_class in controller_classes:
|
||||
controller = controller_class(self)
|
||||
|
46
openlp/plugins/remotes/html/chords.html
Normal file
46
openlp/plugins/remotes/html/chords.html
Normal file
@ -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>
|
96
openlp/plugins/remotes/html/css/chords.css
Normal file
96
openlp/plugins/remotes/html/css/chords.css
Normal file
@ -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;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE 10+ */
|
||||
user-select: none; /* Future */
|
||||
}
|
||||
|
||||
#currentslide {
|
||||
|
331
openlp/plugins/remotes/html/js/chords.js
Normal file
331
openlp/plugins/remotes/html/js/chords.js
Normal file
@ -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}),
|
||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||
('^/(chords)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||
@ -318,10 +319,12 @@ class HttpRouter(RegistryProperties):
|
||||
"""
|
||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||
self.template_vars = {
|
||||
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
||||
'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),
|
||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||
@ -482,7 +485,8 @@ class HttpRouter(RegistryProperties):
|
||||
'display': self.live_controller.desktop_screen.isChecked(),
|
||||
'version': 2,
|
||||
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
||||
'isAuthorised': self.authorised
|
||||
'isAuthorised': self.authorised,
|
||||
'chordNotation': Settings().value('songs/chord notation'),
|
||||
}
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': result}).encode()
|
||||
@ -554,6 +558,7 @@ class HttpRouter(RegistryProperties):
|
||||
item['tag'] = str(frame['verseTag'])
|
||||
else:
|
||||
item['tag'] = str(index + 1)
|
||||
item['chords_text'] = str(frame['chords_text'])
|
||||
item['text'] = str(frame['text'])
|
||||
item['html'] = str(frame['html'])
|
||||
# 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.setOpenExternalLinks(True)
|
||||
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.setObjectName('live_url_label')
|
||||
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.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote 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.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||
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.lib import build_icon, translate
|
||||
from openlp.core.lib.ui import UiStrings, create_button_box
|
||||
from openlp.core.common import Settings
|
||||
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.addStretch()
|
||||
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.dialog_layout.addWidget(self.button_box)
|
||||
self.retranslateUi(edit_verse_dialog)
|
||||
@ -82,3 +98,7 @@ class Ui_EditVerseDialog(object):
|
||||
self.insert_button.setText(translate('SongsPlugin.EditVerseForm', '&Insert'))
|
||||
self.insert_button.setToolTip(translate('SongsPlugin.EditVerseForm',
|
||||
'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 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
|
||||
|
||||
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.verse_text_edit.cursorPositionChanged.connect(self.on_cursor_position_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):
|
||||
"""
|
||||
@ -95,6 +100,41 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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('---['):
|
||||
text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=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 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.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
@ -380,7 +380,7 @@ def clean_song(manager, song):
|
||||
if isinstance(song.lyrics, bytes):
|
||||
song.lyrics = str(song.lyrics, encoding='utf8')
|
||||
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.
|
||||
if not song.authors_songs:
|
||||
name = SongStrings.AuthorUnknown
|
||||
@ -541,3 +541,123 @@ def delete_song(song_id, song_plugin):
|
||||
except OSError:
|
||||
log.exception('Could not remove directory: {path}'.format(path=save_path))
|
||||
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.lyrix import LyrixImport
|
||||
from .importers.videopsalm import VideoPsalmImport
|
||||
from .importers.chordpro import ChordProImport
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -155,29 +156,30 @@ class SongFormat(object):
|
||||
OpenLP2 = 1
|
||||
Generic = 2
|
||||
CCLI = 3
|
||||
DreamBeam = 4
|
||||
EasySlides = 5
|
||||
EasyWorshipDB = 6
|
||||
EasyWorshipService = 7
|
||||
FoilPresenter = 8
|
||||
Lyrix = 9
|
||||
MediaShout = 10
|
||||
OpenSong = 11
|
||||
OPSPro = 12
|
||||
PowerPraise = 13
|
||||
PowerSong = 14
|
||||
PresentationManager = 15
|
||||
ProPresenter = 16
|
||||
SongBeamer = 17
|
||||
SongPro = 18
|
||||
SongShowPlus = 19
|
||||
SongsOfFellowship = 20
|
||||
SundayPlus = 21
|
||||
VideoPsalm = 22
|
||||
WordsOfWorship = 23
|
||||
WorshipAssistant = 24
|
||||
WorshipCenterPro = 25
|
||||
ZionWorx = 26
|
||||
ChordPro = 4
|
||||
DreamBeam = 5
|
||||
EasySlides = 6
|
||||
EasyWorshipDB = 7
|
||||
EasyWorshipService = 8
|
||||
FoilPresenter = 9
|
||||
Lyrix = 10
|
||||
MediaShout = 11
|
||||
OpenSong = 12
|
||||
OPSPro = 13
|
||||
PowerPraise = 14
|
||||
PowerSong = 15
|
||||
PresentationManager = 16
|
||||
ProPresenter = 17
|
||||
SongBeamer = 18
|
||||
SongPro = 19
|
||||
SongShowPlus = 20
|
||||
SongsOfFellowship = 21
|
||||
SundayPlus = 22
|
||||
VideoPsalm = 23
|
||||
WordsOfWorship = 24
|
||||
WorshipAssistant = 25
|
||||
WorshipCenterPro = 26
|
||||
ZionWorx = 27
|
||||
|
||||
# Set optional attribute defaults
|
||||
__defaults__ = {
|
||||
@ -224,6 +226,13 @@ class SongFormat(object):
|
||||
'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
|
||||
'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: {
|
||||
'class': DreamBeamImport,
|
||||
'name': 'DreamBeam',
|
||||
@ -427,6 +436,7 @@ class SongFormat(object):
|
||||
SongFormat.OpenLP2,
|
||||
SongFormat.Generic,
|
||||
SongFormat.CCLI,
|
||||
SongFormat.ChordPro,
|
||||
SongFormat.DreamBeam,
|
||||
SongFormat.EasySlides,
|
||||
SongFormat.EasyWorshipDB,
|
||||
|
178
openlp/plugins/songs/lib/importers/chordpro.py
Normal file
178
openlp/plugins/songs/lib/importers/chordpro.py
Normal file
@ -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.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.importers.songimport import SongImport
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
@ -87,7 +87,7 @@ class OpenSongImport(SongImport):
|
||||
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
|
||||
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
|
||||
1 Some____ Words
|
||||
@ -195,14 +195,34 @@ class OpenSongImport(SongImport):
|
||||
lyrics = str(root.lyrics)
|
||||
else:
|
||||
lyrics = ''
|
||||
chords = []
|
||||
for this_line in lyrics.split('\n'):
|
||||
if not this_line.strip():
|
||||
continue
|
||||
# skip this line if it is a comment
|
||||
if this_line.startswith(';'):
|
||||
continue
|
||||
# skip guitar chords and page and column breaks
|
||||
if this_line.startswith('.') or this_line.startswith('---') or this_line.startswith('-!!'):
|
||||
# skip page and column breaks
|
||||
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
|
||||
# verse/chorus/etc. marker
|
||||
if this_line.startswith('['):
|
||||
@ -228,12 +248,20 @@ class OpenSongImport(SongImport):
|
||||
# number at start of line.. it's verse number
|
||||
if this_line[0].isdigit():
|
||||
verse_num = this_line[0]
|
||||
this_line = this_line[1:].strip()
|
||||
this_line = this_line[1:]
|
||||
verses.setdefault(verse_tag, {})
|
||||
verses[verse_tag].setdefault(verse_num, {})
|
||||
if inst not in verses[verse_tag][verse_num]:
|
||||
verses[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
|
||||
this_line = self.tidy_text(this_line)
|
||||
this_line = this_line.replace('_', '')
|
||||
|
@ -25,10 +25,12 @@ The :mod:`songbeamer` module provides the functionality for importing SongBeamer
|
||||
import logging
|
||||
import os
|
||||
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.importers.songimport import SongImport
|
||||
from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
|
||||
|
||||
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):
|
||||
"""
|
||||
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.current_verse = ''
|
||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||
read_verses = False
|
||||
self.chord_table = None
|
||||
file_name = os.path.split(import_file)[1]
|
||||
if os.path.isfile(import_file):
|
||||
# Detect the encoding
|
||||
@ -125,33 +134,103 @@ class SongBeamerImport(SongImport):
|
||||
continue
|
||||
self.title = file_name.split('.sng')[0]
|
||||
read_verses = False
|
||||
for line in song_data:
|
||||
# Just make sure that the line is of the type 'Unicode'.
|
||||
line = str(line).strip()
|
||||
# The first verse separator doesn't count, but the others does, so line count starts at -1
|
||||
line_number = -1
|
||||
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:
|
||||
self.parseTags(line)
|
||||
elif line.startswith('--'):
|
||||
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
|
||||
self.parse_tags(line)
|
||||
elif stripped_line.startswith('---'):
|
||||
# '---' is a verse breaker
|
||||
if self.current_verse:
|
||||
self.replace_html_tags()
|
||||
self.add_verse(self.current_verse, self.current_verse_type)
|
||||
self.current_verse = ''
|
||||
self.current_verse_type = VerseType.tags[VerseType.Verse]
|
||||
first_verse = False
|
||||
read_verses = 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:
|
||||
if verse_start:
|
||||
verse_start = False
|
||||
if not self.check_verse_marks(line):
|
||||
self.current_verse = line + '\n'
|
||||
verse_mark = self.check_verse_marks(line)
|
||||
# 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:
|
||||
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:
|
||||
self.current_verse += line + '\n'
|
||||
line = self.insert_chords(line_number, line)
|
||||
self.current_verse += line.strip() + '\n'
|
||||
line_number += 1
|
||||
if self.current_verse:
|
||||
self.replace_html_tags()
|
||||
self.add_verse(self.current_verse, self.current_verse_type)
|
||||
if not self.finish():
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
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.
|
||||
|
||||
@ -176,8 +255,10 @@ class SongBeamerImport(SongImport):
|
||||
self.add_copyright(tag_val[1])
|
||||
elif tag_val[0] == '#AddCopyrightInfo':
|
||||
pass
|
||||
elif tag_val[0] == '#AudioFile':
|
||||
self.parse_audio_file(tag_val[1])
|
||||
elif tag_val[0] == '#Author':
|
||||
self.parse_author(tag_val[1])
|
||||
self.parse_author(tag_val[1], 'words')
|
||||
elif tag_val[0] == '#BackgroundImage':
|
||||
pass
|
||||
elif tag_val[0] == '#Bible':
|
||||
@ -187,13 +268,16 @@ class SongBeamerImport(SongImport):
|
||||
elif tag_val[0] == '#CCLI':
|
||||
self.ccli_number = tag_val[1]
|
||||
elif tag_val[0] == '#Chords':
|
||||
pass
|
||||
self.chord_table = self.parse_chords(tag_val[1])
|
||||
elif tag_val[0] == '#ChurchSongID':
|
||||
pass
|
||||
elif tag_val[0] == '#ColorChords':
|
||||
pass
|
||||
elif tag_val[0] == '#Comments':
|
||||
self.comments = tag_val[1]
|
||||
try:
|
||||
self.comments = base64.b64decode(tag_val[1]).decode(self.input_file_encoding)
|
||||
except ValueError:
|
||||
self.comments = tag_val[1]
|
||||
elif tag_val[0] == '#Editor':
|
||||
pass
|
||||
elif tag_val[0] == '#Font':
|
||||
@ -217,7 +301,7 @@ class SongBeamerImport(SongImport):
|
||||
elif tag_val[0] == '#LangCount':
|
||||
pass
|
||||
elif tag_val[0] == '#Melody':
|
||||
self.parse_author(tag_val[1])
|
||||
self.parse_author(tag_val[1], 'music')
|
||||
elif tag_val[0] == '#NatCopyright':
|
||||
pass
|
||||
elif tag_val[0] == '#OTitle':
|
||||
@ -243,7 +327,7 @@ class SongBeamerImport(SongImport):
|
||||
elif tag_val[0] == '#TextAlign':
|
||||
pass
|
||||
elif tag_val[0] == '#Title':
|
||||
self.title = str(tag_val[1]).strip()
|
||||
self.title = tag_val[1].strip()
|
||||
elif tag_val[0] == '#TitleAlign':
|
||||
pass
|
||||
elif tag_val[0] == '#TitleFontSize':
|
||||
@ -263,25 +347,80 @@ class SongBeamerImport(SongImport):
|
||||
elif tag_val[0] == '#Version':
|
||||
pass
|
||||
elif tag_val[0] == '#VerseOrder':
|
||||
# TODO: add the verse order.
|
||||
pass
|
||||
verse_order = tag_val[1].strip()
|
||||
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):
|
||||
"""
|
||||
Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
|
||||
``False``.
|
||||
|
||||
:param line: The line to check for marks (unicode).
|
||||
:param line: The line to check for marks.
|
||||
"""
|
||||
marks = line.split(' ')
|
||||
if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
|
||||
self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
|
||||
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=']
|
||||
new_verse_mark = self.convert_verse_marks(line)
|
||||
if new_verse_mark:
|
||||
self.current_verse_type = new_verse_mark
|
||||
return True
|
||||
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 += 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
|
||||
for 'Mr and Mrs Smith' and turn it to 'Mr Smith' and 'Mrs Smith'.
|
||||
@ -256,7 +256,10 @@ class SongImport(QtCore.QObject):
|
||||
if author2.endswith('.'):
|
||||
author2 = author2[:-1]
|
||||
if author2:
|
||||
self.add_author(author2)
|
||||
if type:
|
||||
self.add_author(author2, type)
|
||||
else:
|
||||
self.add_author(author2)
|
||||
|
||||
def add_author(self, author, type=None):
|
||||
"""
|
||||
@ -304,12 +307,23 @@ class SongImport(QtCore.QObject):
|
||||
if verse_def not in self.verse_order_list_generated:
|
||||
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:
|
||||
self.verse_order_list_generated.append(self.verse_order_list_generated[-1])
|
||||
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_useful = True
|
||||
|
||||
def check_complete(self):
|
||||
|
@ -26,8 +26,9 @@ exproted from Lyrix."""
|
||||
import logging
|
||||
import json
|
||||
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.db import AuthorType
|
||||
|
||||
@ -123,7 +124,11 @@ class VideoPsalmImport(SongImport):
|
||||
for verse in song['Verses']:
|
||||
if 'Text' not in verse:
|
||||
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():
|
||||
self.log_error('Could not import {title}'.format(title=self.title))
|
||||
except Exception as e:
|
||||
|
@ -61,7 +61,7 @@ import re
|
||||
|
||||
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.lib import FormattingTags
|
||||
from openlp.plugins.songs.lib import VerseType, clean_song
|
||||
@ -154,7 +154,7 @@ class OpenLyrics(object):
|
||||
OpenLP does not support the attribute *lang*.
|
||||
|
||||
``<chord>``
|
||||
This property is not supported.
|
||||
This property is fully supported.
|
||||
|
||||
``<comments>``
|
||||
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.
|
||||
if index < len(optional_verses) - 1:
|
||||
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):
|
||||
"""
|
||||
@ -595,8 +607,7 @@ class OpenLyrics(object):
|
||||
|
||||
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
|
||||
converted.
|
||||
Converts the xml text with mixed content to OpenLP representation. Chords and formatting tags are converted.
|
||||
|
||||
: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
|
||||
@ -608,12 +619,14 @@ class OpenLyrics(object):
|
||||
# TODO: Verify format() with template variables
|
||||
if element.tag == NSMAP % 'comment':
|
||||
if element.tail:
|
||||
# Append tail text at chord element.
|
||||
# Append tail text at comment element.
|
||||
text += element.tail
|
||||
return text
|
||||
# Skip <chord> element - not yet supported.
|
||||
# Convert chords to ChordPro format which OpenLP uses internally
|
||||
# TODO: Verify format() with template variables
|
||||
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:
|
||||
# Append tail text at chord element.
|
||||
text += element.tail
|
||||
@ -666,7 +679,7 @@ class OpenLyrics(object):
|
||||
text = self._process_lines_mixed_content(element)
|
||||
# OpenLyrics version <= 0.7 contains <line> elements to represent lines. First child element is tested.
|
||||
else:
|
||||
# Loop over the "line" elements removing comments and chords.
|
||||
# Loop over the "line" elements removing comments
|
||||
for line in element:
|
||||
# Skip comment lines.
|
||||
# TODO: Verify format() with template variables
|
||||
|
@ -60,6 +60,35 @@ class SongsTab(SettingsTab):
|
||||
self.display_copyright_check_box.setObjectName('copyright_check_box')
|
||||
self.mode_layout.addWidget(self.display_copyright_check_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.right_layout.addStretch()
|
||||
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_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.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):
|
||||
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',
|
||||
'Display "{symbol}" symbol before copyright '
|
||||
'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):
|
||||
self.song_search = (check_state == QtCore.Qt.Checked)
|
||||
@ -104,6 +149,21 @@ class SongsTab(SettingsTab):
|
||||
def on_copyright_check_box_changed(self, check_state):
|
||||
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):
|
||||
settings = Settings()
|
||||
settings.beginGroup(self.settings_section)
|
||||
@ -113,12 +173,25 @@ class SongsTab(SettingsTab):
|
||||
self.display_songbook = settings.value('display songbook')
|
||||
self.display_written_by = settings.value('display written by')
|
||||
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.update_on_edit_check_box.setChecked(self.update_edit)
|
||||
self.add_from_service_check_box.setChecked(self.update_load)
|
||||
self.display_songbook_check_box.setChecked(self.display_songbook)
|
||||
self.display_written_by_check_box.setChecked(self.display_written_by)
|
||||
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()
|
||||
|
||||
def save(self):
|
||||
@ -130,6 +203,10 @@ class SongsTab(SettingsTab):
|
||||
settings.setValue('display songbook', self.display_songbook)
|
||||
settings.setValue('display written by', self.display_written_by)
|
||||
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()
|
||||
if self.tab_visited:
|
||||
self.settings_form.register_post_process('songs_config_updated')
|
||||
|
@ -66,7 +66,11 @@ __default_settings__ = {
|
||||
'songs/last directory export': '',
|
||||
'songs/songselect username': '',
|
||||
'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_enchant_backends_and_languages()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -217,5 +217,6 @@ def main():
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -1,4 +1,4 @@
|
||||
[pep8]
|
||||
exclude=resources.py,vlc.py
|
||||
max-line-length = 120
|
||||
ignore = E402
|
||||
ignore = E402,E722
|
||||
|
@ -121,11 +121,11 @@ class TestCategoryActionList(TestCase):
|
||||
self.list.add(self.action2)
|
||||
|
||||
# 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
|
||||
self.assertEquals(len(self.list), 2)
|
||||
self.assertIs(l[0], self.action1)
|
||||
self.assertIs(l[1], self.action2)
|
||||
self.assertIs(list[0], self.action1)
|
||||
self.assertIs(list[1], self.action2)
|
||||
|
||||
def test_remove(self):
|
||||
"""
|
||||
|
@ -22,11 +22,13 @@
|
||||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
from pathlib import Path
|
||||
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, \
|
||||
is_linux, clean_button_text
|
||||
from openlp.core import common
|
||||
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):
|
||||
@ -72,6 +74,72 @@ class TestCommonFunctions(TestCase):
|
||||
mocked_exists.assert_called_with(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):
|
||||
"""
|
||||
Test the de_hump function with a class name
|
||||
@ -83,7 +151,7 @@ class TestCommonFunctions(TestCase):
|
||||
new_string = de_hump(string)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
@ -96,7 +164,20 @@ class TestCommonFunctions(TestCase):
|
||||
new_string = de_hump(string)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
|
@ -8,7 +8,7 @@ from PyQt5 import QtCore, QtWebKit
|
||||
|
||||
from openlp.core.common import Settings
|
||||
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 tests.helpers.testmixin import TestMixin
|
||||
@ -60,6 +60,29 @@ HTML = """
|
||||
position: relative;
|
||||
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>
|
||||
<script>
|
||||
var timer = null;
|
||||
@ -211,6 +234,34 @@ FOOTER_CSS_BASE = """
|
||||
FOOTER_CSS = FOOTER_CSS_BASE % ('nowrap')
|
||||
FOOTER_CSS_WRAP = FOOTER_CSS_BASE % ('normal')
|
||||
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):
|
||||
@ -222,6 +273,7 @@ class Htmbuilder(TestCase, TestMixin):
|
||||
Create the UI
|
||||
"""
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
@ -403,3 +455,17 @@ class Htmbuilder(TestCase, TestMixin):
|
||||
# WHEN: Retrieving the webkit version
|
||||
# THEN: Webkit versions should match
|
||||
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 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, \
|
||||
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'))
|
||||
|
||||
@ -745,3 +747,116 @@ class TestLib(TestCase):
|
||||
# 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, '
|
||||
'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!')
|
||||
|
43
tests/functional/openlp_core_lib/test_projector_constants.py
Normal file
43
tests/functional/openlp_core_lib/test_projector_constants.py
Normal file
@ -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.
|
||||
"""
|
||||
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.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, PJLINK_POWR_STATUS
|
||||
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, S_CONNECTED
|
||||
|
||||
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):
|
||||
@ -163,7 +164,13 @@ class TestPJLink(TestCase):
|
||||
'Lamp 3 hours should have been set to 33333')
|
||||
|
||||
@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
|
||||
"""
|
||||
@ -176,9 +183,17 @@ class TestPJLink(TestCase):
|
||||
|
||||
# THEN: Power should be 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')
|
||||
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
|
||||
"""
|
||||
@ -191,6 +206,8 @@ class TestPJLink(TestCase):
|
||||
|
||||
# THEN: Power should be 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')
|
||||
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
|
||||
self.assertEquals("{test}".format(test=mock_send_command.call_args),
|
||||
"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.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.lib.projector.db import Manufacturer, Model, Projector, ProjectorDB, ProjectorSource, Source
|
||||
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):
|
||||
@ -45,7 +47,11 @@ def compare_data(one, two):
|
||||
one.port == two.port and \
|
||||
one.name == two.name 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):
|
||||
@ -168,6 +174,10 @@ class TestProjectorDB(TestCase):
|
||||
record.name = TEST3_DATA['name']
|
||||
record.location = TEST3_DATA['location']
|
||||
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)
|
||||
self.assertTrue(updated, 'Save updated record should have returned True')
|
||||
record = self.projector.get_projector_by_ip(TEST3_DATA['ip'])
|
||||
@ -246,7 +256,8 @@ class TestProjectorDB(TestCase):
|
||||
projector = Projector()
|
||||
|
||||
# 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.id = 0
|
||||
projector.ip = '127.0.0.1'
|
||||
@ -262,8 +273,9 @@ class TestProjectorDB(TestCase):
|
||||
self.assertEqual(str(projector),
|
||||
'< 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", '
|
||||
'manufacturer="IN YOUR DREAMS", model="OpenLP", other="None", sources="None", '
|
||||
'source_list="[]") >',
|
||||
'manufacturer="IN YOUR DREAMS", model="OpenLP", serial_no="None", other="None", '
|
||||
'sources="None", source_list="[]", model_filter="None", model_lamp="None", '
|
||||
'sw_version="None") >',
|
||||
'Projector.__repr__() should have returned a proper representation string')
|
||||
|
||||
def test_projectorsource_repr(self):
|
||||
|
@ -27,7 +27,7 @@ from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
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
|
||||
|
||||
@ -38,6 +38,23 @@ VERSE = 'The Lord said to {r}Noah{/r}: \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{/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']
|
||||
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
|
||||
service_item = ServiceItem(None)
|
||||
service_item.add_icon = MagicMock()
|
||||
FormattingTags.load_tags()
|
||||
|
||||
# WHEN: We add a custom from a saved service
|
||||
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
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
@ -300,6 +318,7 @@ class TestServiceItem(TestCase):
|
||||
# GIVEN: A new service item and a mocked add icon function
|
||||
service_item = ServiceItem(None)
|
||||
service_item.add_icon = MagicMock()
|
||||
FormattingTags.load_tags()
|
||||
|
||||
# WHEN: We add a custom from a saved service
|
||||
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
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
|
52
tests/functional/openlp_plugins/songs/test_chordproimport.py
Normal file
52
tests/functional/openlp_plugins/songs/test_chordproimport.py
Normal file
@ -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
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
@ -114,6 +114,7 @@ class TestFieldDesc:
|
||||
self.field_type = field_type
|
||||
self.size = size
|
||||
|
||||
|
||||
TEST_DATA_ENCODING = 'cp1252'
|
||||
CODE_PAGE_MAPPINGS = [
|
||||
(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.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
|
||||
|
||||
|
||||
@ -206,7 +206,7 @@ class TestLib(TestCase):
|
||||
assert result[0][3] == 0, 'The start 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.
|
||||
"""
|
||||
@ -264,6 +264,85 @@ class TestLib(TestCase):
|
||||
# THEN: The stripped text matches thed expected 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):
|
||||
"""
|
||||
|
@ -42,10 +42,16 @@ class TestOpenSongFileImport(SongImportTestHelper):
|
||||
self.importer_module_name = 'opensong'
|
||||
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
|
||||
"""
|
||||
# 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.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
|
||||
self.file_import([os.path.join(TEST_PATH, 'Beautiful Garden Of Prayer')],
|
||||
|
@ -42,12 +42,21 @@ class TestSongBeamerFileImport(SongImportTestHelper):
|
||||
self.importer_module_name = 'songbeamer'
|
||||
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.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):
|
||||
"""
|
||||
@ -66,6 +75,16 @@ class TestSongBeamerImport(TestCase):
|
||||
Create the registry
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@ -85,43 +104,38 @@ class TestSongBeamerImport(TestCase):
|
||||
"""
|
||||
Test SongBeamerImport.do_import handles different invalid import_source values
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = SongBeamerImport(mocked_manager, filenames=[])
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = True
|
||||
# GIVEN: A mocked out import wizard
|
||||
mocked_import_wizard = MagicMock()
|
||||
self.importer.import_wizard = mocked_import_wizard
|
||||
self.importer.stop_import_flag = True
|
||||
|
||||
# WHEN: Import source is not a list
|
||||
for source in ['not a list', 0]:
|
||||
importer.import_source = source
|
||||
# WHEN: Import source is not a list
|
||||
for source in ['not a list', 0]:
|
||||
self.importer.import_source = source
|
||||
|
||||
# THEN: do_import should return none and the progress bar maximum should not be set.
|
||||
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
|
||||
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
||||
'setMaxium on import_wizard.progress_bar should not have been called')
|
||||
# THEN: do_import should return none and the progress bar maximum should not be set.
|
||||
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,
|
||||
'setMaxium on import_wizard.progress_bar should not have been called')
|
||||
|
||||
def test_valid_import_source(self):
|
||||
"""
|
||||
Test SongBeamerImport.do_import handles different invalid import_source values
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch('openlp.plugins.songs.lib.importers.songbeamer.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = SongBeamerImport(mocked_manager, filenames=[])
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = True
|
||||
# GIVEN: A mocked out import wizard
|
||||
mocked_import_wizard = MagicMock()
|
||||
self.importer.import_wizard = mocked_import_wizard
|
||||
self.importer.stop_import_flag = True
|
||||
|
||||
# WHEN: Import source is a list
|
||||
importer.import_source = ['List', 'of', 'files']
|
||||
# WHEN: Import source is a list
|
||||
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
|
||||
# import_source.
|
||||
self.assertIsNone(importer.do_import(),
|
||||
'do_import should return None when import_source is a list and stop_import_flag is True')
|
||||
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
|
||||
# THEN: do_import should return none and the progress bar setMaximum should be called with the length of
|
||||
# import_source.
|
||||
self.assertIsNone(self.importer.do_import(),
|
||||
'do_import should return None when import_source is a list and stop_import_flag is True')
|
||||
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(self.importer.import_source))
|
||||
|
||||
def test_check_verse_marks(self):
|
||||
"""
|
||||
@ -130,75 +144,76 @@ class TestSongBeamerImport(TestCase):
|
||||
|
||||
# GIVEN: line with unnumbered verse-type
|
||||
line = 'Refrain'
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back true and c as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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
|
||||
line = 'ReFrain '
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back true and c as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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
|
||||
line = 'VersE 1'
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back true and v1 as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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)
|
||||
line = '$$M=special'
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back true and o as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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
|
||||
line = 'Jesus my saviour'
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back false and none as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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
|
||||
line = 'Praise him'
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back false and none as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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)
|
||||
line = ' '
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back false and none as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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)
|
||||
line = ''
|
||||
self.current_verse_type = None
|
||||
self.importer.current_verse_type = None
|
||||
# WHEN: line is being checked for verse marks
|
||||
result = SongBeamerImport.check_verse_marks(self, line)
|
||||
# THEN: we should get back false and none as self.current_verse_type
|
||||
result = self.importer.check_verse_marks(line)
|
||||
# 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.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):
|
||||
"""
|
||||
|
@ -25,6 +25,7 @@ This module contains tests for the VideoPsalm 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', 'videopsalmsongs'))
|
||||
@ -37,10 +38,16 @@ class TestVideoPsalmFileImport(SongImportTestHelper):
|
||||
self.importer_module_name = 'videopsalm'
|
||||
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
|
||||
"""
|
||||
# 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.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'),
|
||||
|
29
tests/resources/chordprosongs/swing-low.chordpro
Normal file
29
tests/resources/chordprosongs/swing-low.chordpro
Normal file
@ -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}
|
22
tests/resources/chordprosongs/swing-low.json
Normal file
22
tests/resources/chordprosongs/swing-low.json
Normal file
@ -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": [],
|
||||
"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"
|
||||
],
|
||||
[
|
||||
"'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"
|
||||
],
|
||||
[
|
||||
"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"
|
||||
],
|
||||
[
|
||||
"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"
|
||||
],
|
||||
[
|
||||
"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"
|
||||
]
|
||||
]
|
||||
|
@ -19,23 +19,23 @@
|
||||
"verse_order_list": [],
|
||||
"verses": [
|
||||
[
|
||||
"Amazing grace! How sweet the sound!\nThat saved a wretch like me!\nI once was lost, but now am found;\nWas blind, but now I see.",
|
||||
"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"
|
||||
],
|
||||
[
|
||||
"'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"
|
||||
],
|
||||
[
|
||||
"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"
|
||||
],
|
||||
[
|
||||
"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"
|
||||
],
|
||||
[
|
||||
"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"
|
||||
]
|
||||
]
|
||||
|
@ -27,6 +27,8 @@ import os
|
||||
from tempfile import gettempdir
|
||||
|
||||
# Test data
|
||||
TEST_DB_PJLINK1 = 'projector_pjlink1.sqlite'
|
||||
|
||||
TEST_DB = os.path.join(gettempdir(), 'openlp-test-projectordb.sql')
|
||||
|
||||
TEST_SALT = '498e4a67'
|
||||
@ -44,18 +46,243 @@ TEST1_DATA = dict(ip='111.111.111.111',
|
||||
pin='1111',
|
||||
name='___TEST_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',
|
||||
port='2222',
|
||||
pin='2222',
|
||||
name='___TEST_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',
|
||||
port='3333',
|
||||
pin='3333',
|
||||
name='___TEST_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'
|
||||
}
|
||||
|
BIN
tests/resources/projector/projector_pjlink1.sqlite
Normal file
BIN
tests/resources/projector/projector_pjlink1.sqlite
Normal file
Binary file not shown.
29
tests/resources/songbeamersongs/Amazing Grace.json
Normal file
29
tests/resources/songbeamersongs/Amazing Grace.json
Normal file
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
37
tests/resources/songbeamersongs/Amazing Grace.sng
Normal file
37
tests/resources/songbeamersongs/Amazing Grace.sng
Normal file
@ -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_number": "1",
|
||||
"authors": ["Carl Brockhaus", "Johann Jakob Vetter"]
|
||||
"authors": [
|
||||
["Carl Brockhaus", "words"],
|
||||
["Johann Jakob Vetter", "music"]
|
||||
]
|
||||
}
|
||||
|
14
tests/resources/songbeamersongs/When I Call On You.json
Normal file
14
tests/resources/songbeamersongs/When I Call On You.json
Normal file
@ -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"
|
||||
]
|
||||
]
|
||||
}
|
14
tests/resources/songbeamersongs/When I Call On You.sng
Normal file
14
tests/resources/songbeamersongs/When I Call On You.sng
Normal file
@ -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",
|
||||
"authors": ["Author"],
|
||||
"authors": [
|
||||
["Author", "words"]
|
||||
],
|
||||
"verses" : [
|
||||
["Here are a couple of \"weird\" chars’’’.\n", "v"],
|
||||
["Here is another one….\n\n", "v"]
|
||||
|
Loading…
Reference in New Issue
Block a user