diff --git a/openlp.py b/openlp.py index 8a53fe965..5d507606d 100755 --- a/openlp.py +++ b/openlp.py @@ -36,10 +36,9 @@ if __name__ == '__main__': """ Instantiate and run the application. """ - # Mac OS X passes arguments like '-psn_XXXX' to gui application. - # This argument is process serial number. However, this causes - # conflict with other OpenLP arguments. Since we do not use this - # argument we can delete it to avoid any potential conflicts. + # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number. + # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it + # to avoid any potential conflicts. if sys.platform.startswith('darwin'): sys.argv = [x for x in sys.argv if not x.startswith('-psn')] main() diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 4ab94a250..cb9105797 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -36,14 +36,14 @@ logging and a plugin framework are contained within the openlp.core module. import os import sys -import platform import logging from optparse import OptionParser from traceback import format_exception from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists +from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists, \ + is_macosx, is_win from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources from openlp.core.ui.mainwindow import MainWindow @@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main'] log = logging.getLogger() -NT_REPAIR_STYLESHEET = """ +WIN_REPAIR_STYLESHEET = """ QMainWindow::separator { border: none; @@ -126,8 +126,8 @@ class OpenLP(OpenLPMixin, QtGui.QApplication): alternate_rows_repair_stylesheet = \ 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' application_stylesheet += alternate_rows_repair_stylesheet - if os.name == 'nt': - application_stylesheet += NT_REPAIR_STYLESHEET + if is_win(): + application_stylesheet += WIN_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) show_splash = Settings().value('core/show splash') @@ -275,7 +275,7 @@ def main(args=None): # Throw the rest of the arguments at Qt, just in case. qt_args.extend(args) # Bug #1018855: Set the WM_CLASS property in X11 - if platform.system() not in ['Windows', 'Darwin']: + if not is_win() and not is_macosx(): qt_args.append('OpenLP') # Initialise the resources qInitResources() diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 22207dec4..0776547ae 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -127,6 +127,33 @@ def de_hump(name): sub_name = FIRST_CAMEL_REGEX.sub(r'\1_\2', name) return SECOND_CAMEL_REGEX.sub(r'\1_\2', sub_name).lower() + +def is_win(): + """ + Returns true if running on a system with a nt kernel e.g. Windows, Wine + + :return: True if system is running a nt kernel false otherwise + """ + return os.name.startswith('nt') + + +def is_macosx(): + """ + Returns true if running on a system with a darwin kernel e.g. Mac OS X + + :return: True if system is running a darwin kernel false otherwise + """ + return sys.platform.startswith('darwin') + + +def is_linux(): + """ + Returns true if running on a system with a linux kernel e.g. Ubuntu, Debian, etc + + :return: True if system is running a linux kernel false otherwise + """ + return sys.platform.startswith('linux') + from .openlpmixin import OpenLPMixin from .registry import Registry from .registrymixin import RegistryMixin diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 073d3c7f7..89f637e69 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -33,10 +33,10 @@ import logging import os import sys -from openlp.core.common import Settings +from openlp.core.common import Settings, is_win, is_macosx -if sys.platform != 'win32' and sys.platform != 'darwin': +if not is_win() and not is_macosx(): try: from xdg import BaseDirectory XDG_BASE_AVAILABLE = True @@ -145,13 +145,13 @@ def _get_os_dir_path(dir_type): directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources')) if os.path.exists(directory): return directory - if sys.platform == 'win32': + if is_win(): if dir_type == AppLocation.DataDir: return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data') elif dir_type == AppLocation.LanguageDir: return os.path.dirname(openlp.__file__) return os.path.join(str(os.getenv('APPDATA')), 'openlp') - elif sys.platform == 'darwin': + elif is_macosx(): if dir_type == AppLocation.DataDir: return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data') elif dir_type == AppLocation.LanguageDir: diff --git a/openlp/core/common/openlpmixin.py b/openlp/core/common/openlpmixin.py index 1c7fe7d5a..3e8a8926a 100644 --- a/openlp/core/common/openlpmixin.py +++ b/openlp/core/common/openlpmixin.py @@ -33,6 +33,7 @@ import logging import inspect from openlp.core.common import trace_error_handler + DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', 'preview_size_changed', 'resizeEvent'] @@ -41,11 +42,8 @@ class OpenLPMixin(object): """ Base Calling object for OpenLP classes. """ - def __init__(self, parent): - try: - super(OpenLPMixin, self).__init__(parent) - except TypeError: - super(OpenLPMixin, self).__init__() + def __init__(self, *args, **kwargs): + super(OpenLPMixin, self).__init__(*args, **kwargs) self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) if self.logger.getEffectiveLevel() == logging.DEBUG: for name, m in inspect.getmembers(self, inspect.ismethod): diff --git a/openlp/core/common/registryproperties.py b/openlp/core/common/registryproperties.py index 791fc33f7..e2cfffa09 100644 --- a/openlp/core/common/registryproperties.py +++ b/openlp/core/common/registryproperties.py @@ -29,9 +29,7 @@ """ Provide Registry values for adding to classes """ -import os - -from openlp.core.common import Registry +from openlp.core.common import Registry, is_win class RegistryProperties(object): @@ -45,7 +43,7 @@ class RegistryProperties(object): Adds the openlp to the class dynamically. Windows needs to access the application in a dynamic manner. """ - if os.name == 'nt': + if is_win(): return Registry().get('application') else: if not hasattr(self, '_application') or not self._application: diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 634bc5ced..f7202b590 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -36,7 +36,7 @@ import sys from PyQt4 import QtCore, QtGui -from openlp.core.common import ThemeLevel, SlideLimits, UiStrings +from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux log = logging.getLogger(__name__) @@ -44,7 +44,7 @@ log = logging.getLogger(__name__) # Fix for bug #1014422. X11_BYPASS_DEFAULT = True -if sys.platform.startswith('linux'): +if is_linux(): # Default to False on Gnome. X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID')) # Default to False on Xfce. @@ -86,7 +86,7 @@ class Settings(QtCore.QSettings): """ __default_settings__ = { 'advanced/add page break': False, - 'advanced/alternate rows': not sys.platform.startswith('win'), + 'advanced/alternate rows': not is_win(), 'advanced/current media plugin': -1, 'advanced/data path': '', 'advanced/default color': '#ffffff', diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 17d11ef63..ecd6ca5bd 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -111,6 +111,9 @@ class ItemCapabilities(object): ``CanEditTitle`` The capability to edit the title of the item + ``IsOptical`` + .Determines is the service_item is based on an optical device + """ CanPreview = 1 CanEdit = 2 @@ -129,6 +132,7 @@ class ItemCapabilities(object): HasBackgroundAudio = 15 CanAutoStartForLive = 16 CanEditTitle = 17 + IsOptical = 18 class ServiceItem(RegistryProperties): @@ -416,7 +420,10 @@ class ServiceItem(RegistryProperties): for text_image in service_item['serviceitem']['data']: if not self.title: self.title = text_image['title'] - if path: + if self.is_capable(ItemCapabilities.IsOptical): + self.has_original_files = False + self.add_from_command(text_image['path'], text_image['title'], text_image['image']) + elif path: self.has_original_files = False self.add_from_command(path, text_image['title'], text_image['image']) else: @@ -427,7 +434,8 @@ class ServiceItem(RegistryProperties): """ Returns the title of the service item. """ - if self.is_text() or ItemCapabilities.CanEditTitle in self.capabilities: + if self.is_text() or self.is_capable(ItemCapabilities.IsOptical) \ + or self.is_capable(ItemCapabilities.CanEditTitle): return self.title else: if len(self._raw_frames) > 1: @@ -495,7 +503,8 @@ class ServiceItem(RegistryProperties): """ Confirms if the ServiceItem uses a file """ - return self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command + return self.service_item_type == ServiceItemType.Image or \ + (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)) def is_text(self): """ @@ -553,7 +562,7 @@ class ServiceItem(RegistryProperties): frame = self._raw_frames[row] except IndexError: return '' - if self.is_image(): + if self.is_image() or self.is_capable(ItemCapabilities.IsOptical): path_from = frame['path'] else: path_from = os.path.join(frame['path'], frame['title']) @@ -623,12 +632,17 @@ class ServiceItem(RegistryProperties): self.is_valid = False break elif self.is_command(): - file_name = os.path.join(frame['path'], frame['title']) - if not os.path.exists(file_name): - self.is_valid = False - break - if suffix_list and not self.is_text(): - file_suffix = frame['title'].split('.')[-1] - if file_suffix.lower() not in suffix_list: + if self.is_capable(ItemCapabilities.IsOptical): + if not os.path.exists(frame['title']): self.is_valid = False break + else: + file_name = os.path.join(frame['path'], frame['title']) + if not os.path.exists(file_name): + self.is_valid = False + break + if suffix_list and not self.is_text(): + file_suffix = frame['title'].split('.')[-1] + if file_suffix.lower() not in suffix_list: + self.is_valid = False + break diff --git a/openlp/core/lib/toolbar.py b/openlp/core/lib/toolbar.py index b1cc7b249..b24be89a8 100644 --- a/openlp/core/lib/toolbar.py +++ b/openlp/core/lib/toolbar.py @@ -81,4 +81,4 @@ class OpenLPToolbar(QtGui.QToolBar): if handle in self.actions: self.actions[handle].setVisible(visible) else: - log.warn('No handle "%s" in actions list.', str(handle)) + log.warning('No handle "%s" in actions list.', str(handle)) diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index a1e37abcf..af4b263d3 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -33,7 +33,7 @@ import logging from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, UiStrings, translate +from openlp.core.common import Registry, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.utils.actions import ActionList @@ -172,7 +172,7 @@ def create_button(parent, name, **kwargs): kwargs.setdefault('icon', ':/services/service_down.png') kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.')) else: - log.warn('The role "%s" is not defined in create_push_button().', role) + log.warning('The role "%s" is not defined in create_push_button().', role) if kwargs.pop('btn_class', '') == 'toolbutton': button = QtGui.QToolButton(parent) else: @@ -190,7 +190,7 @@ def create_button(parent, name, **kwargs): button.clicked.connect(kwargs.pop('click')) for key in list(kwargs.keys()): if key not in ['text', 'icon', 'tooltip', 'click']: - log.warn('Parameter %s was not consumed in create_button().', key) + log.warning('Parameter %s was not consumed in create_button().', key) return button @@ -247,6 +247,8 @@ def create_action(parent, name, **kwargs): """ action = QtGui.QAction(parent) action.setObjectName(name) + if is_macosx(): + action.setIconVisibleInMenu(False) if kwargs.get('text'): action.setText(kwargs.pop('text')) if kwargs.get('icon'): @@ -275,7 +277,7 @@ def create_action(parent, name, **kwargs): action.triggered.connect(kwargs.pop('triggers')) for key in list(kwargs.keys()): if key not in ['text', 'icon', 'tooltip', 'statustip', 'checked', 'can_shortcuts', 'category', 'triggers']: - log.warn('Parameter %s was not consumed in create_action().' % key) + log.warning('Parameter %s was not consumed in create_action().' % key) return action diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index e0228a43b..65b858b75 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -38,7 +38,7 @@ import bs4 import sqlalchemy from lxml import etree -from openlp.core.common import RegistryProperties +from openlp.core.common import RegistryProperties, is_linux from PyQt4 import Qt, QtCore, QtGui, QtWebKit @@ -137,7 +137,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties): 'pyICU: %s\n' % ICU_VERSION + \ 'pyUNO bridge: %s\n' % self._pyuno_import() + \ 'VLC: %s\n' % VLC_VERSION - if platform.system() == 'Linux': + if is_linux(): if os.environ.get('KDE_FULL_SESSION') == 'true': system += 'Desktop: KDE SC\n' elif os.environ.get('GNOME_DESKTOP_SESSION_ID'): diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index d7c16f0d3..8599c8d35 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -387,17 +387,21 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): self.progress_bar.setValue(self.progress_bar.maximum()) if self.has_run_wizard: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the finish button to return to OpenLP.')) + 'Download complete. Click the %s button to return to OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the finish button to start OpenLP.')) + 'Download complete. Click the %s button to start OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: if self.has_run_wizard: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Click the finish button to return to OpenLP.')) + 'Click the %s button to return to OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Click the finish button to start OpenLP.')) + 'Click the %s button to start OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) self.finish_button.setVisible(True) self.finish_button.setEnabled(True) self.cancel_button.setVisible(False) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index ff1675ff5..c5098eda6 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -31,9 +31,7 @@ The UI widgets for the first time wizard. """ from PyQt4 import QtCore, QtGui -import sys - -from openlp.core.common import translate +from openlp.core.common import translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -64,9 +62,12 @@ class Ui_FirstTimeWizard(object): first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) first_time_wizard.resize(550, 386) first_time_wizard.setModal(True) - first_time_wizard.setWizardStyle(QtGui.QWizard.ModernStyle) first_time_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage | QtGui.QWizard.HaveCustomButton1) + if is_macosx(): + first_time_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, + QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + first_time_wizard.resize(634, 386) self.finish_button = self.button(QtGui.QWizard.FinishButton) self.no_internet_finish_button = self.button(QtGui.QWizard.CustomButton1) self.cancel_button = self.button(QtGui.QWizard.CancelButton) @@ -212,7 +213,8 @@ class Ui_FirstTimeWizard(object): translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard')) self.information_label.setText( translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. ' - 'Click the next button below to start.')) + 'Click the %s button below to start.') % + self.buttonText(QtGui.QWizard.NextButton)) self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins')) self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. ')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) @@ -236,7 +238,7 @@ class Ui_FirstTimeWizard(object): 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.') self.cancelWizardText = translate('OpenLP.FirstTimeWizard', '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), ' - 'click the Cancel button now.') + 'click the %s button now.') % self.buttonText(QtGui.QWizard.CancelButton) self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs')) self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index 4f3d5d251..96e25c27d 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -60,6 +60,12 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont """ super(FormattingTagForm, self).__init__(parent) self.setupUi(self) + self._setup() + + def _setup(self): + """ + Set up the class. This method is mocked out by the tests. + """ self.services = FormattingTagController() self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected) self.new_button.clicked.connect(self.on_new_clicked) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 5c905c972..f9f00a235 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -43,7 +43,7 @@ import sys from PyQt4 import QtCore, QtGui, QtWebKit, QtOpenGL from PyQt4.phonon import Phonon -from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, Settings, translate +from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, Settings, translate, is_macosx from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte from openlp.core.lib.theme import BackgroundType @@ -74,7 +74,7 @@ class Display(QtGui.QGraphicsView): # OpenGL. Only white blank screen is shown on the 2nd monitor all the # time. We need to investigate more how to use OpenGL properly on Mac OS # X. - if sys.platform != 'darwin': + if not is_macosx(): self.setViewport(QtOpenGL.QGLWidget()) def setup(self): @@ -143,7 +143,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): # on Mac OS X. For next OpenLP version we should test it on other # platforms. For OpenLP 2.0 keep it only for OS X to not cause any # regressions on other platforms. - if sys.platform == 'darwin': + if is_macosx(): window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window # For primary screen ensure it stays above the OS X dock # and menu bar diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 6894293ce..77a903c5f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -41,7 +41,8 @@ from datetime import datetime from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, translate +from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, translate, \ + is_win, is_macosx from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \ build_icon from openlp.core.lib.ui import UiStrings, create_action @@ -91,6 +92,8 @@ class Ui_MainWindow(object): main_window.setObjectName('MainWindow') main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) main_window.setDockNestingEnabled(True) + if is_macosx(): + main_window.setDocumentMode(True) # Set up the main container, which contains all the other form widgets. self.main_content = QtGui.QWidget(main_window) self.main_content.setObjectName('main_content') @@ -117,10 +120,12 @@ class Ui_MainWindow(object): self.recent_files_menu = QtGui.QMenu(self.file_menu) self.recent_files_menu.setObjectName('recentFilesMenu') self.file_import_menu = QtGui.QMenu(self.file_menu) - self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) + if not is_macosx(): + self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) self.file_import_menu.setObjectName('file_import_menu') self.file_export_menu = QtGui.QMenu(self.file_menu) - self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) + if not is_macosx(): + self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) self.file_export_menu.setObjectName('file_export_menu') # View Menu self.view_menu = QtGui.QMenu(self.menu_bar) @@ -289,7 +294,7 @@ class Ui_MainWindow(object): triggers=self.on_about_item_clicked) # Give QT Extra Hint that this is an About Menu Item self.about_item.setMenuRole(QtGui.QAction.AboutRole) - if os.name == 'nt': + if is_win(): self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'OpenLP.chm') self.offline_help_item = create_action(main_window, 'offlineHelpItem', icon=':/system/system_help_contents.png', @@ -323,7 +328,7 @@ class Ui_MainWindow(object): # Qt on OS X looks for keywords in the menu items title to determine which menu items get added to the main # menu. If we are running on Mac OS X the menu items whose title contains those keywords but don't belong in the # main menu need to be marked as such with QAction.NoRole. - if sys.platform == 'darwin': + if is_macosx(): self.settings_shortcuts_item.setMenuRole(QtGui.QAction.NoRole) self.formatting_tag_item.setMenuRole(QtGui.QAction.NoRole) add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(), @@ -332,7 +337,7 @@ class Ui_MainWindow(object): add_actions(self.tools_menu, (self.tools_open_data_folder, None)) add_actions(self.tools_menu, (self.tools_first_time_wizard, None)) add_actions(self.tools_menu, [self.update_theme_images]) - if os.name == 'nt': + if is_win(): add_actions(self.help_menu, (self.offline_help_item, self.on_line_help_item, None, self.web_site_item, self.about_item)) else: @@ -426,7 +431,7 @@ class Ui_MainWindow(object): self.settings_plugin_list_item.setStatusTip(translate('OpenLP.MainWindow', 'List the Plugins')) self.about_item.setText(translate('OpenLP.MainWindow', '&About')) self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP')) - if os.name == 'nt': + if is_win(): self.offline_help_item.setText(translate('OpenLP.MainWindow', '&User Guide')) self.on_line_help_item.setText(translate('OpenLP.MainWindow', '&Online Help')) self.search_shortcut_action.setText(UiStrings().Search) @@ -1073,7 +1078,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): if self.live_controller.display: self.live_controller.display.close() self.live_controller.display = None - if os.name == 'nt': + if is_win(): # Needed for Windows to stop crashes on exit Registry().remove('application') diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index b2b6ab0b8..a44604d2f 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -72,6 +72,9 @@ class MediaInfo(object): length = 0 start_time = 0 end_time = 0 + title_track = 0 + audio_track = 0 + subtitle_track = 0 media_type = MediaType() @@ -107,6 +110,40 @@ def set_media_players(players_list, overridden_player='auto'): players = players.replace(overridden_player, '[%s]' % overridden_player) Settings().setValue('media/players', players) + +def parse_optical_path(input): + """ + Split the optical path info. + + :param input: The string to parse + :return: The elements extracted from the string: filename, title, audio_track, subtitle_track, start, end + """ + log.debug('parse_optical_path, about to parse: "%s"' % input) + clip_info = input.split(sep=':') + title = int(clip_info[1]) + audio_track = int(clip_info[2]) + subtitle_track = int(clip_info[3]) + start = float(clip_info[4]) + end = float(clip_info[5]) + clip_name = clip_info[6] + filename = clip_info[7] + # Windows path usually contains a colon after the drive letter + if len(clip_info) > 8: + filename += ':' + clip_info[8] + return filename, title, audio_track, subtitle_track, start, end, clip_name + + +def format_milliseconds(milliseconds): + """ + Format milliseconds into a human readable time string. + :param milliseconds: Milliseconds to format + :return: Time string in format: hh.mm.ss,ttt + """ + seconds, millis = divmod(milliseconds, 1000) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis) + from .mediacontroller import MediaController from .playertab import PlayerTab diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 596b618cb..4a2d475c1 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -36,9 +36,10 @@ import datetime from PyQt4 import QtCore, QtGui from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate -from openlp.core.lib import OpenLPToolbar +from openlp.core.lib import OpenLPToolbar, ItemCapabilities from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players +from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ + parse_optical_path from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.common import AppLocation from openlp.core.ui import DisplayControllerType @@ -175,7 +176,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): # On some platforms importing vlc.py might cause # also OSError exceptions. (e.g. Mac OS X) except (ImportError, OSError): - log.warn('Failed to import %s on path %s', module_name, path) + log.warning('Failed to import %s on path %s', module_name, path) player_classes = MediaPlayer.__subclasses__() for player_class in player_classes: player = player_class(self) @@ -368,7 +369,16 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path()) display = self._define_display(controller) if controller.is_live: - is_valid = self._check_file_type(controller, display, service_item) + # if this is an optical device use special handling + if service_item.is_capable(ItemCapabilities.IsOptical): + log.debug('video is optical and live') + path = service_item.get_frame_path() + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, + controller) + else: + log.debug('video is not optical and live') + is_valid = self._check_file_type(controller, display, service_item) display.override['theme'] = '' display.override['video'] = True if controller.media_info.is_background: @@ -379,12 +389,21 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): controller.media_info.start_time = service_item.start_time controller.media_info.end_time = service_item.end_time elif controller.preview_display: - is_valid = self._check_file_type(controller, display, service_item) + if service_item.is_capable(ItemCapabilities.IsOptical): + log.debug('video is optical and preview') + path = service_item.get_frame_path() + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, + controller) + else: + log.debug('video is not optical and preview') + is_valid = self._check_file_type(controller, display, service_item) if not is_valid: # Media could not be loaded correctly critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'), translate('MediaPlugin.MediaItem', 'Unsupported File')) return False + log.debug('video mediatype: ' + str(controller.media_info.media_type)) # dont care about actual theme, set a black background if controller.is_live and not controller.media_info.is_background: display.frame.evaluateJavaScript('show_video( "setBackBoard", null, null, null,"visible");') @@ -436,6 +455,62 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): log.debug('use %s controller' % self.current_media_players[controller.controller_type]) return True + def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller): + """ + Setup playback of optical media + + :param filename: Path of the optical device/drive. + :param title: The main/title track to play. + :param audio_track: The audio track to play. + :param subtitle_track: The subtitle track to play. + :param start: Start position in miliseconds. + :param end: End position in miliseconds. + :param display: The display to play the media. + :param controller: The media contraoller. + :return: True if setup succeded else False. + """ + log.debug('media_setup_optical') + if controller is None: + controller = self.display_controllers[DisplayControllerType.Plugin] + # stop running videos + self.media_reset(controller) + # Setup media info + controller.media_info = MediaInfo() + controller.media_info.file_info = QtCore.QFileInfo(filename) + if audio_track == -1 and subtitle_track == -1: + controller.media_info.media_type = MediaType.CD + else: + controller.media_info.media_type = MediaType.DVD + controller.media_info.start_time = start/1000 + controller.media_info.end_time = end/1000 + controller.media_info.length = (end - start)/1000 + controller.media_info.title_track = title + controller.media_info.audio_track = audio_track + controller.media_info.subtitle_track = subtitle_track + # When called from mediaitem display is None + if display is None: + display = controller.preview_display + # Find vlc player + used_players = get_media_players()[0] + vlc_player = None + for title in used_players: + player = self.media_players[title] + if player.name == 'vlc': + vlc_player = player + if vlc_player is None: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC player required'), + translate('MediaPlugin.MediaItem', + 'VLC player required for playback of optical devices')) + return False + vlc_player.load(display) + self.resize(display, vlc_player) + self.current_media_players[controller.controller_type] = vlc_player + if audio_track == -1 and subtitle_track == -1: + controller.media_info.media_type = MediaType.CD + else: + controller.media_info.media_type = MediaType.DVD + return True + def _check_file_type(self, controller, display, service_item): """ Select the correct media Player type from the prioritized Player list diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index d02526b0e..7e8acc318 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -38,9 +38,9 @@ import threading from PyQt4 import QtGui -from openlp.core.common import Settings +from openlp.core.common import Settings, is_win, is_macosx from openlp.core.lib import translate -from openlp.core.ui.media import MediaState +from openlp.core.ui.media import MediaState, MediaType from openlp.core.ui.media.mediaplayer import MediaPlayer log = logging.getLogger(__name__) @@ -52,7 +52,7 @@ try: except (ImportError, NameError, NotImplementedError): pass except OSError as e: - if sys.platform.startswith('win'): + if is_win(): if not isinstance(e, WindowsError) and e.winerror != 126: raise else: @@ -139,9 +139,9 @@ class VlcPlayer(MediaPlayer): # You have to give the id of the QFrame (or similar object) # to vlc, different platforms have different functions for this. win_id = int(display.vlc_widget.winId()) - if sys.platform == "win32": + if is_win(): display.vlc_media_player.set_hwnd(win_id) - elif sys.platform == "darwin": + elif is_macosx(): # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa # framework and not the old Carbon. display.vlc_media_player.set_nsobject(win_id) @@ -166,7 +166,19 @@ class VlcPlayer(MediaPlayer): file_path = str(controller.media_info.file_info.absoluteFilePath()) path = os.path.normcase(file_path) # create the media - display.vlc_media = display.vlc_instance.media_new_path(path) + if controller.media_info.media_type == MediaType.CD: + display.vlc_media = display.vlc_instance.media_new_location('cdda://' + path) + display.vlc_media_player.set_media(display.vlc_media) + display.vlc_media_player.play() + # Wait for media to start playing. In this case VLC actually returns an error. + self.media_state_wait(display, vlc.State.Playing) + # If subitems exists, this is a CD + audio_cd_tracks = display.vlc_media.subitems() + if not audio_cd_tracks or audio_cd_tracks.count() < 1: + return False + display.vlc_media = audio_cd_tracks.item_at_index(controller.media_info.title_track) + else: + display.vlc_media = display.vlc_instance.media_new_path(path) # put the media in the media player display.vlc_media_player.set_media(display.vlc_media) # parse the metadata of the file @@ -206,15 +218,40 @@ class VlcPlayer(MediaPlayer): """ controller = display.controller start_time = 0 + log.debug('vlc play') if self.state != MediaState.Paused and controller.media_info.start_time > 0: start_time = controller.media_info.start_time threading.Thread(target=display.vlc_media_player.play).start() if not self.media_state_wait(display, vlc.State.Playing): return False + if self.state != MediaState.Paused and controller.media_info.start_time > 0: + log.debug('vlc play, starttime set') + start_time = controller.media_info.start_time + log.debug('mediatype: ' + str(controller.media_info.media_type)) + # Set tracks for the optical device + if controller.media_info.media_type == MediaType.DVD: + log.debug('vlc play, playing started') + if controller.media_info.title_track > 0: + log.debug('vlc play, title_track set: ' + str(controller.media_info.title_track)) + display.vlc_media_player.set_title(controller.media_info.title_track) + display.vlc_media_player.play() + if not self.media_state_wait(display, vlc.State.Playing): + return False + if controller.media_info.audio_track > 0: + display.vlc_media_player.audio_set_track(controller.media_info.audio_track) + log.debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track)) + if controller.media_info.subtitle_track > 0: + display.vlc_media_player.video_set_spu(controller.media_info.subtitle_track) + log.debug('vlc play, subtitle_track set: ' + str(controller.media_info.subtitle_track)) + if controller.media_info.start_time > 0: + log.debug('vlc play, starttime set: ' + str(controller.media_info.start_time)) + start_time = controller.media_info.start_time + controller.media_info.length = controller.media_info.end_time - controller.media_info.start_time + else: + controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) self.volume(display, controller.media_info.volume) - if start_time > 0: - self.seek(display, controller.media_info.start_time * 1000) - controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) + if start_time > 0 and display.vlc_media_player.is_seekable(): + display.vlc_media_player.set_time(int(start_time * 1000)) controller.seek_slider.setMaximum(controller.media_info.length * 1000) self.state = MediaState.Playing display.vlc_widget.raise_() @@ -248,6 +285,9 @@ class VlcPlayer(MediaPlayer): """ Go to a particular position """ + if display.controller.media_info.media_type == MediaType.CD \ + or display.controller.media_info.media_type == MediaType.DVD: + seek_value += int(display.controller.media_info.start_time * 1000) if display.vlc_media_player.is_seekable(): display.vlc_media_player.set_time(seek_value) @@ -280,7 +320,12 @@ class VlcPlayer(MediaPlayer): self.set_visible(display, False) if not controller.seek_slider.isSliderDown(): controller.seek_slider.blockSignals(True) - controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) + if display.controller.media_info.media_type == MediaType.CD \ + or display.controller.media_info.media_type == MediaType.DVD: + controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time() - + int(display.controller.media_info.start_time * 1000)) + else: + controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) controller.seek_slider.blockSignals(False) def get_info(self): diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index bda52c807..50200313f 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -31,7 +31,7 @@ The Create/Edit theme wizard """ from PyQt4 import QtCore, QtGui -from openlp.core.common import UiStrings, translate +from openlp.core.common import UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets @@ -41,19 +41,21 @@ class Ui_ThemeWizard(object): """ The Create/Edit theme wizard """ - def setupUi(self, themeWizard): + def setupUi(self, theme_wizard): """ Set up the UI """ - themeWizard.setObjectName('OpenLP.ThemeWizard') - themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) - themeWizard.setModal(True) - themeWizard.setWizardStyle(QtGui.QWizard.ModernStyle) - themeWizard.setOptions(QtGui.QWizard.IndependentPages | - QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) + theme_wizard.setObjectName('OpenLP.ThemeWizard') + theme_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + theme_wizard.setModal(True) + theme_wizard.setOptions(QtGui.QWizard.IndependentPages | + QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) + if is_macosx(): + theme_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + theme_wizard.resize(646, 400) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) # Welcome Page - add_welcome_page(themeWizard, ':/wizards/wizard_createtheme.bmp') + add_welcome_page(theme_wizard, ':/wizards/wizard_createtheme.bmp') # Background Page self.background_page = QtGui.QWizardPage() self.background_page.setObjectName('background_page') @@ -137,7 +139,7 @@ class Ui_ThemeWizard(object): self.transparent_layout.setObjectName('Transparent_layout') self.background_stack.addWidget(self.transparent_widget) self.background_layout.addLayout(self.background_stack) - themeWizard.addPage(self.background_page) + theme_wizard.addPage(self.background_page) # Main Area Page self.main_area_page = QtGui.QWizardPage() self.main_area_page.setObjectName('main_area_page') @@ -218,7 +220,7 @@ class Ui_ThemeWizard(object): self.shadow_size_spin_box.setObjectName('shadow_size_spin_box') self.shadow_layout.addWidget(self.shadow_size_spin_box) self.main_area_layout.addRow(self.shadow_check_box, self.shadow_layout) - themeWizard.addPage(self.main_area_page) + theme_wizard.addPage(self.main_area_page) # Footer Area Page self.footer_area_page = QtGui.QWizardPage() self.footer_area_page.setObjectName('footer_area_page') @@ -242,7 +244,7 @@ class Ui_ThemeWizard(object): self.footer_size_spin_box.setObjectName('FooterSizeSpinBox') self.footer_area_layout.addRow(self.footer_size_label, self.footer_size_spin_box) self.footer_area_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) - themeWizard.addPage(self.footer_area_page) + theme_wizard.addPage(self.footer_area_page) # Alignment Page self.alignment_page = QtGui.QWizardPage() self.alignment_page.setObjectName('alignment_page') @@ -264,7 +266,7 @@ class Ui_ThemeWizard(object): self.transitions_check_box.setObjectName('transitions_check_box') self.alignment_layout.addRow(self.transitions_label, self.transitions_check_box) self.alignment_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) - themeWizard.addPage(self.alignment_page) + theme_wizard.addPage(self.alignment_page) # Area Position Page self.area_position_page = QtGui.QWizardPage() self.area_position_page.setObjectName('area_position_page') @@ -334,7 +336,7 @@ class Ui_ThemeWizard(object): self.footer_height_spin_box.setObjectName('footer_height_spin_box') self.footer_position_layout.addRow(self.footer_height_label, self.footer_height_spin_box) self.area_position_layout.addWidget(self.footer_position_group_box) - themeWizard.addPage(self.area_position_page) + theme_wizard.addPage(self.area_position_page) # Preview Page self.preview_page = QtGui.QWizardPage() self.preview_page.setObjectName('preview_page') @@ -362,8 +364,8 @@ class Ui_ThemeWizard(object): self.preview_box_label.setObjectName('preview_box_label') self.preview_area_layout.addWidget(self.preview_box_label) self.preview_layout.addWidget(self.preview_area) - themeWizard.addPage(self.preview_page) - self.retranslateUi(themeWizard) + theme_wizard.addPage(self.preview_page) + self.retranslateUi(theme_wizard) QtCore.QObject.connect(self.background_combo_box, QtCore.SIGNAL('currentIndexChanged(int)'), self.background_stack, QtCore.SLOT('setCurrentIndex(int)')) QtCore.QObject.connect(self.outline_check_box, QtCore.SIGNAL('toggled(bool)'), self.outline_color_button, @@ -391,11 +393,11 @@ class Ui_ThemeWizard(object): QtCore.QObject.connect(self.footer_position_check_box, QtCore.SIGNAL('toggled(bool)'), self.footer_height_spin_box, QtCore.SLOT('setDisabled(bool)')) - def retranslateUi(self, themeWizard): + def retranslateUi(self, theme_wizard): """ Translate the UI on the fly """ - themeWizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) + theme_wizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) self.title_label.setText('%s' % translate('OpenLP.ThemeWizard', 'Welcome to the Theme Wizard')) self.information_label.setText( @@ -484,8 +486,8 @@ class Ui_ThemeWizard(object): self.footer_height_label.setText(translate('OpenLP.ThemeWizard', 'Height:')) self.footer_height_spin_box.setSuffix(translate('OpenLP.ThemeWizard', 'px')) self.footer_position_check_box.setText(translate('OpenLP.ThemeWizard', 'Use default location')) - themeWizard.setOption(QtGui.QWizard.HaveCustomButton1, False) - themeWizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) + theme_wizard.setOption(QtGui.QWizard.HaveCustomButton1, False) + theme_wizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) self.preview_page.setTitle(translate('OpenLP.ThemeWizard', 'Preview and Save')) self.preview_page.setSubTitle(translate('OpenLP.ThemeWizard', 'Preview the theme and save it.')) self.theme_name_label.setText(translate('OpenLP.ThemeWizard', 'Theme name:')) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 23bc0a9e1..7199d1742 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -34,7 +34,7 @@ import os from PyQt4 import QtGui -from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate +from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -121,9 +121,10 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): """ self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) self.setModal(True) - self.setWizardStyle(QtGui.QWizard.ModernStyle) self.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage) + if is_macosx(): + self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) add_welcome_page(self, image) self.add_custom_pages() if self.with_progress_page: diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 9b024eb84..983c0cfd0 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -44,10 +44,10 @@ from random import randint from PyQt4 import QtGui, QtCore -from openlp.core.common import Registry, AppLocation, Settings +from openlp.core.common import Registry, AppLocation, Settings, is_win, is_macosx -if sys.platform != 'win32' and sys.platform != 'darwin': +if not is_win() and not is_macosx(): try: from xdg import BaseDirectory XDG_BASE_AVAILABLE = True @@ -109,6 +109,22 @@ class VersionThread(QtCore.QThread): Registry().execute('openlp_version_check', '%s' % version) +class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): + """ + Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248 + (Redirecting to urls with special chars) + """ + def redirect_request(self, req, fp, code, msg, headers, newurl): + # Test if the newurl can be decoded to ascii + try: + test_url = newurl.encode('latin1').decode('ascii') + fixed_url = newurl + except Exception: + # The url could not be decoded to ascii, so we do some url encoding + fixed_url = urllib.parse.quote(newurl.encode('latin1').decode('utf-8', 'replace'), safe='/:') + return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) + + def get_application_version(): """ Returns the application version of the running instance of OpenLP:: @@ -341,6 +357,9 @@ def get_web_page(url, header=None, update_openlp=False): # http://docs.python.org/library/urllib2.html if not url: return None + # This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437 + opener = urllib.request.build_opener(HTTPRedirectHandlerFixed()) + urllib.request.install_opener(opener) req = urllib.request.Request(url) if not header or header[0].lower() != 'user-agent': user_agent = _get_user_agent() diff --git a/openlp/core/utils/actions.py b/openlp/core/utils/actions.py index d81e16b2e..9b5117233 100644 --- a/openlp/core/utils/actions.py +++ b/openlp/core/utils/actions.py @@ -279,8 +279,8 @@ class ActionList(object): actions.append(action) ActionList.shortcut_map[shortcuts[1]] = actions else: - log.warn('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % - (shortcuts[1], action.objectName())) + log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % + (shortcuts[1], action.objectName())) shortcuts.remove(shortcuts[1]) # Check the primary shortcut. existing_actions = ActionList.shortcut_map.get(shortcuts[0], []) @@ -290,8 +290,8 @@ class ActionList(object): actions.append(action) ActionList.shortcut_map[shortcuts[0]] = actions else: - log.warn('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % - (shortcuts[0], action.objectName())) + log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % + (shortcuts[0], action.objectName())) shortcuts.remove(shortcuts[0]) action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts]) diff --git a/openlp/core/utils/languagemanager.py b/openlp/core/utils/languagemanager.py index dd048e04c..3c741e58f 100644 --- a/openlp/core/utils/languagemanager.py +++ b/openlp/core/utils/languagemanager.py @@ -35,7 +35,7 @@ import sys from PyQt4 import QtCore, QtGui -from openlp.core.common import AppLocation, Settings, translate +from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx log = logging.getLogger(__name__) @@ -60,7 +60,7 @@ class LanguageManager(object): app_translator = QtCore.QTranslator() app_translator.load(language, lang_path) # A translator for buttons and other default strings provided by Qt. - if sys.platform != 'win32' and sys.platform != 'darwin': + if not is_win() and not is_macosx(): lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath) default_translator = QtCore.QTranslator() default_translator.load('qt_%s' % language, lang_path) diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index 09c0942b7..2b0b57695 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -423,7 +423,7 @@ class BibleUpgradeForm(OpenLPWizard): else: language_id = self.new_bibles[number].get_language(name) if not language_id: - log.warn('Upgrading from "%s" failed' % filename[0]) + log.warning('Upgrading from "%s" failed' % filename[0]) self.new_bibles[number].session.close() del self.new_bibles[number] self.increment_progress_bar( @@ -444,7 +444,7 @@ class BibleUpgradeForm(OpenLPWizard): book_ref_id = self.new_bibles[number].\ get_book_ref_id_by_name(book, len(books), language_id) if not book_ref_id: - log.warn('Upgrading books from %s - download name: "%s" aborted by user' % ( + log.warning('Upgrading books from %s - download name: "%s" aborted by user' % ( meta_data['download_source'], meta_data['download_name'])) self.new_bibles[number].session.close() del self.new_bibles[number] @@ -457,7 +457,7 @@ class BibleUpgradeForm(OpenLPWizard): if oldbook: verses = old_bible.get_verses(oldbook['id']) if not verses: - log.warn('No verses found to import for book "%s"', book) + log.warning('No verses found to import for book "%s"', book) continue for verse in verses: if self.stop_import_flag: @@ -472,7 +472,7 @@ class BibleUpgradeForm(OpenLPWizard): if not language_id: language_id = self.new_bibles[number].get_language(name) if not language_id: - log.warn('Upgrading books from "%s" failed' % name) + log.warning('Upgrading books from "%s" failed' % name) self.new_bibles[number].session.close() del self.new_bibles[number] self.increment_progress_bar( @@ -493,7 +493,7 @@ class BibleUpgradeForm(OpenLPWizard): (number + 1, max_bibles, name, book['name'])) book_ref_id = self.new_bibles[number].get_book_ref_id_by_name(book['name'], len(books), language_id) if not book_ref_id: - log.warn('Upgrading books from %s " failed - aborted by user' % name) + log.warning('Upgrading books from %s " failed - aborted by user' % name) self.new_bibles[number].session.close() del self.new_bibles[number] self.success[number] = False @@ -503,7 +503,7 @@ class BibleUpgradeForm(OpenLPWizard): book_details['testament_id']) verses = old_bible.get_verses(book['id']) if not verses: - log.warn('No verses found to import for book "%s"', book['name']) + log.warning('No verses found to import for book "%s"', book['name']) self.new_bibles[number].delete_book(db_book) continue for verse in verses: diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 6b26dfabe..37c858ab1 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -32,7 +32,6 @@ The :mod:`http` module enables OpenLP to retrieve scripture from bible websites. import logging import re import socket -import urllib.request import urllib.parse import urllib.error from html.parser import HTMLParseError @@ -165,7 +164,7 @@ class BGExtract(RegistryProperties): if len(verse_parts) > 1: verse = int(verse_parts[0]) except TypeError: - log.warn('Illegal verse number: %s', str(verse)) + log.warning('Illegal verse number: %s', str(verse)) verses.append((verse, text)) verse_list = {} for verse, text in verses[::-1]: @@ -198,7 +197,7 @@ class BGExtract(RegistryProperties): if len(verse_parts) > 1: clean_verse_num = int(verse_parts[0]) except TypeError: - log.warn('Illegal verse number: %s', str(raw_verse_num)) + log.warning('Illegal verse number: %s', str(raw_verse_num)) if clean_verse_num: verse_text = raw_verse_num.next_element part = raw_verse_num.next_element.next_element diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index fa8323d7f..ac8061322 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -30,7 +30,7 @@ import logging from lxml import etree, objectify -from openlp.core.common import translate +from openlp.core.common import translate, trace_error_handler from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB @@ -88,6 +88,7 @@ class OpenSongBible(BibleDB): 'Incorrect Bible file type supplied. This looks like a Zefania XML bible, ' 'please use the Zefania import option.')) return False + # No language info in the opensong format, so ask the user language_id = self.get_language(bible_name) if not language_id: log.error('Importing books from "%s" failed' % self.filename) @@ -123,7 +124,7 @@ class OpenSongBible(BibleDB): if len(verse_parts) > 1: number = int(verse_parts[0]) except TypeError: - log.warn('Illegal verse number: %s', str(verse.attrib['n'])) + log.warning('Illegal verse number: %s', str(verse.attrib['n'])) verse_number = number else: verse_number += 1 @@ -134,6 +135,7 @@ class OpenSongBible(BibleDB): self.session.commit() self.application.process_events() except etree.XMLSyntaxError as inst: + trace_error_handler(log) critical_error_message_box( message=translate('BiblesPlugin.OpenSongImport', 'Incorrect Bible file type supplied. OpenSong Bibles may be ' diff --git a/openlp/plugins/bibles/lib/osis.py b/openlp/plugins/bibles/lib/osis.py index 4f85bef1a..9f0bb3801 100644 --- a/openlp/plugins/bibles/lib/osis.py +++ b/openlp/plugins/bibles/lib/osis.py @@ -27,14 +27,12 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import os import logging -import chardet -import codecs -import re +from lxml import etree -from openlp.core.common import AppLocation, translate +from openlp.core.common import translate, trace_error_handler from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB +from openlp.core.lib.ui import critical_error_message_box log = logging.getLogger(__name__) @@ -53,143 +51,151 @@ class OSISBible(BibleDB): log.debug(self.__class__.__name__) BibleDB.__init__(self, parent, **kwargs) self.filename = kwargs['filename'] - self.language_regex = re.compile(r'(.*?)') - self.verse_regex = re.compile(r'(.*?)') - self.note_regex = re.compile(r'(.*?)') - self.title_regex = re.compile(r'(.*?)') - self.milestone_regex = re.compile(r'') - self.fi_regex = re.compile(r'(.*?)') - self.rf_regex = re.compile(r'(.*?)') - self.lb_regex = re.compile(r'') - self.lg_regex = re.compile(r'') - self.l_regex = re.compile(r'') - self.w_regex = re.compile(r'') - self.q_regex = re.compile(r'') - self.q1_regex = re.compile(r'') - self.q2_regex = re.compile(r'') - self.trans_regex = re.compile(r'(.*?)') - self.divine_name_regex = re.compile(r'(.*?)') - self.spaces_regex = re.compile(r'([ ]{2,})') - filepath = os.path.join( - AppLocation.get_directory(AppLocation.PluginsDir), 'bibles', 'resources', 'osisbooks.csv') def do_import(self, bible_name=None): """ Loads a Bible from file. """ log.debug('Starting OSIS import from "%s"' % self.filename) - detect_file = None - db_book = None - osis = None + if not isinstance(self.filename, str): + self.filename = str(self.filename, 'utf8') + import_file = None success = True - last_chapter = 0 - match_count = 0 - self.wizard.increment_progress_bar( - translate('BiblesPlugin.OsisImport', 'Detecting encoding (this may take a few minutes)...')) try: - detect_file = open(self.filename, 'r') - details = chardet.detect(detect_file.read(1048576)) - detect_file.seek(0) - lines_in_file = int(len(detect_file.readlines())) - except IOError: - log.exception('Failed to detect OSIS file encoding') - return - finally: - if detect_file: - detect_file.close() - try: - osis = codecs.open(self.filename, 'r', details['encoding']) - repl = replacement - language_id = False - # Decide if the bible probably contains only NT or AT and NT or - # AT, NT and Apocrypha - if lines_in_file < 11500: - book_count = 27 - chapter_count = 260 - elif lines_in_file < 34200: - book_count = 66 - chapter_count = 1188 - else: - book_count = 67 - chapter_count = 1336 - for file_record in osis: + # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding + # detection, and the two mechanisms together interfere with each other. + import_file = open(self.filename, 'rb') + osis_bible_tree = etree.parse(import_file) + namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'} + # Find bible language + language_id = None + language = osis_bible_tree.xpath("//ns:osisText/@xml:lang", namespaces=namespace) + if language: + language_id = BiblesResourcesDB.get_language(language[0]) + # The language couldn't be detected, ask the user + if not language_id: + language_id = self.get_language(bible_name) + if not language_id: + log.error('Importing books from "%s" failed' % self.filename) + return False + num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=namespace)) + self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', + 'Removing unused tags (this may take a few minutes)...')) + # We strip unused tags from the XML, this should leave us with only chapter, verse and div tags. + # Strip tags we don't use - remove content + etree.strip_elements(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}note', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}milestone', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}title', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}abbr', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}catchWord', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}index', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdgGroup', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}figure'), + with_tail=False) + # Strip tags we don't use - keep content + etree.strip_tags(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}p', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}l', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}lg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}q', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}a', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}w', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}divineName', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}foreign', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}hi', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}inscription', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}mentioned', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}name', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}reference', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}seg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}transChange', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}salute', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}signed', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}closer', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}speech', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}speaker', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}list', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}item', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}table', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}head', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}row', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}cell', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}caption')) + # Precompile a few xpath-querys + verse_in_chapter = etree.XPath('count(//ns:chapter[1]/ns:verse)', namespaces=namespace) + text_in_verse = etree.XPath('count(//ns:verse[1]/text())', namespaces=namespace) + # Find books in the bible + bible_books = osis_bible_tree.xpath("//ns:div[@type='book']", namespaces=namespace) + for book in bible_books: if self.stop_import_flag: break - # Try to find the bible language - if not language_id: - language_match = self.language_regex.search(file_record) - if language_match: - language = BiblesResourcesDB.get_language( - language_match.group(1)) - if language: - language_id = language['id'] - self.save_meta('language_id', language_id) - continue - match = self.verse_regex.search(file_record) - if match: - # Set meta language_id if not detected till now - if not language_id: - language_id = self.get_language(bible_name) - if not language_id: - log.error('Importing books from "%s" failed' % self.filename) - return False - match_count += 1 - book = str(match.group(1)) - chapter = int(match.group(2)) - verse = int(match.group(3)) - verse_text = match.group(4) - book_ref_id = self.get_book_ref_id_by_name(book, book_count, language_id) - if not book_ref_id: - log.error('Importing books from "%s" failed' % self.filename) - return False - book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) - if not db_book or db_book.name != book_details['name']: - log.debug('New book: "%s"' % book_details['name']) - db_book = self.create_book( - book_details['name'], - book_ref_id, - book_details['testament_id']) - if last_chapter == 0: - self.wizard.progress_bar.setMaximum(chapter_count) - if last_chapter != chapter: - if last_chapter != 0: - self.session.commit() + # Remove div-tags in the book + etree.strip_tags(book, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}div')) + book_ref_id = self.get_book_ref_id_by_name(book.get('osisID'), num_books) + if not book_ref_id: + book_ref_id = self.get_book_ref_id_by_localised_name(book.get('osisID')) + if not book_ref_id: + log.error('Importing books from "%s" failed' % self.filename) + return False + book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) + db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id']) + # Find out if chapter-tags contains the verses, or if it is used as milestone/anchor + if int(verse_in_chapter(book)) > 0: + # The chapter tags contains the verses + for chapter in book: + chapter_number = chapter.get("osisID").split('.')[1] + # Find out if verse-tags contains the text, or if it is used as milestone/anchor + if int(text_in_verse(chapter)) == 0: + # verse-tags are used as milestone + for verse in chapter: + # If this tag marks the start of a verse, the verse text is between this tag and + # the next tag, which the "tail" attribute gives us. + if verse.get('sID'): + verse_number = verse.get("osisID").split('.')[2] + verse_text = verse.tail + if verse_text: + self.create_verse(db_book.id, chapter_number, verse_number, verse_text.strip()) + else: + # Verse-tags contains the text + for verse in chapter: + verse_number = verse.get("osisID").split('.')[2] + self.create_verse(db_book.id, chapter_number, verse_number, verse.text.strip()) self.wizard.increment_progress_bar( - translate('BiblesPlugin.OsisImport', 'Importing %s %s...', - 'Importing ...') % (book_details['name'], chapter)) - last_chapter = chapter - # All of this rigmarole below is because the mod2osis tool from the Sword library embeds XML in the - # OSIS but neglects to enclose the verse text (with XML) in <[CDATA[ ]]> tags. - verse_text = self.note_regex.sub('', verse_text) - verse_text = self.title_regex.sub('', verse_text) - verse_text = self.milestone_regex.sub('', verse_text) - verse_text = self.fi_regex.sub('', verse_text) - verse_text = self.rf_regex.sub('', verse_text) - verse_text = self.lb_regex.sub(' ', verse_text) - verse_text = self.lg_regex.sub('', verse_text) - verse_text = self.l_regex.sub(' ', verse_text) - verse_text = self.w_regex.sub('', verse_text) - verse_text = self.q1_regex.sub('"', verse_text) - verse_text = self.q2_regex.sub('\'', verse_text) - verse_text = self.q_regex.sub('', verse_text) - verse_text = self.divine_name_regex.sub(repl, verse_text) - verse_text = self.trans_regex.sub('', verse_text) - verse_text = verse_text.replace('', '') \ - .replace('', '').replace('', '') \ - .replace('', '').replace('', '') \ - .replace('', '').replace('', '') - verse_text = self.spaces_regex.sub(' ', verse_text) - self.create_verse(db_book.id, chapter, verse, verse_text) - self.application.process_events() - self.session.commit() - if match_count == 0: - success = False + translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' % + {'bookname': db_book.name, 'chapter': chapter_number})) + else: + # The chapter tags is used as milestones. For now we assume verses is also milestones + chapter_number = 0 + for element in book: + if element.tag == '{http://www.bibletechnologies.net/2003/OSIS/namespace}chapter' \ + and element.get('sID'): + chapter_number = element.get("osisID").split('.')[1] + self.wizard.increment_progress_bar( + translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' % + {'bookname': db_book.name, 'chapter': chapter_number})) + elif element.tag == '{http://www.bibletechnologies.net/2003/OSIS/namespace}verse' \ + and element.get('sID'): + # If this tag marks the start of a verse, the verse text is between this tag and + # the next tag, which the "tail" attribute gives us. + verse_number = element.get("osisID").split('.')[2] + verse_text = element.tail + if verse_text: + self.create_verse(db_book.id, chapter_number, verse_number, verse_text.strip()) + self.session.commit() + self.application.process_events() except (ValueError, IOError): log.exception('Loading bible from OSIS file failed') + trace_error_handler(log) success = False + except etree.XMLSyntaxError as e: + log.exception('Loading bible from OSIS file failed') + trace_error_handler(log) + success = False + critical_error_message_box(message=translate('BiblesPlugin.OsisImport', + 'The file is not a valid OSIS-XML file: \n%s' % e.msg)) finally: - if osis: - osis.close() + if import_file: + import_file.close() if self.stop_import_flag: return False else: diff --git a/openlp/plugins/bibles/lib/zefania.py b/openlp/plugins/bibles/lib/zefania.py index 4e8373a93..81fb49eb5 100644 --- a/openlp/plugins/bibles/lib/zefania.py +++ b/openlp/plugins/bibles/lib/zefania.py @@ -64,11 +64,18 @@ class ZefaniaBible(BibleDB): # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding # detection, and the two mechanisms together interfere with each other. import_file = open(self.filename, 'rb') - language_id = self.get_language(bible_name) + zefania_bible_tree = etree.parse(import_file) + # Find bible language + language_id = None + language = zefania_bible_tree.xpath("/XMLBIBLE/INFORMATION/language/text()") + if language: + language_id = BiblesResourcesDB.get_language(language[0]) + # The language couldn't be detected, ask the user + if not language_id: + language_id = self.get_language(bible_name) if not language_id: log.error('Importing books from "%s" failed' % self.filename) return False - zefania_bible_tree = etree.parse(import_file) num_books = int(zefania_bible_tree.xpath("count(//BIBLEBOOK)")) # Strip tags we don't use - keep content etree.strip_tags(zefania_bible_tree, ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF')) @@ -97,7 +104,6 @@ class ZefaniaBible(BibleDB): self.session.commit() self.application.process_events() except Exception as e: - print(str(e)) critical_error_message_box( message=translate('BiblesPlugin.ZefaniaImport', 'Incorrect Bible file type supplied. Zefania Bibles may be ' diff --git a/openlp/plugins/media/forms/__init__.py b/openlp/plugins/media/forms/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/openlp/plugins/media/forms/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py new file mode 100644 index 000000000..ca091693e --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + + +from PyQt4 import QtCore, QtGui +from openlp.core.common import translate +from openlp.core.lib import build_icon + + +class Ui_MediaClipSelector(object): + def setupUi(self, media_clip_selector): + media_clip_selector.setObjectName('media_clip_selector') + media_clip_selector.resize(554, 654) + self.combobox_size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) + media_clip_selector.setSizePolicy( + QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) + self.main_layout = QtGui.QVBoxLayout(media_clip_selector) + self.main_layout.setContentsMargins(8, 8, 8, 8) + self.main_layout.setObjectName('main_layout') + # Source groupbox + self.source_groupbox = QtGui.QGroupBox(media_clip_selector) + self.source_groupbox.setObjectName('source_groupbox') + self.source_layout = QtGui.QHBoxLayout() + self.source_layout.setContentsMargins(8, 8, 8, 8) + self.source_layout.setObjectName('source_layout') + self.source_groupbox.setLayout(self.source_layout) + # Media path label + self.media_path_label = QtGui.QLabel(self.source_groupbox) + self.media_path_label.setObjectName('media_path_label') + self.source_layout.addWidget(self.media_path_label) + # Media path combobox + self.media_path_combobox = QtGui.QComboBox(self.source_groupbox) + # Make the combobox expand + self.media_path_combobox.setSizePolicy(self.combobox_size_policy) + self.media_path_combobox.setEditable(True) + self.media_path_combobox.setObjectName('media_path_combobox') + self.source_layout.addWidget(self.media_path_combobox) + # Load disc button + self.load_disc_button = QtGui.QPushButton(media_clip_selector) + self.load_disc_button.setEnabled(True) + self.load_disc_button.setObjectName('load_disc_button') + self.source_layout.addWidget(self.load_disc_button) + self.main_layout.addWidget(self.source_groupbox) + # Track details group box + self.track_groupbox = QtGui.QGroupBox(media_clip_selector) + self.track_groupbox.setObjectName('track_groupbox') + self.track_layout = QtGui.QFormLayout() + self.track_layout.setContentsMargins(8, 8, 8, 8) + self.track_layout.setObjectName('track_layout') + self.label_alignment = self.track_layout.labelAlignment() + self.track_groupbox.setLayout(self.track_layout) + # Title track + self.title_label = QtGui.QLabel(self.track_groupbox) + self.title_label.setObjectName('title_label') + self.titles_combo_box = QtGui.QComboBox(self.track_groupbox) + self.titles_combo_box.setSizePolicy(self.combobox_size_policy) + self.titles_combo_box.setEditText('') + self.titles_combo_box.setObjectName('titles_combo_box') + self.track_layout.addRow(self.title_label, self.titles_combo_box) + # Audio track + self.audio_track_label = QtGui.QLabel(self.track_groupbox) + self.audio_track_label.setObjectName('audio_track_label') + self.audio_tracks_combobox = QtGui.QComboBox(self.track_groupbox) + self.audio_tracks_combobox.setSizePolicy(self.combobox_size_policy) + self.audio_tracks_combobox.setObjectName('audio_tracks_combobox') + self.track_layout.addRow(self.audio_track_label, self.audio_tracks_combobox) + self.main_layout.addWidget(self.track_groupbox) + # Subtitle track + self.subtitle_track_label = QtGui.QLabel(self.track_groupbox) + self.subtitle_track_label.setObjectName('subtitle_track_label') + self.subtitle_tracks_combobox = QtGui.QComboBox(self.track_groupbox) + self.subtitle_tracks_combobox.setSizePolicy(self.combobox_size_policy) + self.subtitle_tracks_combobox.setObjectName('subtitle_tracks_combobox') + self.track_layout.addRow(self.subtitle_track_label, self.subtitle_tracks_combobox) + # Preview frame + self.preview_frame = QtGui.QFrame(media_clip_selector) + self.preview_frame.setMinimumSize(QtCore.QSize(320, 240)) + self.preview_frame.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding)) + self.preview_frame.setStyleSheet('background-color:black;') + self.preview_frame.setFrameShape(QtGui.QFrame.NoFrame) + self.preview_frame.setObjectName('preview_frame') + self.main_layout.addWidget(self.preview_frame) + # player controls + self.controls_layout = QtGui.QHBoxLayout() + self.controls_layout.setObjectName('controls_layout') + self.play_button = QtGui.QToolButton(media_clip_selector) + self.play_button.setIcon(build_icon(':/slides/media_playback_start.png')) + self.play_button.setObjectName('play_button') + self.controls_layout.addWidget(self.play_button) + self.position_slider = QtGui.QSlider(media_clip_selector) + self.position_slider.setTracking(False) + self.position_slider.setOrientation(QtCore.Qt.Horizontal) + self.position_slider.setObjectName('position_slider') + self.controls_layout.addWidget(self.position_slider) + self.position_timeedit = QtGui.QTimeEdit(media_clip_selector) + self.position_timeedit.setReadOnly(True) + self.position_timeedit.setObjectName('position_timeedit') + self.controls_layout.addWidget(self.position_timeedit) + self.main_layout.addLayout(self.controls_layout) + # Range + self.range_groupbox = QtGui.QGroupBox(media_clip_selector) + self.range_groupbox.setObjectName('range_groupbox') + self.range_layout = QtGui.QGridLayout() + self.range_layout.setContentsMargins(8, 8, 8, 8) + self.range_layout.setObjectName('range_layout') + self.range_groupbox.setLayout(self.range_layout) + # Start position + self.start_position_label = QtGui.QLabel(self.range_groupbox) + self.start_position_label.setObjectName('start_position_label') + self.range_layout.addWidget(self.start_position_label, 0, 0, self.label_alignment) + self.start_position_edit = QtGui.QTimeEdit(self.range_groupbox) + self.start_position_edit.setObjectName('start_position_edit') + self.range_layout.addWidget(self.start_position_edit, 0, 1) + self.set_start_button = QtGui.QPushButton(self.range_groupbox) + self.set_start_button.setObjectName('set_start_button') + self.range_layout.addWidget(self.set_start_button, 0, 2) + self.jump_start_button = QtGui.QPushButton(self.range_groupbox) + self.jump_start_button.setObjectName('jump_start_button') + self.range_layout.addWidget(self.jump_start_button, 0, 3) + # End position + self.end_position_label = QtGui.QLabel(self.range_groupbox) + self.end_position_label.setObjectName('end_position_label') + self.range_layout.addWidget(self.end_position_label, 1, 0, self.label_alignment) + self.end_timeedit = QtGui.QTimeEdit(self.range_groupbox) + self.end_timeedit.setObjectName('end_timeedit') + self.range_layout.addWidget(self.end_timeedit, 1, 1) + self.set_end_button = QtGui.QPushButton(self.range_groupbox) + self.set_end_button.setObjectName('set_end_button') + self.range_layout.addWidget(self.set_end_button, 1, 2) + self.jump_end_button = QtGui.QPushButton(self.range_groupbox) + self.jump_end_button.setObjectName('jump_end_button') + self.range_layout.addWidget(self.jump_end_button, 1, 3) + self.main_layout.addWidget(self.range_groupbox) + # Save and close buttons + self.button_box = QtGui.QDialogButtonBox(media_clip_selector) + self.button_box.addButton(QtGui.QDialogButtonBox.Save) + self.button_box.addButton(QtGui.QDialogButtonBox.Close) + self.close_button = self.button_box.button(QtGui.QDialogButtonBox.Close) + self.save_button = self.button_box.button(QtGui.QDialogButtonBox.Save) + self.main_layout.addWidget(self.button_box) + + self.retranslateUi(media_clip_selector) + self.button_box.accepted.connect(media_clip_selector.accept) + self.button_box.rejected.connect(media_clip_selector.reject) + QtCore.QMetaObject.connectSlotsByName(media_clip_selector) + media_clip_selector.setTabOrder(self.media_path_combobox, self.load_disc_button) + media_clip_selector.setTabOrder(self.load_disc_button, self.titles_combo_box) + media_clip_selector.setTabOrder(self.titles_combo_box, self.audio_tracks_combobox) + media_clip_selector.setTabOrder(self.audio_tracks_combobox, self.subtitle_tracks_combobox) + media_clip_selector.setTabOrder(self.subtitle_tracks_combobox, self.play_button) + media_clip_selector.setTabOrder(self.play_button, self.position_slider) + media_clip_selector.setTabOrder(self.position_slider, self.position_timeedit) + media_clip_selector.setTabOrder(self.position_timeedit, self.start_position_edit) + media_clip_selector.setTabOrder(self.start_position_edit, self.set_start_button) + media_clip_selector.setTabOrder(self.set_start_button, self.jump_start_button) + media_clip_selector.setTabOrder(self.jump_start_button, self.end_timeedit) + media_clip_selector.setTabOrder(self.end_timeedit, self.set_end_button) + media_clip_selector.setTabOrder(self.set_end_button, self.jump_end_button) + media_clip_selector.setTabOrder(self.jump_end_button, self.save_button) + media_clip_selector.setTabOrder(self.save_button, self.close_button) + + def retranslateUi(self, media_clip_selector): + media_clip_selector.setWindowTitle(translate('MediaPlugin.MediaClipSelector', 'Select Media Clip')) + self.source_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Source')) + self.media_path_label.setText(translate('MediaPlugin.MediaClipSelector', 'Media path:')) + self.media_path_combobox.lineEdit().setPlaceholderText(translate('MediaPlugin.MediaClipSelector', + 'Select drive from list')) + self.load_disc_button.setText(translate('MediaPlugin.MediaClipSelector', 'Load disc')) + self.track_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Track Details')) + self.title_label.setText(translate('MediaPlugin.MediaClipSelector', 'Title:')) + self.audio_track_label.setText(translate('MediaPlugin.MediaClipSelector', 'Audio track:')) + self.subtitle_track_label.setText(translate('MediaPlugin.MediaClipSelector', 'Subtitle track:')) + self.position_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.range_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Clip Range')) + self.start_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'Start point:')) + self.start_position_edit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.set_start_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set start point')) + self.jump_start_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to start point')) + self.end_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'End point:')) + self.end_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.set_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set end point')) + self.jump_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to end point')) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py new file mode 100644 index 000000000..d63e8a8bb --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -0,0 +1,693 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +import os +import logging +import re +from time import sleep +from datetime import datetime + +from PyQt4 import QtCore, QtGui + +from openlp.core.common import translate, is_win, is_linux, is_macosx, RegistryProperties +from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector +from openlp.core.lib.ui import critical_error_message_box + +if is_win(): + from win32com.client import Dispatch + +if is_linux(): + import dbus + +try: + from openlp.core.ui.media.vendor import vlc +except (ImportError, NameError, NotImplementedError): + pass +except OSError as e: + if is_win(): + if not isinstance(e, WindowsError) and e.winerror != 126: + raise + else: + raise + +log = logging.getLogger(__name__) + + +class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryProperties): + """ + Class to manage the clip selection + """ + log.info('%s MediaClipSelectorForm loaded', __name__) + + def __init__(self, media_item, parent, manager): + """ + Constructor + """ + super(MediaClipSelectorForm, self).__init__(parent) + self.vlc_instance = None + self.vlc_media_player = None + self.vlc_media = None + self.timer = None + self.audio_cd_tracks = None + self.audio_cd = False + self.playback_length = 0 + self.media_item = media_item + self.setupUi(self) + # setup play/pause icon + self.play_icon = QtGui.QIcon() + self.play_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, + QtGui.QIcon.Off) + self.pause_icon = QtGui.QIcon() + self.pause_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, + QtGui.QIcon.Off) + + def reject(self): + """ + Exit Dialog and do not save + """ + log.debug('MediaClipSelectorForm.reject') + # Tear down vlc + if self.vlc_media_player: + self.vlc_media_player.stop() + self.vlc_media_player.release() + self.vlc_media_player = None + if self.vlc_instance: + self.vlc_instance.release() + self.vlc_instance = None + if self.vlc_media: + self.vlc_media.release() + self.vlc_media = None + return QtGui.QDialog.reject(self) + + def exec_(self): + """ + Start dialog + """ + self.reset_ui() + self.setup_vlc() + return QtGui.QDialog.exec_(self) + + def reset_ui(self): + """ + Reset the UI to default values + """ + self.playback_length = 0 + self.position_slider.setMinimum(0) + self.disable_all() + self.toggle_disable_load_media(False) + self.subtitle_tracks_combobox.clear() + self.audio_tracks_combobox.clear() + self.titles_combo_box.clear() + time = QtCore.QTime() + self.start_position_edit.setTime(time) + self.end_timeedit.setTime(time) + self.position_timeedit.setTime(time) + + def setup_vlc(self): + """ + Setup VLC instance and mediaplayer + """ + self.vlc_instance = vlc.Instance() + # creating an empty vlc media player + self.vlc_media_player = self.vlc_instance.media_player_new() + # The media player has to be 'connected' to the QFrame. + # (otherwise a video would be displayed in it's own window) + # This is platform specific! + # You have to give the id of the QFrame (or similar object) + # to vlc, different platforms have different functions for this. + win_id = int(self.preview_frame.winId()) + if is_win(): + self.vlc_media_player.set_hwnd(win_id) + elif is_macosx(): + # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa + # framework and not the old Carbon. + self.vlc_media_player.set_nsobject(win_id) + else: + # for Linux using the X Server + self.vlc_media_player.set_xwindow(win_id) + self.vlc_media = None + # Setup timer every 100 ms to update position + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(self.update_position) + self.timer.start(100) + self.find_optical_devices() + self.audio_cd = False + self.audio_cd_tracks = None + + def detect_audio_cd(self, path): + """ + Detects is the given path is an audio CD + + :param path: Path to the device to be tested. + :return: True if it was an audio CD else False. + """ + # Detect by trying to play it as a CD + self.vlc_media = self.vlc_instance.media_new_location('cdda://' + path) + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.play() + # Wait for media to start playing. In this case VLC actually returns an error. + self.media_state_wait(vlc.State.Playing) + self.vlc_media_player.set_pause(1) + # If subitems exists, this is a CD + self.audio_cd_tracks = self.vlc_media.subitems() + if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1: + return False + # Insert into titles_combo_box + self.titles_combo_box.clear() + for i in range(self.audio_cd_tracks.count()): + item = self.audio_cd_tracks.item_at_index(i) + item_title = item.get_meta(vlc.Meta.Title) + self.titles_combo_box.addItem(item_title, i) + self.vlc_media_player.set_media(self.audio_cd_tracks.item_at_index(0)) + self.audio_cd = True + self.titles_combo_box.setDisabled(False) + self.titles_combo_box.setCurrentIndex(0) + self.on_titles_combo_box_currentIndexChanged(0) + + return True + + @QtCore.pyqtSlot(bool) + def on_load_disc_button_clicked(self, clicked): + """ + Load the media when the load-button has been clicked + + :param clicked: Given from signal, not used. + """ + log.debug('on_load_disc_button_clicked') + self.disable_all() + self.application.set_busy_cursor() + path = self.media_path_combobox.currentText() + # Check if given path is non-empty and exists before starting VLC + if not path: + log.debug('no given path') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + return + if not os.path.exists(path): + log.debug('Given path does not exists') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'Given path does not exists')) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + return + # VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to + # detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS. + if os.name == 'nt': + # If the given path is in the format "D:\" or "D:", prefix it with "/" to make VLC happy + pattern = re.compile('^\w:\\\\*$') + if pattern.match(path): + path = '/' + path + self.vlc_media = self.vlc_instance.media_new_location('dvd://' + path) + else: + self.vlc_media = self.vlc_instance.media_new_path(path) + if not self.vlc_media: + log.debug('vlc media player is none') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'An error happened during initialization of VLC player')) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + return + # put the media in the media player + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.audio_set_mute(True) + # start playback to get vlc to parse the media + if self.vlc_media_player.play() < 0: + log.debug('vlc play returned error') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'VLC player failed playing the media')) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) + return + self.vlc_media_player.audio_set_mute(True) + if not self.media_state_wait(vlc.State.Playing): + # Tests if this is an audio CD + if not self.detect_audio_cd(path): + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'VLC player failed playing the media')) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) + return + # pause + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + self.media_state_wait(vlc.State.Paused) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) + if not self.audio_cd: + # Temporarily disable signals + self.blockSignals(True) + # Get titles, insert in combobox + titles = self.vlc_media_player.video_get_title_description() + self.titles_combo_box.clear() + for title in titles: + self.titles_combo_box.addItem(title[1].decode(), title[0]) + # Re-enable signals + self.blockSignals(False) + # Main title is usually title #1 + if len(titles) > 1: + self.titles_combo_box.setCurrentIndex(1) + # Enable audio track combobox if anything is in it + if len(titles) > 0: + self.titles_combo_box.setDisabled(False) + log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + + @QtCore.pyqtSlot(bool) + def on_play_button_clicked(self, clicked): + """ + Toggle the playback + + :param clicked: Given from signal, not used. + """ + if self.vlc_media_player.get_state() == vlc.State.Playing: + self.vlc_media_player.pause() + self.play_button.setIcon(self.play_icon) + else: + self.vlc_media_player.play() + self.media_state_wait(vlc.State.Playing) + self.play_button.setIcon(self.pause_icon) + + @QtCore.pyqtSlot(bool) + def on_set_start_button_clicked(self, clicked): + """ + Copy the current player position to start_position_edit + + :param clicked: Given from signal, not used. + """ + vlc_ms_pos = self.vlc_media_player.get_time() + time = QtCore.QTime() + new_pos_time = time.addMSecs(vlc_ms_pos) + self.start_position_edit.setTime(new_pos_time) + # If start time is after end time, update end time. + end_time = self.end_timeedit.time() + if end_time < new_pos_time: + self.end_timeedit.setTime(new_pos_time) + + @QtCore.pyqtSlot(bool) + def on_set_end_button_clicked(self, clicked): + """ + Copy the current player position to end_timeedit + + :param clicked: Given from signal, not used. + """ + vlc_ms_pos = self.vlc_media_player.get_time() + time = QtCore.QTime() + new_pos_time = time.addMSecs(vlc_ms_pos) + self.end_timeedit.setTime(new_pos_time) + # If start time is after end time, update start time. + start_time = self.start_position_edit.time() + if start_time > new_pos_time: + self.start_position_edit.setTime(new_pos_time) + + @QtCore.pyqtSlot(QtCore.QTime) + def on_start_timeedit_timeChanged(self, new_time): + """ + Called when start_position_edit is changed manually + + :param new_time: The new time + """ + # If start time is after end time, update end time. + end_time = self.end_timeedit.time() + if end_time < new_time: + self.end_timeedit.setTime(new_time) + + @QtCore.pyqtSlot(QtCore.QTime) + def on_end_timeedit_timeChanged(self, new_time): + """ + Called when end_timeedit is changed manually + + :param new_time: The new time + """ + # If start time is after end time, update start time. + start_time = self.start_position_edit.time() + if start_time > new_time: + self.start_position_edit.setTime(new_time) + + @QtCore.pyqtSlot(bool) + def on_jump_end_button_clicked(self, clicked): + """ + Set the player position to the position stored in end_timeedit + + :param clicked: Given from signal, not used. + """ + end_time = self.end_timeedit.time() + end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() + self.vlc_media_player.set_time(end_time_ms) + + @QtCore.pyqtSlot(bool) + def on_jump_start_button_clicked(self, clicked): + """ + Set the player position to the position stored in start_position_edit + + :param clicked: Given from signal, not used. + """ + start_time = self.start_position_edit.time() + start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() + self.vlc_media_player.set_time(start_time_ms) + + @QtCore.pyqtSlot(int) + def on_titles_combo_box_currentIndexChanged(self, index): + """ + When a new title is chosen, it is loaded by VLC and info about audio and subtitle tracks is reloaded + + :param index: The index of the newly chosen title track. + """ + log.debug('in on_titles_combo_box_changed, index: %d', index) + if not self.vlc_media_player: + log.error('vlc_media_player was None') + return + self.application.set_busy_cursor() + if self.audio_cd: + self.vlc_media = self.audio_cd_tracks.item_at_index(index) + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + if not self.media_state_wait(vlc.State.Playing): + log.error('Could not start playing audio cd, needed to get track info') + self.application.set_normal_cursor() + return + self.vlc_media_player.audio_set_mute(True) + # pause + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + self.vlc_media_player.audio_set_mute(False) + self.application.set_normal_cursor() + self.toggle_disable_player(False) + else: + self.vlc_media_player.set_title(index) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + if not self.media_state_wait(vlc.State.Playing): + log.error('Could not start playing dvd, needed to get track info') + self.application.set_normal_cursor() + return + self.vlc_media_player.audio_set_mute(True) + # Get audio tracks + audio_tracks = self.vlc_media_player.audio_get_track_description() + log.debug('number of audio tracks: %d' % len(audio_tracks)) + # Clear the audio track combobox, insert new tracks + self.audio_tracks_combobox.clear() + for audio_track in audio_tracks: + self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) + # Enable audio track combobox if anything is in it + if len(audio_tracks) > 0: + self.audio_tracks_combobox.setDisabled(False) + # First track is "deactivated", so set to next if it exists + if len(audio_tracks) > 1: + self.audio_tracks_combobox.setCurrentIndex(1) + # Get subtitle tracks, insert in combobox + subtitles_tracks = self.vlc_media_player.video_get_spu_description() + self.subtitle_tracks_combobox.clear() + for subtitle_track in subtitles_tracks: + self.subtitle_tracks_combobox.addItem(subtitle_track[1].decode(), subtitle_track[0]) + # Enable subtitle track combobox is anything in it + if len(subtitles_tracks) > 0: + self.subtitle_tracks_combobox.setDisabled(False) + self.vlc_media_player.audio_set_mute(False) + self.vlc_media_player.set_pause(1) + # If a title or audio track is available the player is enabled + if self.titles_combo_box.count() > 0 or len(audio_tracks) > 0: + self.toggle_disable_player(False) + # Set media length info + self.playback_length = self.vlc_media_player.get_length() + log.debug('playback_length: %d ms' % self.playback_length) + self.position_slider.setMaximum(self.playback_length) + # setup start and end time + rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) + time = QtCore.QTime() + playback_length_time = time.addMSecs(rounded_vlc_ms_length) + self.start_position_edit.setMaximumTime(playback_length_time) + self.end_timeedit.setMaximumTime(playback_length_time) + self.end_timeedit.setTime(playback_length_time) + # Pause once again, just to make sure + loop_count = 0 + while self.vlc_media_player.get_state() == vlc.State.Playing and loop_count < 20: + sleep(0.1) + self.vlc_media_player.set_pause(1) + loop_count += 1 + log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + self.application.set_normal_cursor() + + @QtCore.pyqtSlot(int) + def on_audio_tracks_combobox_currentIndexChanged(self, index): + """ + When a new audio track is chosen update audio track bing played by VLC + + :param index: The index of the newly chosen audio track. + """ + if not self.vlc_media_player: + return + audio_track = self.audio_tracks_combobox.itemData(index) + log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: %d audio_track: %s' % (index, audio_track)) + if audio_track and int(audio_track) > 0: + self.vlc_media_player.audio_set_track(int(audio_track)) + + @QtCore.pyqtSlot(int) + def on_subtitle_tracks_combobox_currentIndexChanged(self, index): + """ + When a new subtitle track is chosen update subtitle track bing played by VLC + + :param index: The index of the newly chosen subtitle. + """ + if not self.vlc_media_player: + return + subtitle_track = self.subtitle_tracks_combobox.itemData(index) + if subtitle_track: + self.vlc_media_player.video_set_spu(int(subtitle_track)) + + def on_position_slider_sliderMoved(self, position): + """ + Set player position according to new slider position. + + :param position: Position to seek to. + """ + self.vlc_media_player.set_time(position) + + def update_position(self): + """ + Update slider position and displayed time according to VLC player position. + """ + if self.vlc_media_player: + vlc_ms_pos = self.vlc_media_player.get_time() + rounded_vlc_ms_pos = int(round(vlc_ms_pos / 100.0) * 100.0) + time = QtCore.QTime() + new_pos_time = time.addMSecs(rounded_vlc_ms_pos) + self.position_timeedit.setTime(new_pos_time) + self.position_slider.setSliderPosition(vlc_ms_pos) + + def disable_all(self): + """ + Disable all elements in the dialog + """ + self.toggle_disable_load_media(True) + self.titles_combo_box.setDisabled(True) + self.audio_tracks_combobox.setDisabled(True) + self.subtitle_tracks_combobox.setDisabled(True) + self.toggle_disable_player(True) + + def toggle_disable_load_media(self, action): + """ + Enable/disable load media combobox and button. + + :param action: If True elements are disabled, if False they are enabled. + """ + self.media_path_combobox.setDisabled(action) + self.load_disc_button.setDisabled(action) + + def toggle_disable_player(self, action): + """ + Enable/disable player elements. + + :param action: If True elements are disabled, if False they are enabled. + """ + self.play_button.setDisabled(action) + self.position_slider.setDisabled(action) + self.position_timeedit.setDisabled(action) + self.start_position_edit.setDisabled(action) + self.set_start_button.setDisabled(action) + self.jump_start_button.setDisabled(action) + self.end_timeedit.setDisabled(action) + self.set_end_button.setDisabled(action) + self.jump_end_button.setDisabled(action) + self.save_button.setDisabled(action) + + def accept(self): + """ + Saves the current media and trackinfo as a clip to the mediamanager + """ + log.debug('in MediaClipSelectorForm.accept') + start_time = self.start_position_edit.time() + start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() + end_time = self.end_timeedit.time() + end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() + title = self.titles_combo_box.itemData(self.titles_combo_box.currentIndex()) + path = self.media_path_combobox.currentText() + optical = '' + if self.audio_cd: + # Check for load problems + if start_time_ms is None or end_time_ms is None or title is None: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'CD not loaded correctly'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The CD was not loaded correctly, please re-load and try again.')) + return + optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms) + else: + audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) + subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) + # Check for load problems + if start_time_ms is None or end_time_ms is None or title is None or audio_track is None\ + or subtitle_track is None: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'DVD not loaded correctly'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The DVD was not loaded correctly, please re-load and try again.')) + return + optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms) + # Ask for an alternative name for the mediaclip + while True: + new_optical_name, ok = QtGui.QInputDialog.getText(self, translate('MediaPlugin.MediaClipSelectorForm', + 'Set name of mediaclip'), + translate('MediaPlugin.MediaClipSelectorForm', + 'Name of mediaclip:'), + QtGui.QLineEdit.Normal) + # User pressed cancel, don't save the clip + if not ok: + return + # User pressed ok, but the input text is blank + if not new_optical_name: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', + 'Enter a valid name or cancel'), + translate('MediaPlugin.MediaClipSelectorForm', + 'Enter a valid name or cancel')) + # The entered new name contains a colon, which we don't allow because colons is used to seperate clip info + elif new_optical_name.find(':') >= 0: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'Invalid character'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The name of the mediaclip must not contain the character ":"')) + # New name entered and we use it + else: + break + # Append the new name to the optical string and the path + optical += new_optical_name + ':' + path + self.media_item.add_optical_clip(optical) + + def media_state_wait(self, media_state): + """ + Wait for the video to change its state + Wait no longer than 15 seconds. (loading an optical disc takes some time) + + :param media_state: VLC media state to wait for. + :return: True if state was reached within 15 seconds, False if not or error occurred. + """ + start = datetime.now() + while media_state != self.vlc_media_player.get_state(): + if self.vlc_media_player.get_state() == vlc.State.Error: + return False + if (datetime.now() - start).seconds > 15: + return False + return True + + def find_optical_devices(self): + """ + Attempt to autodetect optical devices on the computer, and add them to the media-dropdown + :return: + """ + # Clear list first + self.media_path_combobox.clear() + if is_win(): + # use win api to find optical drives + fso = Dispatch('scripting.filesystemobject') + for drive in fso.Drives: + log.debug('Drive %s has type %d' % (drive.DriveLetter, drive.DriveType)) + # if type is 4, it is a cd-rom drive + if drive.DriveType == 4: + self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter) + elif is_linux(): + # Get disc devices from dbus and find the ones that are optical + bus = dbus.SystemBus() + try: + udev_manager_obj = bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks') + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.UDisks') + for dev in udev_manager.EnumerateDevices(): + device_obj = bus.get_object("org.freedesktop.UDisks", dev) + device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) + if device_props.Get('org.freedesktop.UDisks.Device', 'DeviceIsDrive'): + drive_props = device_props.Get('org.freedesktop.UDisks.Device', 'DriveMediaCompatibility') + if any('optical' in prop for prop in drive_props): + self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', + 'DeviceFile')) + return + except dbus.exceptions.DBusException: + log.debug('could not use udisks, will try udisks2') + udev_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2') + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.DBus.ObjectManager') + for k, v in udev_manager.GetManagedObjects().items(): + drive_info = v.get('org.freedesktop.UDisks2.Drive', {}) + drive_props = drive_info.get('MediaCompatibility') + if drive_props and any('optical' in prop for prop in drive_props): + for device in udev_manager.GetManagedObjects().values(): + if dbus.String('org.freedesktop.UDisks2.Block') in device: + if device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('Drive')] == k: + block_file = '' + for c in device[dbus.String('org.freedesktop.UDisks2.Block')][ + dbus.String('PreferredDevice')]: + if chr(c) != '\x00': + block_file += chr(c) + self.media_path_combobox.addItem(block_file) + elif is_macosx(): + # Look for DVD folders in devices to find optical devices + volumes = os.listdir('/Volumes') + candidates = list() + for volume in volumes: + if volume.startswith('.'): + continue + dirs = os.listdir('/Volumes/' + volume) + # Detect DVD + if 'VIDEO_TS' in dirs: + self.media_path_combobox.addItem('/Volumes/' + volume) + # Detect audio cd + files = [f for f in dirs if os.path.isfile(f)] + for file in files: + if file.endswith('aiff'): + self.media_path_combobox.addItem('/Volumes/' + volume) + break diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 0780b175d..679180c15 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -29,6 +29,7 @@ import logging import os +from datetime import time from PyQt4 import QtCore, QtGui @@ -38,17 +39,21 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi build_icon, check_item_selected from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType -from openlp.core.ui.media import get_media_players, set_media_players +from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds from openlp.core.utils import get_locale_key +from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE +if VLC_AVAILABLE: + from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm log = logging.getLogger(__name__) CLAPPERBOARD = ':/media/slidecontroller_multimedia.png' +OPTICAL = ':/media/media_optical.png' VIDEO_ICON = build_icon(':/media/media_video.png') AUDIO_ICON = build_icon(':/media/media_audio.png') -DVD_ICON = build_icon(':/media/media_video.png') +OPTICAL_ICON = build_icon(OPTICAL) ERROR_ICON = build_icon(':/general/general_delete.png') @@ -88,6 +93,10 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.list_view.activateDnD() def retranslateUi(self): + """ + This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem`` + to another language. + """ self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media') self.replace_action.setText(UiStrings().ReplaceBG) self.replace_action.setToolTip(UiStrings().ReplaceLiveBG) @@ -106,10 +115,35 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.has_edit_icon = False def add_list_view_to_toolbar(self): + """ + Creates the main widget for listing items. + """ MediaManagerItem.add_list_view_to_toolbar(self) self.list_view.addAction(self.replace_action) + def add_start_header_bar(self): + """ + Adds buttons to the start of the header bar. + """ + if 'vlc' in get_media_players()[0]: + diable_optical_button_text = False + optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + else: + diable_optical_button_text = True + optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + optical_button_tooltip = translate('MediaPlugin.MediaItem', + 'Load CD/DVD - only supported when VLC is installed and enabled') + self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text=optical_button_text, + tooltip=optical_button_tooltip, + triggers=self.on_load_optical) + if diable_optical_button_text: + self.load_optical.setDisabled(True) + def add_end_header_bar(self): + """ + Adds buttons to the end of the header bar. + """ # Replace backgrounds do not work at present so remove functionality. self.replace_action = self.toolbar.add_toolbar_action('replace_action', icon=':/slides/slide_blank.png', triggers=self.on_replace_click) @@ -198,22 +232,42 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): if item is None: return False filename = item.data(QtCore.Qt.UserRole) - if not os.path.exists(filename): - if not remote: - # File is no longer present - critical_error_message_box( - translate('MediaPlugin.MediaItem', 'Missing Media File'), - translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) - return False - (path, name) = os.path.split(filename) - service_item.title = name - service_item.processor = self.display_type_combo_box.currentText() - service_item.add_from_command(path, name, CLAPPERBOARD) - # Only get start and end times if going to a service - if context == ServiceItemContext.Service: - # Start media and obtain the length - if not self.media_controller.media_length(service_item): + # Special handling if the filename is a optical clip + if filename.startswith('optical:'): + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename) + if not os.path.exists(name): + if not remote: + # Optical disc is no longer present + critical_error_message_box( + translate('MediaPlugin.MediaItem', 'Missing Media File'), + translate('MediaPlugin.MediaItem', 'The optical disc %s is no longer available.') % name) return False + service_item.processor = self.display_type_combo_box.currentText() + service_item.add_from_command(filename, name, CLAPPERBOARD) + service_item.title = clip_name + # Set the length + self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None) + service_item.set_media_length((end - start) / 1000) + service_item.start_time = start / 1000 + service_item.end_time = end / 1000 + service_item.add_capability(ItemCapabilities.IsOptical) + else: + if not os.path.exists(filename): + if not remote: + # File is no longer present + critical_error_message_box( + translate('MediaPlugin.MediaItem', 'Missing Media File'), + translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) + return False + (path, name) = os.path.split(filename) + service_item.title = name + service_item.processor = self.display_type_combo_box.currentText() + service_item.add_from_command(path, name, CLAPPERBOARD) + # Only get start and end times if going to a service + if context == ServiceItemContext.Service: + # Start media and obtain the length + if not self.media_controller.media_length(service_item): + return False service_item.add_capability(ItemCapabilities.CanAutoStartForLive) service_item.add_capability(ItemCapabilities.CanEditTitle) service_item.add_capability(ItemCapabilities.RequiresMedia) @@ -224,6 +278,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return True def initialise(self): + """ + Initialize media item. + """ self.list_view.clear() self.list_view.setIconSize(QtCore.QSize(88, 50)) self.service_path = os.path.join(AppLocation.get_section_data_path(self.settings_section), 'thumbnails') @@ -241,6 +298,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): ' '.join(self.media_controller.audio_extensions_list), UiStrings().AllFiles) def display_setup(self): + """ + Setup media controller display. + """ self.media_controller.setup_display(self.display_controller.preview_display, False) def populate_display_types(self): @@ -280,7 +340,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): Settings().setValue(self.settings_section + '/media files', self.get_file_list()) def load_list(self, media, target_group=None): - # Sort the media by its filename considering language specific characters. """ Load the media list @@ -290,12 +349,22 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): media.sort(key=lambda file_name: get_locale_key(os.path.split(str(file_name))[1])) for track in media: track_info = QtCore.QFileInfo(track) - if not os.path.exists(track): + if track.startswith('optical:'): + # Handle optical based item + (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track) + item_name = QtGui.QListWidgetItem(clip_name) + item_name.setIcon(OPTICAL_ICON) + item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip('%s@%s-%s' % (file_name, format_milliseconds(start), format_milliseconds(end))) + elif not os.path.exists(track): + # File doesn't exist, mark as error. file_name = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(file_name) item_name.setIcon(ERROR_ICON) item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip(track) elif track_info.isFile(): + # Normal media file handling. file_name = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(file_name) if '*.%s' % (file_name.split('.')[-1].lower()) in self.media_controller.audio_extensions_list: @@ -303,15 +372,16 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): else: item_name.setIcon(VIDEO_ICON) item_name.setData(QtCore.Qt.UserRole, track) - else: - file_name = os.path.split(str(track))[1] - item_name = QtGui.QListWidgetItem(file_name) - item_name.setIcon(build_icon(DVD_ICON)) - item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(track) + item_name.setToolTip(track) self.list_view.addItem(item_name) def get_list(self, type=MediaType.Audio): + """ + Get the list of media, optional select media type. + + :param type: Type to get, defaults to audio. + :return: The media list + """ media = Settings().value(self.settings_section + '/media files') media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) if type == MediaType.Audio: @@ -323,6 +393,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return media def search(self, string, show_error): + """ + Performs a search for items containing ``string`` + + :param string: String to be displayed + :param show_error: Should the error be shown (True) + :return: The search result. + """ files = Settings().value(self.settings_section + '/media files') results = [] string = string.lower() @@ -331,3 +408,32 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): if filename.lower().find(string) > -1: results.append([file, filename]) return results + + def on_load_optical(self): + """ + When the load optical button is clicked, open the clip selector window. + """ + # self.media_clip_selector_form.exec_() + if VLC_AVAILABLE: + media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) + media_clip_selector_form.exec_() + del media_clip_selector_form + else: + QtGui.QMessageBox.critical(self, 'VLC is not available', 'VLC is not available') + + def add_optical_clip(self, optical): + """ + Add a optical based clip to the mediamanager, called from media_clip_selector_form. + + :param optical: The clip to add. + """ + full_list = self.get_file_list() + # If the clip already is in the media list it isn't added and an error message is displayed. + if optical in full_list: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'), + translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved')) + return + # Append the optical string to the media list + full_list.append(optical) + self.load_list([optical]) + Settings().setValue(self.settings_section + '/media files', self.get_file_list()) diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 584c1401f..d032b9161 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -42,7 +42,9 @@ import logging import os import time -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from win32com.client import Dispatch import pywintypes # Declare an empty exception to match the exception imported from UNO @@ -93,7 +95,7 @@ class ImpressController(PresentationController): Impress is able to run on this machine. """ log.debug('check_available') - if os.name == 'nt': + if is_win(): return self.get_com_servicemanager() is not None else: return uno_available @@ -104,7 +106,7 @@ class ImpressController(PresentationController): UNO interface when required. """ log.debug('start process Openoffice') - if os.name == 'nt': + if is_win(): self.manager = self.get_com_servicemanager() self.manager._FlagAsMethod('Bridge_GetStruct') self.manager._FlagAsMethod('Bridge_GetValueObject') @@ -129,7 +131,7 @@ class ImpressController(PresentationController): try: uno_instance = get_uno_instance(resolver) except: - log.warn('Unable to find running instance ') + log.warning('Unable to find running instance ') self.start_process() loop += 1 try: @@ -138,7 +140,7 @@ class ImpressController(PresentationController): desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) return desktop except: - log.warn('Failed to get UNO desktop') + log.warning('Failed to get UNO desktop') return None def get_com_desktop(self): @@ -152,7 +154,7 @@ class ImpressController(PresentationController): try: desktop = self.manager.createInstance('com.sun.star.frame.Desktop') except (AttributeError, pywintypes.com_error): - log.warn('Failure to find desktop - Impress may have closed') + log.warning('Failure to find desktop - Impress may have closed') return desktop if desktop else None def get_com_servicemanager(self): @@ -163,7 +165,7 @@ class ImpressController(PresentationController): try: return Dispatch('com.sun.star.ServiceManager') except pywintypes.com_error: - log.warn('Failed to get COM service manager. Impress Controller has been disabled') + log.warning('Failed to get COM service manager. Impress Controller has been disabled') return None def kill(self): @@ -175,12 +177,12 @@ class ImpressController(PresentationController): self.docs[0].close_presentation() desktop = None try: - if os.name != 'nt': + if not is_win(): desktop = self.get_uno_desktop() else: desktop = self.get_com_desktop() except: - log.warn('Failed to find an OpenOffice desktop to terminate') + log.warning('Failed to find an OpenOffice desktop to terminate') if not desktop: return docs = desktop.getComponents() @@ -198,7 +200,7 @@ class ImpressController(PresentationController): desktop.terminate() log.debug('OpenOffice killed') except: - log.warn('Failed to terminate OpenOffice') + log.warning('Failed to terminate OpenOffice') class ImpressDocument(PresentationDocument): @@ -223,7 +225,7 @@ class ImpressDocument(PresentationDocument): is available the presentation is loaded and started. """ log.debug('Load Presentation OpenOffice') - if os.name == 'nt': + if is_win(): desktop = self.controller.get_com_desktop() if desktop is None: self.controller.start_process() @@ -236,7 +238,7 @@ class ImpressDocument(PresentationDocument): return False self.desktop = desktop properties = [] - if os.name != 'nt': + if not is_win(): # Recent versions of Impress on Windows won't start the presentation if it starts as minimized. It seems OK # on Linux though. properties.append(self.create_property('Minimized', True)) @@ -244,9 +246,9 @@ class ImpressDocument(PresentationDocument): try: self.document = desktop.loadComponentFromURL(url, '_blank', 0, properties) except: - log.warn('Failed to load presentation %s' % url) + log.warning('Failed to load presentation %s' % url) return False - if os.name == 'nt': + if is_win(): # As we can't start minimized the Impress window gets in the way. # Either window.setPosSize(0, 0, 200, 400, 12) or .setVisible(False) window = self.document.getCurrentController().getFrame().getContainerWindow() @@ -264,7 +266,7 @@ class ImpressDocument(PresentationDocument): log.debug('create thumbnails OpenOffice') if self.check_thumbnails(): return - if os.name == 'nt': + if is_win(): thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \ .replace(':', '|').replace(' ', '%20') else: @@ -297,7 +299,7 @@ class ImpressDocument(PresentationDocument): Create an OOo style property object which are passed into some Uno methods. """ log.debug('create property OpenOffice') - if os.name == 'nt': + if is_win(): property_object = self.controller.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue') else: property_object = PropertyValue() @@ -318,7 +320,7 @@ class ImpressDocument(PresentationDocument): self.presentation = None self.document.dispose() except: - log.warn("Closing presentation failed") + log.warning("Closing presentation failed") self.document = None self.controller.remove_doc(self) @@ -335,7 +337,7 @@ class ImpressDocument(PresentationDocument): log.debug("getPresentation failed to find a presentation") return False except: - log.warn("getPresentation failed to find a presentation") + log.warning("getPresentation failed to find a presentation") return False return True diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 724282eb4..ac115228a 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -98,7 +98,7 @@ class Controller(object): return True if not self.doc.is_loaded(): if not self.doc.load_presentation(): - log.warn('Failed to activate %s' % self.doc.filepath) + log.warning('Failed to activate %s' % self.doc.filepath) return False if self.is_live: self.doc.start_presentation() @@ -109,7 +109,7 @@ class Controller(object): if self.doc.is_active(): return True else: - log.warn('Failed to activate %s' % self.doc.filepath) + log.warning('Failed to activate %s' % self.doc.filepath) return False def slide(self, slide): diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index b98ae131a..0283fefd4 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -34,7 +34,7 @@ import re from subprocess import check_output, CalledProcessError, STDOUT from openlp.core.utils import AppLocation -from openlp.core.common import Settings +from openlp.core.common import Settings, is_win from openlp.core.lib import ScreenList from .presentationcontroller import PresentationController, PresentationDocument @@ -123,7 +123,7 @@ class PdfController(PresentationController): else: # Fallback to autodetection application_path = AppLocation.get_directory(AppLocation.AppDir) - if os.name == 'nt': + if is_win(): # for windows we only accept mudraw.exe in the base folder application_path = AppLocation.get_directory(AppLocation.AppDir) if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 0f9c2ff35..f42e4f814 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -33,7 +33,9 @@ This modul is for controlling powerpiont. PPT API documentation: import os import logging -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from win32com.client import Dispatch import winreg import win32ui @@ -69,7 +71,7 @@ class PowerpointController(PresentationController): PowerPoint is able to run on this machine. """ log.debug('check_available') - if os.name == 'nt': + if is_win(): try: winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, 'PowerPoint.Application').Close() return True @@ -77,7 +79,7 @@ class PowerpointController(PresentationController): pass return False - if os.name == 'nt': + if is_win(): def start_process(self): """ Loads PowerPoint process. @@ -271,7 +273,7 @@ class PowerpointDocument(PresentationDocument): trace_error_handler(log) self.show_error_msg() - if os.name == 'nt': + if is_win(): def start_presentation(self): """ Starts a presentation from the beginning. diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index a9090dd1e..7e03e322f 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -30,7 +30,9 @@ import logging import os -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from ctypes import cdll from ctypes.wintypes import RECT @@ -63,11 +65,11 @@ class PptviewController(PresentationController): PPT Viewer is able to run on this machine. """ log.debug('check_available') - if os.name != 'nt': + if not is_win(): return False return self.check_installed() - if os.name == 'nt': + if is_win(): def check_installed(self): """ Check the viewer is installed. diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 5e0d7395d..7f080df22 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -90,7 +90,7 @@ class PresentationPlugin(Plugin): try: self.controllers[controller].start_process() except Exception: - log.warn('Failed to start controller process') + log.warning('Failed to start controller process') self.controllers[controller].available = False self.media_item.build_file_mask_string() @@ -134,7 +134,7 @@ class PresentationPlugin(Plugin): try: __import__(module_name, globals(), locals(), []) except ImportError: - log.warn('Failed to import %s on path %s', module_name, path) + log.warning('Failed to import %s on path %s', module_name, path) controller_classes = PresentationController.__subclasses__() for controller_class in controller_classes: controller = controller_class(self) diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 589da4d33..412ec0369 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -124,7 +124,7 @@ class SongExportForm(OpenLPWizard): self.export_song_layout = QtGui.QHBoxLayout(self.export_song_page) self.export_song_layout.setObjectName('export_song_layout') self.grid_layout = QtGui.QGridLayout() - self.grid_layout.setObjectName('grid_layout') + self.grid_layout.setObjectName('range_layout') self.selected_list_widget = QtGui.QListWidget(self.export_song_page) self.selected_list_widget.setObjectName('selected_list_widget') self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 1) diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py index f9f658c5b..aecdf9682 100755 --- a/openlp/plugins/songs/forms/songselectform.py +++ b/openlp/plugins/songs/forms/songselectform.py @@ -37,7 +37,7 @@ from time import sleep from PyQt4 import QtCore, QtGui from openlp.core import Settings -from openlp.core.common import Registry +from openlp.core.common import Registry, is_win from openlp.core.lib import translate from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog from openlp.plugins.songs.lib.songselect import SongSelectImport @@ -377,7 +377,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog): Adds the openlp to the class dynamically. Windows needs to access the application in a dynamic manner. """ - if os.name == 'nt': + if is_win(): return Registry().get('application') else: if not hasattr(self, '_application'): diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index a9206a397..3afba1b66 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -31,8 +31,6 @@ The :mod:`db` module provides the database and schema that is the backend for the Songs plugin """ -import re - from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.sql.expression import func, text @@ -329,7 +327,9 @@ def init_schema(url): Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True) ) - mapper(Author, authors_table) + mapper(Author, authors_table, properties={ + 'songs': relation(Song, secondary=authors_songs_table, viewonly=True) + }) mapper(AuthorSong, authors_songs_table, properties={ 'author': relation(Author) }) @@ -339,7 +339,8 @@ def init_schema(url): # Use the authors_songs relation when you need access to the 'author_type' attribute # or when creating new relations 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"), - 'authors': relation(Author, secondary=authors_songs_table, viewonly=True), + # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198) + 'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'), 'book': relation(Book, backref='songs'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table) diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index e9ee2c2f3..12c8887f7 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -32,7 +32,7 @@ The :mod:`importer` modules provides the general song import functionality. import os import logging -from openlp.core.common import translate, UiStrings +from openlp.core.common import translate, UiStrings, is_win from openlp.core.ui.wizard import WizardStrings from .importers.opensong import OpenSongImport from .importers.easyslides import EasySlidesImport @@ -70,14 +70,14 @@ except ImportError: log.exception('Error importing %s', 'OooImport') HAS_OOO = False HAS_MEDIASHOUT = False -if os.name == 'nt': +if is_win(): try: from .importers.mediashout import MediaShoutImport HAS_MEDIASHOUT = True except ImportError: log.exception('Error importing %s', 'MediaShoutImport') HAS_WORSHIPCENTERPRO = False -if os.name == 'nt': +if is_win(): try: from .importers.worshipcenterpro import WorshipCenterProImport HAS_WORSHIPCENTERPRO = True diff --git a/openlp/plugins/songs/lib/importers/openoffice.py b/openlp/plugins/songs/lib/importers/openoffice.py index 0e499f7ae..a0bb1df88 100644 --- a/openlp/plugins/songs/lib/importers/openoffice.py +++ b/openlp/plugins/songs/lib/importers/openoffice.py @@ -32,13 +32,14 @@ import time from PyQt4 import QtCore +from openlp.core.common import is_win from openlp.core.utils import get_uno_command, get_uno_instance from openlp.core.lib import translate from .songimport import SongImport log = logging.getLogger(__name__) -if os.name == 'nt': +if is_win(): from win32com.client import Dispatch NoConnectException = Exception else: @@ -106,7 +107,7 @@ class OpenOfficeImport(SongImport): Start OpenOffice.org process TODO: The presentation/Impress plugin may already have it running """ - if os.name == 'nt': + if is_win(): self.start_ooo_process() self.desktop = self.ooo_manager.createInstance('com.sun.star.frame.Desktop') else: @@ -133,7 +134,7 @@ class OpenOfficeImport(SongImport): Start the OO Process """ try: - if os.name == 'nt': + if is_win(): self.ooo_manager = Dispatch('com.sun.star.ServiceManager') self.ooo_manager._FlagAsMethod('Bridge_GetStruct') self.ooo_manager._FlagAsMethod('Bridge_GetValueObject') @@ -150,7 +151,7 @@ class OpenOfficeImport(SongImport): Open the passed file in OpenOffice.org Impress """ self.file_path = file_path - if os.name == 'nt': + if is_win(): url = file_path.replace('\\', '/') url = url.replace(':', '|').replace(' ', '%20') url = 'file:///' + url diff --git a/openlp/plugins/songs/lib/importers/songshowplus.py b/openlp/plugins/songs/lib/importers/songshowplus.py index 6c9feab68..6a1b720fb 100644 --- a/openlp/plugins/songs/lib/importers/songshowplus.py +++ b/openlp/plugins/songs/lib/importers/songshowplus.py @@ -152,7 +152,7 @@ class SongShowPlusImport(SongImport): if match: self.ccli_number = int(match.group()) else: - log.warn("Can't parse CCLI Number from string: %s" % self.decode(data)) + log.warning("Can't parse CCLI Number from string: %s" % self.decode(data)) elif block_key == VERSE: self.add_verse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Verse], verse_no)) elif block_key == CHORUS: diff --git a/openlp/plugins/songs/lib/importers/songsoffellowship.py b/openlp/plugins/songs/lib/importers/songsoffellowship.py index c1ef8666f..2cc49caef 100644 --- a/openlp/plugins/songs/lib/importers/songsoffellowship.py +++ b/openlp/plugins/songs/lib/importers/songsoffellowship.py @@ -37,12 +37,13 @@ import logging import os import re +from openlp.core.common import is_win from .openoffice import OpenOfficeImport log = logging.getLogger(__name__) -if os.name == 'nt': +if is_win(): from .openoffice import PAGE_BEFORE, PAGE_AFTER, PAGE_BOTH RuntimeException = Exception else: diff --git a/openlp/plugins/songs/lib/importers/worshipcenterpro.py b/openlp/plugins/songs/lib/importers/worshipcenterpro.py index 817bd8cae..4c241b05c 100644 --- a/openlp/plugins/songs/lib/importers/worshipcenterpro.py +++ b/openlp/plugins/songs/lib/importers/worshipcenterpro.py @@ -58,7 +58,7 @@ class WorshipCenterProImport(SongImport): try: conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s' % self.import_source) except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: - log.warn('Unable to connect the WorshipCenter Pro database %s. %s', self.import_source, str(e)) + log.warning('Unable to connect the WorshipCenter Pro database %s. %s', self.import_source, str(e)) # Unfortunately no specific exception type self.log_error(self.import_source, translate('SongsPlugin.WorshipCenterProImport', 'Unable to connect the WorshipCenter Pro database.')) diff --git a/resources/forms/mediaclipselector.ui b/resources/forms/mediaclipselector.ui new file mode 100644 index 000000000..b3f22e678 --- /dev/null +++ b/resources/forms/mediaclipselector.ui @@ -0,0 +1,336 @@ + + + MediaClipSelector + + + + 0 + 0 + 683 + 739 + + + + + 0 + 0 + + + + + 683 + 686 + + + + Qt::NoFocus + + + Select media clip + + + false + + + Qt::ImhNone + + + + + 0 + 0 + + + + + + + true + + + + 0 + 0 + + + + true + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + Set current position as start point + + + + + + + true + + + Load disc + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 40 + + + + + + + + true + + + + + + + ../images/media_playback_start.png../images/media_playback_start.png + + + + + + + true + + + End point + + + + + + + true + + + + + + + true + + + Title + + + + + + + true + + + + + + + true + + + Set current position as end point + + + + + + + true + + + Save current clip + + + + + + + true + + + Close + + + + + + + true + + + Start point + + + + + + + true + + + Jump to start point + + + + + + + true + + + Audio track + + + + + + + true + + + true + + + HH:mm:ss.z + + + + + + + + 665 + 375 + + + + background-color:black; + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + true + + + Subtitle track + + + + + + + true + + + Jump to end point + + + + + + + true + + + Media path + + + + + + + true + + + + + + + + + + true + + + false + + + Qt::Horizontal + + + false + + + + + + + + media_path_combobox + load_disc_pushbutton + title_combo_box + audio_tracks_combobox + subtitle_tracks_combobox + play_pushbutton + position_horizontalslider + media_position_timeedit + start_timeedit + set_start_pushbutton + jump_start_pushbutton + end_timeedit + set_end_pushbutton + jump_end_pushbutton + save_pushbutton + close_pushbutton + + + + diff --git a/resources/images/media_optical.png b/resources/images/media_optical.png new file mode 100644 index 000000000..033b98472 Binary files /dev/null and b/resources/images/media_optical.png differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 79036f08f..0347cc3c7 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -99,6 +99,7 @@ export_load.png + openlp-osx-wizard.png wizard_exportsong.bmp wizard_importsong.bmp wizard_importbible.bmp @@ -139,6 +140,7 @@ media_stop.png media_audio.png media_video.png + media_optical.png slidecontroller_multimedia.png auto-start_active.png auto-start_inactive.png @@ -150,10 +152,10 @@ messagebox_warning.png - network_server.png + network_server.png network_ssl.png - network_auth.png - + network_auth.png + song_usage_active.png song_usage_inactive.png diff --git a/resources/images/openlp-osx-wizard.png b/resources/images/openlp-osx-wizard.png new file mode 100644 index 000000000..79437b5ed Binary files /dev/null and b/resources/images/openlp-osx-wizard.png differ diff --git a/tests/functional/openlp_core_common/test_common.py b/tests/functional/openlp_core_common/test_common.py index f52256c5c..0474bd404 100644 --- a/tests/functional/openlp_core_common/test_common.py +++ b/tests/functional/openlp_core_common/test_common.py @@ -32,7 +32,8 @@ Functional tests to test the AppLocation class and related methods. from unittest import TestCase -from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate +from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \ + is_linux from tests.functional import MagicMock, patch @@ -139,3 +140,51 @@ class TestCommonFunctions(TestCase): # THEN: the translated string should be returned, and the mocked function should have been called mocked_translate.assert_called_with(context, text, comment, encoding, n) self.assertEqual('Translated string', result, 'The translated string should have been returned') + + def is_win_test(self): + """ + Test the is_win() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'nt' and 'win32' repectivly + mocked_os.name = 'nt' + mocked_sys.platform = 'win32' + + # THEN: The three platform functions should perform properly + self.assertTrue(is_win(), 'is_win() should return True') + self.assertFalse(is_macosx(), 'is_macosx() should return False') + self.assertFalse(is_linux(), 'is_linux() should return False') + + def is_macosx_test(self): + """ + Test the is_macosx() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'darwin' repectivly + mocked_os.name = 'posix' + mocked_sys.platform = 'darwin' + + # THEN: The three platform functions should perform properly + self.assertTrue(is_macosx(), 'is_macosx() should return True') + self.assertFalse(is_win(), 'is_win() should return False') + self.assertFalse(is_linux(), 'is_linux() should return False') + + def is_linux_test(self): + """ + Test the is_linux() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'linux3' repectivly + mocked_os.name = 'posix' + mocked_sys.platform = 'linux3' + + # THEN: The three platform functions should perform properly + self.assertTrue(is_linux(), 'is_linux() should return True') + self.assertFalse(is_win(), 'is_win() should return False') + self.assertFalse(is_macosx(), 'is_macosx() should return False') diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py index ab7663a83..1190810da 100644 --- a/tests/functional/openlp_core_lib/test_file_dialog.py +++ b/tests/functional/openlp_core_lib/test_file_dialog.py @@ -5,7 +5,7 @@ from unittest import TestCase from openlp.core.common import UiStrings from openlp.core.lib.filedialog import FileDialog -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, call, patch class TestFileDialog(TestCase): @@ -65,11 +65,9 @@ class TestFileDialog(TestCase): # THEN: os.path.exists should have been called with known args. QmessageBox.information should have been # called. The returned result should correlate with the input. - self.mocked_os.path.exists.assert_callde_with('/Valid File') - self.mocked_os.path.exists.assert_callde_with('/url%20encoded%20file%20%231') - self.mocked_os.path.exists.assert_callde_with('/url encoded file #1') - self.mocked_os.path.exists.assert_callde_with('/non-existing') - self.mocked_os.path.exists.assert_callde_with('/non-existing') + call_list = [call('/Valid File'), call('/url%20encoded%20file%20%231'), call('/url encoded file #1'), + call('/non-existing'), call('/non-existing')] + self.mocked_os.path.exists.assert_has_calls(call_list) self.mocked_qt_gui.QmessageBox.information.called_with(self.mocked_parent, UiStrings().FileNotFound, UiStrings().FileNotFoundMessage % '/non-existing') self.assertEqual(result, ['/Valid File', '/url encoded file #1'], 'The returned file list is incorrect') diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index d75e94040..4fd6f6b83 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -206,3 +206,24 @@ class TestServiceItem(TestCase): 'This service item should be able to be run in a can be made to Loop') self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), 'This service item should be able to have new items added to it') + + def service_item_load_optical_media_from_service_test(self): + """ + Test the Service Item - load an optical media item + """ + # GIVEN: A new service item and a mocked add icon function + service_item = ServiceItem(None) + service_item.add_icon = MagicMock() + + # WHEN: We load a serviceitem with optical media + line = convert_file_service_item(TEST_PATH, 'serviceitem-dvd.osj') + with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: + mocked_exists.return_value = True + service_item.set_from_service(line) + + # THEN: We should get back a valid service item with optical media info + self.assertTrue(service_item.is_valid, 'The service item should be valid') + self.assertTrue(service_item.is_capable(ItemCapabilities.IsOptical), 'The item should be Optical') + self.assertEqual(service_item.start_time, 654.375, 'Start time should be 654.375') + self.assertEqual(service_item.end_time, 672.069, 'End time should be 672.069') + self.assertEqual(service_item.media_length, 17.694, 'Media length should be 17.694') diff --git a/tests/functional/openlp_core_lib/test_theme.py b/tests/functional/openlp_core_lib/test_theme.py index bcdced35f..7b09135dc 100644 --- a/tests/functional/openlp_core_lib/test_theme.py +++ b/tests/functional/openlp_core_lib/test_theme.py @@ -51,7 +51,7 @@ class TestTheme(TestCase): """ pass - def test_new_theme(self): + def new_theme_test(self): """ Test the theme creation - basic test """ diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index 591762947..7030c27f3 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -29,10 +29,14 @@ """ Package to test the openlp.core.lib.ui package. """ -from PyQt4 import QtGui +from PyQt4 import QtCore, QtGui from unittest import TestCase -from openlp.core.lib.ui import * +from openlp.core.common import UiStrings, translate +from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \ + create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \ + set_case_insensitive_completer +from tests.functional import MagicMock, patch class TestUi(TestCase): @@ -40,7 +44,7 @@ class TestUi(TestCase): Test the functions in the ui module """ - def test_add_welcome_page(self): + def add_welcome_page_test(self): """ Test appending a welcome page to a wizard """ @@ -54,7 +58,7 @@ class TestUi(TestCase): self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.') self.assertIsInstance(wizard.page(0).pixmap(QtGui.QWizard.WatermarkPixmap), QtGui.QPixmap) - def test_create_button_box(self): + def create_button_box_test(self): """ Test creating a button box for a dialog """ @@ -82,7 +86,7 @@ class TestUi(TestCase): self.assertEqual(1, len(btnbox.buttons())) self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) - def test_create_horizontal_adjusting_combo_box(self): + def create_horizontal_adjusting_combo_box_test(self): """ Test creating a horizontal adjusting combo box """ @@ -97,7 +101,7 @@ class TestUi(TestCase): self.assertEqual('combo1', combo.objectName()) self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy()) - def test_create_button(self): + def create_button_test(self): """ Test creating a button """ @@ -129,7 +133,7 @@ class TestUi(TestCase): self.assertEqual('my_btn', btn.objectName()) self.assertTrue(btn.isEnabled()) - def test_create_action(self): + def create_action_test(self): """ Test creating an action """ @@ -154,9 +158,46 @@ class TestUi(TestCase): self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my statustip', action.statusTip()) - def test_create_checked_enabled_visible_action(self): + def create_action_on_mac_osx_test(self): """ - Test creating an action with the 'checked', 'enabled' and 'visible' properties. + Test creating an action on OS X calls the correct method + """ + # GIVEN: A dialog and a mocked out is_macosx() method to always return True + with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \ + patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction: + mocked_is_macosx.return_value = True + mocked_action = MagicMock() + MockedQAction.return_value = mocked_action + dialog = QtGui.QDialog() + + # WHEN: An action is created + create_action(dialog, 'my_action') + + # THEN: setIconVisibleInMenu should be called + mocked_action.setIconVisibleInMenu.assert_called_with(False) + + def create_action_not_on_mac_osx_test(self): + """ + Test creating an action on something other than OS X doesn't call the method + """ + # GIVEN: A dialog and a mocked out is_macosx() method to always return True + with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \ + patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction: + mocked_is_macosx.return_value = False + mocked_action = MagicMock() + MockedQAction.return_value = mocked_action + dialog = QtGui.QDialog() + + # WHEN: An action is created + create_action(dialog, 'my_action') + + # THEN: setIconVisibleInMenu should not be called + self.assertEqual(0, mocked_action.setIconVisibleInMenu.call_count, + 'setIconVisibleInMenu should not have been called') + + def create_checked_disabled_invisible_action_test(self): + """ + Test that an invisible, disabled, checked action is created correctly """ # GIVEN: A dialog dialog = QtGui.QDialog() @@ -165,11 +206,24 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) # THEN: These properties should be set - self.assertEqual(True, action.isChecked()) - self.assertEqual(False, action.isEnabled()) - self.assertEqual(False, action.isVisible()) + self.assertTrue(action.isChecked(), 'The action should be checked') + self.assertFalse(action.isEnabled(), 'The action should be disabled') + self.assertFalse(action.isVisible(), 'The action should be invisble') - def test_create_valign_selection_widgets(self): + def create_action_separator_test(self): + """ + Test creating an action as separator + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create an action as a separator + action = create_action(dialog, 'my_action', separator=True) + + # THEN: The action should be a separator + self.assertTrue(action.isSeparator(), 'The action should be a separator') + + def create_valign_selection_widgets_test(self): """ Test creating a combo box for valign selection """ @@ -186,7 +240,7 @@ class TestUi(TestCase): for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]: self.assertTrue(combo.findText(text) >= 0) - def test_find_and_set_in_combo_box(self): + def find_and_set_in_combo_box_test(self): """ Test finding a string in a combo box and setting it as the selected item if present """ @@ -213,7 +267,7 @@ class TestUi(TestCase): # THEN: The index should have changed self.assertEqual(2, combo.currentIndex()) - def test_create_widget_action(self): + def create_widget_action_test(self): """ Test creating an action for a widget """ @@ -227,7 +281,7 @@ class TestUi(TestCase): self.assertIsInstance(action, QtGui.QAction) self.assertEqual(action.objectName(), 'some action') - def test_set_case_insensitive_completer(self): + def set_case_insensitive_completer_test(self): """ Test setting a case insensitive completer on a widget """ diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index 2e26c286a..35bd1675d 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -47,7 +47,7 @@ class TestFirstTimeForm(TestCase, TestMixin): Registry().register('application', self.app) self.first_time_form = FirstTimeForm(screens) - def test_access_to_config(self): + def access_to_config_test(self): """ Test if we can access the First Time Form's config file """ @@ -59,7 +59,7 @@ class TestFirstTimeForm(TestCase, TestMixin): self.assertTrue(self.first_time_form.web_access, 'First Time Wizard\'s web configuration file should be available') - def test_parsable_config(self): + def parsable_config_test(self): """ Test if the First Time Form's config file is parsable """ diff --git a/tests/functional/openlp_core_ui/test_formattingtagscontroller.py b/tests/functional/openlp_core_ui/test_formattingtagscontroller.py index 1d8512940..38cae0bf4 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagscontroller.py +++ b/tests/functional/openlp_core_ui/test_formattingtagscontroller.py @@ -39,7 +39,7 @@ class TestFormattingTagController(TestCase): def setUp(self): self.services = FormattingTagController() - def test_strip(self): + def strip_test(self): """ Test that the _strip strips the correct chars """ @@ -52,7 +52,7 @@ class TestFormattingTagController(TestCase): # THEN: The tag should be returned with the wrappers removed. self.assertEqual(result, 'tag', 'FormattingTagForm._strip should return u\'tag\' when called with u\'{tag}\'') - def test_end_tag_changed_processes_correctly(self): + def end_tag_changed_processes_correctly_test(self): """ Test that the end html tags are generated correctly """ @@ -77,7 +77,7 @@ class TestFormattingTagController(TestCase): self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % error) - def test_start_tag_changed_processes_correctly(self): + def start_tag_changed_processes_correctly_test(self): """ Test that the end html tags are generated correctly """ @@ -100,7 +100,7 @@ class TestFormattingTagController(TestCase): self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % error) - def test_start_html_to_end_html(self): + def start_html_to_end_html_test(self): """ Test that the end html tags are generated correctly """ diff --git a/tests/functional/openlp_core_ui/test_formattingtagsform.py b/tests/functional/openlp_core_ui/test_formattingtagsform.py index e71a75651..736a306c3 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagsform.py +++ b/tests/functional/openlp_core_ui/test_formattingtagsform.py @@ -29,17 +29,17 @@ """ Package to test the openlp.core.ui.formattingtagsform package. """ +from PyQt4 import QtGui from unittest import TestCase +from openlp.core.common import translate -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, patch, call from openlp.core.ui.formattingtagform import FormattingTagForm # TODO: Tests Still TODO # __init__ # exec_ -# on_new_clicked -# on_delete_clicked # on_saved_clicked # _reloadTable @@ -47,30 +47,60 @@ from openlp.core.ui.formattingtagform import FormattingTagForm class TestFormattingTagForm(TestCase): def setUp(self): - self.init_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm.__init__') - self.qdialog_patcher = patch('openlp.core.ui.formattingtagform.QtGui.QDialog') - self.ui_formatting_tag_dialog_patcher = patch('openlp.core.ui.formattingtagform.Ui_FormattingTagDialog') - self.mocked_init = self.init_patcher.start() - self.mocked_qdialog = self.qdialog_patcher.start() - self.mocked_ui_formatting_tag_dialog = self.ui_formatting_tag_dialog_patcher.start() - self.mocked_init.return_value = None + """ + Mock out stuff for all the tests + """ + self.setup_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm._setup') + self.setup_patcher.start() def tearDown(self): - self.init_patcher.stop() - self.qdialog_patcher.stop() - self.ui_formatting_tag_dialog_patcher.stop() - - def test_on_text_edited(self): """ - Test that the appropriate actions are preformed when on_text_edited is called + Remove the mocks + """ + self.setup_patcher.stop() + + def on_row_selected_test(self): + """ + Test that the appropriate actions are preformed when on_row_selected is called + """ + # GIVEN: An instance of the Formatting Tag Form and a mocked delete_button + form = FormattingTagForm(None) + form.delete_button = MagicMock() + + # WHEN: on_row_selected is called + form.on_row_selected() + + # THEN: setEnabled and should have been called on delete_button + form.delete_button.setEnabled.assert_called_with(True) + + def on_new_clicked_test(self): + """ + Test that clicking the Add a new tag button does the right thing """ - # GIVEN: An instance of the Formatting Tag Form and a mocked save_push_button - form = FormattingTagForm() - form.save_button = MagicMock() + # GIVEN: A formatting tag form and a mocked out tag table widget + form = FormattingTagForm(None) + form.tag_table_widget = MagicMock() + row_count = 5 + form.tag_table_widget.rowCount.return_value = row_count - # WHEN: on_text_edited is called with an arbitrary value - # form.on_text_edited('text') + # WHEN: on_new_clicked is run (i.e. the Add new button was clicked) + with patch('openlp.core.ui.formattingtagform.QtGui.QTableWidgetItem') as MockedQTableWidgetItem: + mocked_table_widget = MagicMock() + MockedQTableWidgetItem.return_value = mocked_table_widget + form.on_new_clicked() - # THEN: setEnabled and setDefault should have been called on save_push_button - # form.save_button.setEnabled.assert_called_with(True) + # THEN: A new row should be added to the table + form.tag_table_widget.rowCount.assert_called_with() + form.tag_table_widget.insertRow.assert_called_with(row_count) + expected_set_item_calls = [ + call(row_count, 0, mocked_table_widget), + call(row_count, 1, mocked_table_widget), + call(row_count, 2, mocked_table_widget), + call(row_count, 3, mocked_table_widget) + ] + self.assertEqual(expected_set_item_calls, form.tag_table_widget.setItem.call_args_list, + 'setItem should have been called correctly') + form.tag_table_widget.resizeRowsToContents.assert_called_with() + form.tag_table_widget.scrollToBottom.assert_called_with() + form.tag_table_widget.selectRow.assert_called_with(row_count) diff --git a/tests/functional/openlp_core_ui/test_media.py b/tests/functional/openlp_core_ui/test_media.py index 4c6fa7f86..fbb5787c0 100644 --- a/tests/functional/openlp_core_ui/test_media.py +++ b/tests/functional/openlp_core_ui/test_media.py @@ -32,7 +32,7 @@ Package to test the openlp.core.ui package. from PyQt4 import QtCore from unittest import TestCase -from openlp.core.ui.media import get_media_players +from openlp.core.ui.media import get_media_players, parse_optical_path from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -126,3 +126,59 @@ class TestMedia(TestCase, TestMixin): # THEN: the used_players should be an empty list, and the overridden player should be an empty string self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct') self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players') + + def test_parse_optical_path_linux(self): + """ + Test that test_parse_optical_path() parses a optical path with linux device path correctly + """ + + # GIVEN: An optical formatted path + org_title_track = 1 + org_audio_track = 2 + org_subtitle_track = -1 + org_start = 1234 + org_end = 4321 + org_name = 'test name' + org_device_path = '/dev/dvd' + path = 'optical:%d:%d:%d:%d:%d:%s:%s' % (org_title_track, org_audio_track, org_subtitle_track, + org_start, org_end, org_name, org_device_path) + + # WHEN: parsing the path + (device_path, title_track, audio_track, subtitle_track, start, end, name) = parse_optical_path(path) + + # THEN: The return values should match the original values + self.assertEqual(org_title_track, title_track, 'Returned title_track should match the original') + self.assertEqual(org_audio_track, audio_track, 'Returned audio_track should match the original') + self.assertEqual(org_subtitle_track, subtitle_track, 'Returned subtitle_track should match the original') + self.assertEqual(org_start, start, 'Returned start should match the original') + self.assertEqual(org_end, end, 'Returned end should match the original') + self.assertEqual(org_name, name, 'Returned end should match the original') + self.assertEqual(org_device_path, device_path, 'Returned device_path should match the original') + + def test_parse_optical_path_win(self): + """ + Test that test_parse_optical_path() parses a optical path with windows device path correctly + """ + + # GIVEN: An optical formatted path + org_title_track = 1 + org_audio_track = 2 + org_subtitle_track = -1 + org_start = 1234 + org_end = 4321 + org_name = 'test name' + org_device_path = 'D:' + path = 'optical:%d:%d:%d:%d:%d:%s:%s' % (org_title_track, org_audio_track, org_subtitle_track, + org_start, org_end, org_name, org_device_path) + + # WHEN: parsing the path + (device_path, title_track, audio_track, subtitle_track, start, end, name) = parse_optical_path(path) + + # THEN: The return values should match the original values + self.assertEqual(org_title_track, title_track, 'Returned title_track should match the original') + self.assertEqual(org_audio_track, audio_track, 'Returned audio_track should match the original') + self.assertEqual(org_subtitle_track, subtitle_track, 'Returned subtitle_track should match the original') + self.assertEqual(org_start, start, 'Returned start should match the original') + self.assertEqual(org_end, end, 'Returned end should match the original') + self.assertEqual(org_name, name, 'Returned end should match the original') + self.assertEqual(org_device_path, device_path, 'Returned device_path should match the original') diff --git a/tests/functional/openlp_plugins/bibles/test_opensongimport.py b/tests/functional/openlp_plugins/bibles/test_opensongimport.py index 542cf289d..3a4049bc6 100644 --- a/tests/functional/openlp_plugins/bibles/test_opensongimport.py +++ b/tests/functional/openlp_plugins/bibles/test_opensongimport.py @@ -31,6 +31,7 @@ This module contains tests for the OpenSong Bible importer. """ import os +import json from unittest import TestCase from tests.functional import MagicMock, patch @@ -39,29 +40,6 @@ from openlp.plugins.bibles.lib.db import BibleDB TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'bibles')) -OPENSONG_TEST_DATA = { - 'opensong-dk1933.xml': { - 'book': 'Genesis', - 'chapter': 1, - 'verses': [ - (1, 'I Begyndelsen skabte Gud Himmelen og Jorden.'), - (2, 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. ' - 'Men Guds Ånd svævede over Vandene.'), - (3, 'Og Gud sagde: "Der blive Lys!" Og der blev Lys.'), - (4, 'Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket,'), - (5, 'og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, ' - 'og det blev Morgen, første Dag.'), - (6, 'Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!"'), - (7, 'Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen ' - 'fra Vandet over Hvælvingen;'), - (8, 'og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag.'), - (9, 'Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" ' - 'Og således skete det;'), - (10, 'og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, ' - 'at det var godt.') - ] - } -} class TestOpenSongImport(TestCase): @@ -94,31 +72,33 @@ class TestOpenSongImport(TestCase): def file_import_test(self): """ - Test the actual import of real song files + Test the actual import of OpenSong Bible file """ # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'opensong-dk1933.xml' with patch('openlp.plugins.bibles.lib.opensong.OpenSongBible.application'): - for bible_file in OPENSONG_TEST_DATA: - mocked_manager = MagicMock() - mocked_import_wizard = MagicMock() - importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') - importer.wizard = mocked_import_wizard - importer.get_book_ref_id_by_name = MagicMock() - importer.create_verse = MagicMock() - importer.create_book = MagicMock() - importer.session = MagicMock() - importer.get_language = MagicMock() - importer.get_language.return_value = 'Danish' + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OpenSongBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'Danish' - # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) - importer.do_import() + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() - # THEN: The create_verse() method should have been called with each verse in the file. - self.assertTrue(importer.create_verse.called) - for verse_tag, verse_text in OPENSONG_TEST_DATA[bible_file]['verses']: - importer.create_verse.assert_any_call(importer.create_book().id, 1, verse_tag, verse_text) + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, 1, int(verse_tag), verse_text) def zefania_import_error_test(self): """ diff --git a/tests/functional/openlp_plugins/bibles/test_osisimport.py b/tests/functional/openlp_plugins/bibles/test_osisimport.py new file mode 100644 index 000000000..ba23feba1 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_osisimport.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the OSIS Bible importer. +""" + +import os +import json +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.bibles.lib.osis import OSISBible +from openlp.plugins.bibles.lib.db import BibleDB + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) + + +class TestOsisImport(TestCase): + """ + Test the functions in the :mod:`osisimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry') + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.manager_patcher.start() + + def tearDown(self): + self.registry_patcher.stop() + self.manager_patcher.stop() + + def create_importer_test(self): + """ + Test creating an instance of the OSIS file importer + """ + # GIVEN: A mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + + # THEN: The importer should be an instance of BibleDB + self.assertIsInstance(importer, BibleDB) + + def file_import_nested_tags_test(self): + """ + Test the actual import of OSIS Bible file, with nested chapter and verse tags + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'osis-dk1933.xml' + with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'Danish' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) + + def file_import_mixed_tags_test(self): + """ + Test the actual import of OSIS Bible file, with chapter tags containing milestone verse tags. + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'kjv.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'osis-kjv.xml' + with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'English' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) + + def file_import_milestone_tags_test(self): + """ + Test the actual import of OSIS Bible file, with milestone chapter and verse tags. + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'web.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'osis-web.xml' + with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'English' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) diff --git a/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py b/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py index 50307b401..a8a06ea76 100644 --- a/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py +++ b/tests/functional/openlp_plugins/bibles/test_zefaniaimport.py @@ -31,6 +31,7 @@ This module contains tests for the Zefania Bible importer. """ import os +import json from unittest import TestCase from tests.functional import MagicMock, patch @@ -39,29 +40,6 @@ from openlp.plugins.bibles.lib.db import BibleDB TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'bibles')) -ZEFANIA_TEST_DATA = { - 'zefania-dk1933.xml': { - 'book': 'Genesis', - 'chapter': 1, - 'verses': [ - ('1', 'I Begyndelsen skabte Gud Himmelen og Jorden.'), - ('2', 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. ' - 'Men Guds Ånd svævede over Vandene.'), - ('3', 'Og Gud sagde: "Der blive Lys!" Og der blev Lys.'), - ('4', 'Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket,'), - ('5', 'og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, ' - 'og det blev Morgen, første Dag.'), - ('6', 'Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!"'), - ('7', 'Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen ' - 'fra Vandet over Hvælvingen;'), - ('8', 'og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag.'), - ('9', 'Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" ' - 'Og således skete det;'), - ('10', 'og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, ' - 'at det var godt.') - ] - } -} class TestZefaniaImport(TestCase): @@ -94,28 +72,30 @@ class TestZefaniaImport(TestCase): def file_import_test(self): """ - Test the actual import of real song files + Test the actual import of Zefania Bible file """ # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'zefania-dk1933.xml' with patch('openlp.plugins.bibles.lib.zefania.ZefaniaBible.application'): - for bible_file in ZEFANIA_TEST_DATA: - mocked_manager = MagicMock() - mocked_import_wizard = MagicMock() - importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') - importer.wizard = mocked_import_wizard - importer.get_book_ref_id_by_name = MagicMock() - importer.create_verse = MagicMock() - importer.create_book = MagicMock() - importer.session = MagicMock() - importer.get_language = MagicMock() - importer.get_language.return_value = 'Danish' + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'Danish' - # WHEN: Importing bible file - importer.filename = os.path.join(TEST_PATH, bible_file) - importer.do_import() + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() - # THEN: The create_verse() method should have been called with each verse in the file. - self.assertTrue(importer.create_verse.called) - for verse_tag, verse_text in ZEFANIA_TEST_DATA[bible_file]['verses']: - importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py index 517732e4d..9d72677ce 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -59,6 +59,19 @@ class TestBibleHTTP(TestCase): # THEN: We should get back a valid service item assert len(books) == 66, 'The bible should not have had any books added or removed' + def bible_gateway_extract_books_support_redirect_test(self): + """ + Test the Bible Gateway retrieval of book list for DN1933 bible with redirect (bug 1251437) + """ + # GIVEN: A new Bible Gateway extraction class + handler = BGExtract() + + # WHEN: The Books list is called + books = handler.get_books_from_http('DN1933') + + # THEN: We should get back a valid service item + assert len(books) == 66, 'This bible should have 66 books' + def bible_gateway_extract_verse_test(self): """ Test the Bible Gateway retrieval of verse list for NIV bible John 3 diff --git a/tests/interfaces/openlp_plugins/media/__init__.py b/tests/interfaces/openlp_plugins/media/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_plugins/media/forms/__init__.py b/tests/interfaces/openlp_plugins/media/forms/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py new file mode 100644 index 000000000..89810aa50 --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Module to test the MediaClipSelectorForm. +""" + +import os +from unittest import TestCase, SkipTest +from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE + +if os.name == 'nt' and not VLC_AVAILABLE: + raise SkipTest('Windows without VLC, skipping this test since it cannot run without vlc') + +from PyQt4 import QtGui, QtTest, QtCore + +from openlp.core.common import Registry +from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm +from tests.interfaces import MagicMock, patch +from tests.helpers.testmixin import TestMixin + + +class TestMediaClipSelectorForm(TestCase, TestMixin): + """ + Test the EditCustomSlideForm. + """ + def setUp(self): + """ + Create the UI + """ + Registry.create() + self.get_application() + self.main_window = QtGui.QMainWindow() + Registry().register('main_window', self.main_window) + # Mock VLC so we don't actually use it + self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc') + self.vlc_patcher.start() + Registry().register('application', self.app) + # Mock the media item + self.mock_media_item = MagicMock() + # create form to test + self.form = MediaClipSelectorForm(self.mock_media_item, self.main_window, None) + mock_media_state_wait = MagicMock() + mock_media_state_wait.return_value = True + self.form.media_state_wait = mock_media_state_wait + self.form.application.set_busy_cursor = MagicMock() + self.form.application.set_normal_cursor = MagicMock() + self.form.find_optical_devices = MagicMock() + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + self.vlc_patcher.stop() + del self.form + del self.main_window + + def basic_test(self): + """ + Test if the dialog is correctly set up. + """ + # GIVEN: A mocked QDialog.exec_() method + with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + # WHEN: Show the dialog. + self.form.exec_() + + # THEN: The media path should be empty. + assert self.form.media_path_combobox.currentText() == '', 'There should not be any text in the media path.' + + def click_load_button_test(self): + """ + Test that the correct function is called when load is clicked, and that it behaves as expected. + """ + # GIVEN: Mocked methods. + with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ + mocked_critical_error_message_box,\ + patch('openlp.plugins.media.forms.mediaclipselectorform.os.path.exists') as mocked_os_path_exists,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + + # WHEN: The load button is clicked with no path set + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) + + # THEN: we should get an error + mocked_critical_error_message_box.assert_called_with(message='No path was given') + + # WHEN: The load button is clicked with a non-existing path + mocked_os_path_exists.return_value = False + self.form.media_path_combobox.insertItem(0, '/non-existing/test-path.test') + self.form.media_path_combobox.setCurrentIndex(0) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) + + # THEN: we should get an error + assert self.form.media_path_combobox.currentText() == '/non-existing/test-path.test',\ + 'The media path should be the given one.' + mocked_critical_error_message_box.assert_called_with(message='Given path does not exists') + + # WHEN: The load button is clicked with a mocked existing path + mocked_os_path_exists.return_value = True + self.form.vlc_media_player = MagicMock() + self.form.vlc_media_player.play.return_value = -1 + self.form.media_path_combobox.insertItem(0, '/existing/test-path.test') + self.form.media_path_combobox.setCurrentIndex(0) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) + + # THEN: we should get an error + assert self.form.media_path_combobox.currentText() == '/existing/test-path.test',\ + 'The media path should be the given one.' + mocked_critical_error_message_box.assert_called_with(message='VLC player failed playing the media') + + def title_combobox_test(self): + """ + Test the behavior when the title combobox is updated + """ + # GIVEN: Mocked methods and some entries in the title combobox. + with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + self.form.vlc_media_player.get_length.return_value = 1000 + self.form.audio_tracks_combobox.itemData = MagicMock() + self.form.subtitle_tracks_combobox.itemData = MagicMock() + self.form.audio_tracks_combobox.itemData.return_value = None + self.form.subtitle_tracks_combobox.itemData.return_value = None + self.form.titles_combo_box.insertItem(0, 'Test Title 0') + self.form.titles_combo_box.insertItem(1, 'Test Title 1') + + # WHEN: There exists audio and subtitle tracks and the index is updated. + self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), + (0, b'Audio Track 1')] + self.form.vlc_media_player.video_get_spu_description.return_value = [(-1, b'Disabled'), + (0, b'Subtitle Track 1')] + self.form.titles_combo_box.setCurrentIndex(1) + + # THEN: The subtitle and audio track comboboxes should be updated and get signals and call itemData. + self.form.audio_tracks_combobox.itemData.assert_any_call(0) + self.form.audio_tracks_combobox.itemData.assert_any_call(1) + self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) + + def click_save_button_test(self): + """ + Test that the correct function is called when save is clicked, and that it behaves as expected. + """ + # GIVEN: Mocked methods. + with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ + mocked_critical_error_message_box,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + + # WHEN: The save button is clicked with a NoneType in start_time_ms or end_time_ms + self.form.accept() + + # THEN: we should get an error message + mocked_critical_error_message_box.assert_called_with('DVD not loaded correctly', + 'The DVD was not loaded correctly, ' + 'please re-load and try again.') diff --git a/tests/resources/bibles/dk1933.json b/tests/resources/bibles/dk1933.json new file mode 100644 index 000000000..f364cb47e --- /dev/null +++ b/tests/resources/bibles/dk1933.json @@ -0,0 +1,16 @@ +{ + "book": "Genesis", + "chapter": 1, + "verses": [ + [ "1", "I Begyndelsen skabte Gud Himmelen og Jorden."], + [ "2", "Og Jorden var øde og tom, og der var Mørke over Verdensdybet. Men Guds Ånd svævede over Vandene." ], + [ "3", "Og Gud sagde: \"Der blive Lys!\" Og der blev Lys." ], + [ "4", "Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket," ], + [ "5", "og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, og det blev Morgen, første Dag." ], + [ "6", "Derpå sagde Gud: \"Der blive en Hvælving midt i Vandene til at skille Vandene ad!\"" ], + [ "7", "Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen fra Vandet over Hvælvingen;" ], + [ "8", "og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag." ], + [ "9", "Derpå sagde Gud: \"Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!\" Og således skete det;" ], + [ "10", "og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, at det var godt." ] + ] +} \ No newline at end of file diff --git a/tests/resources/bibles/kjv.json b/tests/resources/bibles/kjv.json new file mode 100644 index 000000000..a375a1b40 --- /dev/null +++ b/tests/resources/bibles/kjv.json @@ -0,0 +1,16 @@ +{ + "book": "Genesis", + "chapter": 1, + "verses": [ + [ "1", "In the beginning God created the heaven and the earth."], + [ "2", "And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters." ], + [ "3", "And God said, Let there be light: and there was light." ], + [ "4", "And God saw the light, that it was good: and God divided the light from the darkness." ], + [ "5", "And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day." ], + [ "6", "And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters." ], + [ "7", "And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so." ], + [ "8", "And God called the firmament Heaven. And the evening and the morning were the second day." ], + [ "9", "And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so." ], + [ "10", "And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good." ] + ] +} diff --git a/tests/resources/bibles/osis-dk1933.xml b/tests/resources/bibles/osis-dk1933.xml new file mode 100644 index 000000000..d51d073fe --- /dev/null +++ b/tests/resources/bibles/osis-dk1933.xml @@ -0,0 +1,32 @@ + + + + +
+ + Dette er Biblen + Bible.DanDetteBiblen + Bible.KJV + + + Bible.KJV + +
+
+
+ + I Begyndelsen skabte Gud Himmelen og Jorden. + Og Jorden var øde og tom, og der var Mørke over Verdensdybet. Men Guds Ånd svævede over Vandene.

+ Og Gud sagde: "Der blive Lys!" Og der blev Lys. + Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket, + og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, og det blev Morgen, første Dag.

+ Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!" + Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen fra Vandet over Hvælvingen; + og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag.

+ Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" Og således skete det; + og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, at det var godt. + +

+
+
+
diff --git a/tests/resources/bibles/osis-kjv.xml b/tests/resources/bibles/osis-kjv.xml new file mode 100644 index 000000000..72e42e3aa --- /dev/null +++ b/tests/resources/bibles/osis-kjv.xml @@ -0,0 +1,41 @@ + + + +
+ + King James Version (1769) with Strongs Numbers and Morphology + Bible.KJV + Gen-Rev + Bible.KJV + + + Bible.KJV + + + Dict.Strongs + + + Dict.Robinsons + + + Dict.strongMorph + +
+
+THE FIRST BOOK OF MOSES CALLED GENESIS + +CHAPTER 1. +In the beginning God created the heaven and the earth. +And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. +And God said, Let there be light: and there was light. +And God saw the light, that it was good: and God divided the light from the darkness.the light from…: Heb. between the light and between the darkness +And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day.And the evening…: Heb. And the evening was, and the morning was etc. +And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters.firmament: Heb. expansion +And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so. +And God called the firmament Heaven. And the evening and the morning were the second day.And the evening…: Heb. And the evening was, and the morning was etc. +And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so. +And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good. + +
+
+
diff --git a/tests/resources/bibles/osis-web.xml b/tests/resources/bibles/osis-web.xml new file mode 100644 index 000000000..515739ea8 --- /dev/null +++ b/tests/resources/bibles/osis-web.xml @@ -0,0 +1,109 @@ + + + +
+ + 2007-08-26T08.23.41 +

This draft version of the World English Bible is +substantially complete in the New Testament, Genesis, Exodus, Job, Psalms, Proverbs, Ecclesiastes, Song of Solomon, and the “minor” prophets. Editing continues on the other books of the Old Testament. All WEB companion Apocrypha books are still in +rough draft form.

+

Converted web.gbf in GBF to web.osis.xml in +an XML format that is mostly compliant with OSIS 2.0 using gbf2osis.exe. +(Please see http://ebt.cx/translation/ for links to this software.)

+

GBF and OSIS metadata fields do not exactly correspond to each other, so +the conversion is not perfect in the metadata. However, the Scripture portion +should be correct.

+

No attempt was to convert quotation marks to structural markers using q or +speech elements, because this would require language and style-dependent +processing, and because the current OSIS specification is deficient in that +quotation mark processing is not guaranteed to produce the correct results +for all languages and translations. In English texts, the hard part of the +conversion to markup is figuring out what ’ means. +The other difficulty is that OSIS in no way guarantees that these punctuation +marks would be reconstituted properly by software that reads OSIS files +for anything other than modern English, and even then, it does not +accommodate all styles of punctuation and all cases. +We strongly recommend that anyone using OSIS NOT replace quotation mark +punctuation in any existing text with q or speech elements. It is better +for multiple language processing capabilities to leave the quotation +punctuation as part of the text. If you need the q or speech markup, then you +may supplement those punctuation marks with those markup elements, but specify +the n='' parameter in those elements to indicate that no generation of any +punctuation from those markup elements is required or desired. That way you +can have BOTH correct punctuation already in the text AND markup so that you +can automatically determine when you are in a quotation or not, independent +of language. This may be useful for a search by speaker, for example.

+

The output of gbf2osis marks Jesus' words in a non-standard way using the q +element AND quotation marks if they were marked with FR/Fr markers in the GBF +file. The OSIS 2.0 specification requires that quotation marks be stripped out, +and reinserted by software that reads the OSIS files when q elements are used. +This is not acceptable for the reasons given above, and we choose not to do +that, but we used the q element with who='Jesus' to indicate Jesus' words. +Do not generate any additional punctuation due to these markers. The correct +punctuation is already in the text.

+

OSIS does not currently support footnote start anchors. Therefore, these +start anchors have been represented with milestone elements, in case someone +might like to use them, for example, to start an href element in a conversion +to HTML. (OSIS sort of supports the same idea by allowing a catchword to be +defined within a footnote, but I did not implement the processing to convert +to this different way of doing things, and it isn't exactly the same, anyway.)

+

Traditional psalm book titles are rendered as text rather than titles, because +the title element does not support containing transChange elements, as would be +required to encode the KJV text using OSIS title elements. This may actually be +a superior solution, anyway, in that the Masoretic text makes no such distinction +(even though many modern typeset Bibles do make a typographic distinction in this +case).

+

The schema location headers were modified to use local copies rather than the +standard locations so that these files could be validated and used without an +Internet connection active at all times (very important for the developer's +remote island location), but you may wish to change them back.

+
+ + World English Bible + WEB committee + 2007-08-26 + Rainbow Missions, Inc. + Bible + Bible.en.WEB.draft.2007-08-26 + http://eBible.org/web/ + ENG + Wherever English is spoken in the world. + The World English Bible is dedicated to the Public Domain by the translators and editors. It is not copyrighted. “World English Bible” and the World English Bible logo are a trademarks of Rainbow +Missions, Inc. They may only be used to identify this translation of the Holy Bible as published by Rainbow Missions, Inc., and faithful copies and quotations. “Faithful copies” include copies converted to other formats (i. e. HTML, PDF, etc.) or +typeset differently, without altering the text of the Scriptures, except that changing the spellings between preferred American and British usage is allowed. Use of the markings of direct quotes of Jesus Christ for different rendition (i. e. red text) +is optional. Comments and typo reports are welcome at http://eBible.org/cgi-bin/comment.cgi. Please see http://eBible.org/web/ for updates, revision status, free downloads, and printed edition purchase information. + Gen-Mal + Tob-AddEsth + Bar-EpJer + AddDan + Matt-Rev + Bible.WEB + +
+
+
+ Genesis + +

+ +In the beginning GodThe Hebrew word rendered “God” is “Elohim.” After “God,” the Hebrew has the two letters “Aleph Tav” (the first and last letters of the Hebrew alphabet) as a grammatical marker. created the heavens and the earth. +Now the earth was formless and empty. Darkness was on the surface of the deep. God’s Spirit was hovering over the surface of the waters.

+

+ +God said, “Let there be light,” and there was light. +God saw the light, and saw that it was good. God divided the light from the darkness. +God called the light “day,” and the darkness he called “night.” There was evening and there was morning, one day.

+

+ +God said, “Let there be an expanse in the middle of the waters, and let it divide the waters from the waters.” +God made the expanse, and divided the waters which were under the expanse from the waters which were above the expanse; and it was so. +God called the expanse “sky.” There was evening and there was morning, a second day.

+

+ +God said, “Let the waters under the sky be gathered together to one place, and let the dry land appear;” and it was so. +God called the dry land “earth,” and the gathering together of the waters he called “seas.” God saw that it was good. +

+
+
+
+
diff --git a/tests/resources/bibles/web.json b/tests/resources/bibles/web.json new file mode 100644 index 000000000..0fbc95669 --- /dev/null +++ b/tests/resources/bibles/web.json @@ -0,0 +1,16 @@ +{ + "book": "Genesis", + "chapter": "1", + "verses": [ + [ "1", "In the beginning God created the heavens and the earth."], + [ "2", "Now the earth was formless and empty. Darkness was on the surface of the deep. God’s Spirit was hovering over the surface of the waters." ], + [ "3", "God said, “Let there be light,” and there was light." ], + [ "4", "God saw the light, and saw that it was good. God divided the light from the darkness." ], + [ "5", "God called the light “day,” and the darkness he called “night.” There was evening and there was morning, one day." ], + [ "6", "God said, “Let there be an expanse in the middle of the waters, and let it divide the waters from the waters.”" ], + [ "7", "God made the expanse, and divided the waters which were under the expanse from the waters which were above the expanse; and it was so." ], + [ "8", "God called the expanse “sky.” There was evening and there was morning, a second day." ], + [ "9", "God said, “Let the waters under the sky be gathered together to one place, and let the dry land appear;” and it was so." ], + [ "10", "God called the dry land “earth,” and the gathering together of the waters he called “seas.” God saw that it was good." ] + ] +} diff --git a/tests/resources/serviceitem-dvd.osj b/tests/resources/serviceitem-dvd.osj new file mode 100644 index 000000000..997dbd539 --- /dev/null +++ b/tests/resources/serviceitem-dvd.osj @@ -0,0 +1 @@ +[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 18, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "First DVD Clip", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:First DVD Clip:/dev/sr0", "title": "/dev/sr0"}]}}]