diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 4ab94a250..32ef5c5a3 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 @@ -126,7 +126,7 @@ 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': + if is_win(): application_stylesheet += NT_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) @@ -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/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 aa0851ecb..2d9e396e1 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 + ``HasDisplayTitle`` The item contains 'displaytitle' on every frame which should be preferred over 'title' when displaying the item @@ -139,9 +142,10 @@ class ItemCapabilities(object): HasBackgroundAudio = 15 CanAutoStartForLive = 16 CanEditTitle = 17 - HasDisplayTitle = 18 - HasNotes = 19 - HasThumbnails = 20 + IsOptical = 18 + HasDisplayTitle = 19 + HasNotes = 20 + HasThumbnails = 21 class ServiceItem(RegistryProperties): @@ -441,7 +445,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'], text_image.get('display_title', ''), text_image.get('notes', '')) @@ -453,7 +460,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: @@ -521,7 +529,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): """ @@ -579,7 +588,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']) @@ -649,12 +658,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..cbc35e28d 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -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 @@ -275,7 +275,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/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..1a6b688c7 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 @@ -289,7 +290,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 +324,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 +333,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 +427,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 +1074,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/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..ecd1f718e 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -123,7 +123,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 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..28d37f32e --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -0,0 +1,665 @@ +# -*- 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 +if os.name == 'nt': + from win32com.client import Dispatch + import string +import sys + +if sys.platform.startswith('linux'): + import dbus +import logging +import re +from time import sleep +from datetime import datetime + + +from PyQt4 import QtCore, QtGui + +from openlp.core.common import translate +from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector +from openlp.core.lib.ui import critical_error_message_box +from openlp.core.ui.media import format_milliseconds +try: + from openlp.core.ui.media.vendor import vlc +except (ImportError, NameError, NotImplementedError): + pass +except OSError as e: + if sys.platform.startswith('win'): + if not isinstance(e, WindowsError) and e.winerror != 126: + raise + else: + raise + +log = logging.getLogger(__name__) + + +class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): + """ + 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 sys.platform == "win32": + self.vlc_media_player.set_hwnd(win_id) + elif sys.platform == "darwin": + # 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_title_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() + 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) + 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) + 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) + 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) + 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) + return + self.vlc_media_player.audio_set_mute(True) + if not self.audio_cd: + # 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]) + # Main title is usually title #1 + if len(titles) > 1: + self.titles_combo_box.setCurrentIndex(1) + else: + self.titles_combo_box.setCurrentIndex(0) + # Enable audio track combobox if anything is in it + if len(titles) > 0: + self.titles_combo_box.setDisabled(False) + self.toggle_disable_load_media(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 + 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') + return + self.vlc_media_player.audio_set_mute(True) + # Sleep 1 second to make sure VLC has the needed metadata + sleep(1) + # pause + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + self.vlc_media_player.audio_set_mute(False) + 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') + return + self.vlc_media_player.audio_set_mute(True) + # Sleep 1 second to make sure VLC has the needed metadata + sleep(1) + self.vlc_media_player.set_time(0) + # Get audio tracks, insert in combobox + audio_tracks = self.vlc_media_player.audio_get_track_description() + 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()) + + @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 on_save_button_clicked') + 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: + 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()) + 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 > 30: + 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 os.name == 'nt': + # 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 sys.platform.startswith('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 sys.platform.startswith('darwin'): + # 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 b59cc62c5..92b23bda6 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() @@ -265,7 +267,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: @@ -298,7 +300,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() @@ -319,7 +321,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) @@ -336,7 +338,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 55551fcab..cf0f5bb99 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -33,7 +33,9 @@ This module is for controlling powerpoint. 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 win32com import winreg @@ -70,7 +72,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 @@ -78,7 +80,7 @@ class PowerpointController(PresentationController): pass return False - if os.name == 'nt': + if is_win(): def start_process(self): """ Loads PowerPoint process. @@ -273,7 +275,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 35704a22f..4aea501b4 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -35,7 +35,9 @@ import re from xml.etree import ElementTree -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from ctypes import cdll from ctypes.wintypes import RECT @@ -68,11 +70,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/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..d61166acc 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -139,6 +139,7 @@ media_stop.png media_audio.png media_video.png + media_optical.png slidecontroller_multimedia.png auto-start_active.png auto-start_inactive.png 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 09812fd7f..0d349813e 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -242,3 +242,24 @@ class TestServiceItem(TestCase): # THEN: verify that it is setup as a Command and that the frame data matches self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command') self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match') + + 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_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/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..e69de29bb 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..e97a06238 --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -0,0 +1,157 @@ +# -*- 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() + # 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 + + 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) 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"}]}}]