diff --git a/openlp/core/common/json.py b/openlp/core/common/json.py index 4a54e41b3..06a6f9eac 100644 --- a/openlp/core/common/json.py +++ b/openlp/core/common/json.py @@ -111,6 +111,8 @@ class OpenLPJSONDecoder(JSONDecoder): :param dict obj: A decoded JSON object :return: The custom object from the serialized data if the custom object is registered, else obj """ + if '__Path__' in obj: + return PathSerializer.encode_json(obj, **self.kwargs) try: key = obj['json_meta']['class'] except KeyError: @@ -150,8 +152,8 @@ class OpenLPJSONEncoder(JSONEncoder): if isinstance(obj, JSONMixin): return obj.json_object() elif obj.__class__.__name__ in _registered_classes: - return _registered_classes[obj.__class__.__name__].json_object(obj) - return super().default(obj) + return _registered_classes[obj.__class__.__name__].json_object(obj, **self.kwargs) + return super().default(obj, **self.kwargs) def is_serializable(obj): @@ -174,17 +176,22 @@ class PathSerializer(JSONMixin, register_names=('Path', 'PosixPath', 'WindowsPat :param kwargs: Contains any extra parameters. Not used! :return Path: The deserialized Path object """ - path = Path(*obj['parts']) + if '__Path__' in obj: + parts = obj['__Path__'] + else: + parts = obj['parts'] + path = Path(*parts) if base_path and not path.is_absolute(): return base_path / path return path @classmethod - def json_object(cls, obj, base_path=None, **kwargs): + def json_object(cls, obj, base_path=None, is_js=False, **kwargs): """ Create a dictionary that can be JSON decoded. :param Path base_path: If specified, an absolute path to make a relative path from. + :param bool is_js: Encode the path as a uri. For example for use in the js rendering code. :param kwargs: Contains any extra parameters. Not used! :return: The dictionary representation of this Path object. :rtype: dict[tuple] @@ -193,6 +200,8 @@ class PathSerializer(JSONMixin, register_names=('Path', 'PosixPath', 'WindowsPat if base_path: with suppress(ValueError): path = path.relative_to(base_path) + if is_js is True: + return path.as_uri() json_dict = {'parts': path.parts} cls.attach_meta(json_dict) return json_dict diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index f974d949b..6fbb8a8ed 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -210,6 +210,8 @@ class Settings(QtCore.QSettings): 'media/media auto start': QtCore.Qt.Unchecked, 'media/stream command': '', 'media/vlc arguments': '', + 'media/video': '', + 'media/audio': '', 'remotes/download version': '0.0', 'players/background color': '#000000', 'servicemanager/last directory': None, @@ -610,7 +612,7 @@ class Settings(QtCore.QSettings): elif isinstance(default_value, dict): return {} elif isinstance(setting, str): - if 'json_meta' in setting or setting.startswith('{'): + if 'json_meta' in setting or '__Path__' in setting or setting.startswith('{'): return json.loads(setting, cls=OpenLPJSONDecoder) # Convert the setting to the correct type. if isinstance(default_value, bool): diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js index 02980a393..e198eeee6 100644 --- a/openlp/core/display/html/display.js +++ b/openlp/core/display/html/display.js @@ -117,20 +117,6 @@ function _prepareText(text) { return "

" + _nl2br(text) + "

"; } -/** - * The paths we get are JSON versions of Python Path objects, so let's just fix that. - * @private - * @param {object} path - The Path object - * @returns {string} The actual file path - */ -function _pathToString(path) { - var filename = path.__Path__.join("/").replace("//", "/"); - if (!filename.startsWith("/")) { - filename = "/" + filename; - } - return filename; -} - /** * An audio player with a play list */ @@ -676,13 +662,13 @@ var Display = { } break; case BackgroundType.Image: - background_filename = _pathToString(theme.background_filename); - backgroundStyle["background-image"] = "url('file://" + background_filename + "')"; + backgroundStyle["background-image"] = "url('" + theme.background_filename + "')"; + console.warn(backgroundStyle["background-image"]); break; case BackgroundType.Video: - background_filename = _pathToString(theme.background_filename); backgroundStyle["background-color"] = theme.background_border_color; - backgroundHtml = ""; + backgroundHtml = ""; + console.warn(backgroundHtml); break; default: backgroundStyle["background"] = "#000"; diff --git a/openlp/core/display/window.py b/openlp/core/display/window.py index c8d99fd22..393b6ccce 100644 --- a/openlp/core/display/window.py +++ b/openlp/core/display/window.py @@ -332,9 +332,9 @@ class DisplayWindow(QtWidgets.QWidget): theme_copy = copy.deepcopy(theme) theme_copy.background_type = 'image' theme_copy.background_filename = self.checkerboard_path - exported_theme = theme_copy.export_theme() + exported_theme = theme_copy.export_theme(is_js=True) else: - exported_theme = theme.export_theme() + exported_theme = theme.export_theme(is_js=True) self.run_javascript('Display.setTheme({theme});'.format(theme=exported_theme)) def get_video_types(self): diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 75973f389..13b7c848f 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -173,6 +173,7 @@ class ItemCapabilities(object): HasNotes = 20 HasThumbnails = 21 HasMetaData = 22 + CanStream = 23 def get_text_file_string(text_file_path): diff --git a/openlp/core/lib/theme.py b/openlp/core/lib/theme.py index d5219a5d1..bc5ba69b8 100644 --- a/openlp/core/lib/theme.py +++ b/openlp/core/lib/theme.py @@ -225,17 +225,18 @@ class Theme(object): jsn = json.loads(theme, cls=OpenLPJSONDecoder) self.expand_json(jsn) - def export_theme(self, theme_path=None): + def export_theme(self, theme_path=None, is_js=False): """ Loop through the fields and build a dictionary of them + :param pathlib.Path | None theme_path: + :param bool is_js: For internal use, for example with the theme js code. + :return str: The json encoded theme object """ theme_data = {} for attr, value in self.__dict__.items(): theme_data["{attr}".format(attr=attr)] = value - if theme_path: - return json.dumps(theme_data, cls=OpenLPJSONEncoder, base_path=theme_path) - return json.dumps(theme_data, cls=OpenLPJSONEncoder) + return json.dumps(theme_data, cls=OpenLPJSONEncoder, base_path=theme_path, is_js=is_js) def parse(self, xml): """ diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 3c079b7d0..d63d0e9ba 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -26,6 +26,19 @@ import logging log = logging.getLogger(__name__ + '.__init__') +# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source +AUDIO_EXT = ['*.3ga', '*.669', '*.a52', '*.aac', '*.ac3', '*.adt', '*.adts', '*.aif', '*.aifc', '*.aiff', '*.amr', + '*.aob', '*.ape', '*.awb', '*.caf', '*.dts', '*.flac', '*.it', '*.kar', '*.m4a', '*.m4b', '*.m4p', '*.m5p', + '*.mid', '*.mka', '*.mlp', '*.mod', '*.mpa', '*.mp1', '*.mp2', '*.mp3', '*.mpc', '*.mpga', '*.mus', + '*.oga', '*.ogg', '*.oma', '*.opus', '*.qcp', '*.ra', '*.rmi', '*.s3m', '*.sid', '*.spx', '*.thd', '*.tta', + '*.voc', '*.vqf', '*.w64', '*.wav', '*.wma', '*.wv', '*.xa', '*.xm'] +VIDEO_EXT = ['*.3g2', '*.3gp', '*.3gp2', '*.3gpp', '*.amv', '*.asf', '*.avi', '*.bik', '*.divx', '*.drc', '*.dv', + '*.f4v', '*.flv', '*.gvi', '*.gxf', '*.iso', '*.m1v', '*.m2v', '*.m2t', '*.m2ts', '*.m4v', '*.mkv', + '*.mov', '*.mp2', '*.mp2v', '*.mp4', '*.mp4v', '*.mpe', '*.mpeg', '*.mpeg1', '*.mpeg2', '*.mpeg4', '*.mpg', + '*.mpv2', '*.mts', '*.mtv', '*.mxf', '*.mxg', '*.nsv', '*.nuv', '*.ogg', '*.ogm', '*.ogv', '*.ogx', '*.ps', + '*.rec', '*.rm', '*.rmvb', '*.rpl', '*.thp', '*.tod', '*.ts', '*.tts', '*.txd', '*.vob', '*.vro', '*.webm', + '*.wm', '*.wmv', '*.wtv', '*.xesc', '*.nut', '*.rv', '*.xvid'] + class MediaState(object): """ diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 4938c5f39..588bf636e 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -42,9 +42,9 @@ from openlp.core.common.settings import Settings from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import DisplayControllerType -from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path +from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, VIDEO_EXT, AUDIO_EXT from openlp.core.ui.media.endpoint import media_endpoint -from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT, VlcPlayer, get_vlc +from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc log = logging.getLogger(__name__) @@ -184,7 +184,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): display.has_audio = False self.vlc_player.setup(display, preview) - def set_controls_visible(self, controller, value): + @staticmethod + def set_controls_visible(controller, value): """ After a new display is configured, all media related widget will be created too @@ -229,7 +230,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): display = self._define_display(controller) if controller.is_live: # if this is an optical device use special handling - if service_item.is_capable(ItemCapabilities.IsOptical): + if service_item.is_capable(ItemCapabilities.CanStream): + is_valid = self._check_file_type(controller, display, True) + controller.media_info.media_type = MediaType.Stream + elif 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) @@ -249,7 +253,10 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.media_info.start_time = service_item.start_time controller.media_info.end_time = service_item.end_time elif controller.preview_display: - if service_item.is_capable(ItemCapabilities.IsOptical): + if service_item.is_capable(ItemCapabilities.CanStream): + controller.media_info.media_type = MediaType.Stream + is_valid = self._check_file_type(controller, display, True) + elif 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) @@ -270,6 +277,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): # display.frame.runJavaScript('show_video("setBackBoard", null, null,"visible");') # now start playing - Preview is autoplay! autoplay = False + if service_item.is_capable(ItemCapabilities.CanStream): + autoplay = True # Preview requested if not controller.is_live: autoplay = True @@ -346,13 +355,21 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.media_info.media_type = MediaType.DVD return True - def _check_file_type(self, controller, display): + def _check_file_type(self, controller, display, stream=False): """ Select the correct media Player type from the prioritized Player list :param controller: First element is the controller which should be used :param display: Which display to use + :param stream: Are we streaming or not """ + if stream: + self.resize(display, self.vlc_player) + display.media_info.media_type = MediaType.Stream + if self.vlc_player.load(display, None): + self.current_media_players[controller.controller_type] = self.vlc_player + return True + return True for file in controller.media_info.file_info: if file.is_file: suffix = '*%s' % file.suffix.lower() diff --git a/openlp/core/ui/media/mediatab.py b/openlp/core/ui/media/mediatab.py index ac4e66f42..c844129ef 100644 --- a/openlp/core/ui/media/mediatab.py +++ b/openlp/core/ui/media/mediatab.py @@ -33,9 +33,9 @@ from openlp.core.common.settings import Settings from openlp.core.lib.settingstab import SettingsTab from openlp.core.ui.icons import UiIcons -LINUX_STREAM = 'v4l2://{video} :v4l2-standard= :input-slave={audio} :live-caching=300' +LINUX_STREAM = 'v4l2://{video}:v4l2-standard= :input-slave=alsa://{audio} :live-caching=300' WIN_STREAM = 'dshow://:dshow-vdev={video} :dshow-adev={audio} :live-caching=300' -OSX_STREAM = 'avcapture://{video} :qtsound://{audio} :live-caching=300' +OSX_STREAM = 'avcapture://{video}:qtsound://{audio} :live-caching=300' log = logging.getLogger(__name__) @@ -68,11 +68,15 @@ class MediaTab(SettingsTab): self.left_layout.addWidget(self.live_media_group_box) self.stream_media_group_box = QtWidgets.QGroupBox(self.left_column) self.stream_media_group_box.setObjectName('stream_media_group_box') - self.stream_media_layout = QtWidgets.QHBoxLayout(self.stream_media_group_box) + self.stream_media_layout = QtWidgets.QFormLayout(self.stream_media_group_box) self.stream_media_layout.setObjectName('stream_media_layout') self.stream_media_layout.setContentsMargins(0, 0, 0, 0) - self.stream_edit = QtWidgets.QLabel(self) - self.stream_media_layout.addWidget(self.stream_edit) + self.video_edit = QtWidgets.QLineEdit(self) + self.stream_media_layout.addRow(translate('MediaPlugin.MediaTab', 'Video:'), self.video_edit) + self.audio_edit = QtWidgets.QLineEdit(self) + self.stream_media_layout.addRow(translate('MediaPlugin.MediaTab', 'Audio:'), self.audio_edit) + self.stream_cmd = QtWidgets.QLabel(self) + self.stream_media_layout.addWidget(self.stream_cmd) self.left_layout.addWidget(self.stream_media_group_box) self.vlc_arguments_group_box = QtWidgets.QGroupBox(self.left_column) self.vlc_arguments_group_box.setObjectName('vlc_arguments_group_box') @@ -84,7 +88,6 @@ class MediaTab(SettingsTab): self.left_layout.addWidget(self.vlc_arguments_group_box) self.left_layout.addStretch() self.right_layout.addStretch() - # # Signals and slots def retranslate_ui(self): """ @@ -100,22 +103,28 @@ class MediaTab(SettingsTab): Load the settings """ self.auto_start_check_box.setChecked(Settings().value(self.settings_section + '/media auto start')) - self.stream_edit.setText(Settings().value(self.settings_section + '/stream command')) - if not self.stream_edit.text(): - if is_linux: - self.stream_edit.setText(LINUX_STREAM) - elif is_win: - self.stream_edit.setText(WIN_STREAM) - else: - self.stream_edit.setText(OSX_STREAM) + self.stream_cmd.setText(Settings().value(self.settings_section + '/stream command')) + self.audio_edit.setText(Settings().value(self.settings_section + '/audio')) + self.video_edit.setText(Settings().value(self.settings_section + '/video')) + if not self.stream_cmd.text(): + self.set_base_stream() self.vlc_arguments_edit.setPlainText(Settings().value(self.settings_section + '/vlc arguments')) if Settings().value('advanced/experimental'): + # vlc.MediaPlayer().audio_output_device_enum() for cam in QCameraInfo.availableCameras(): log.debug(cam.deviceName()) log.debug(cam.description()) for au in QAudioDeviceInfo.availableDevices(QAudio.AudioInput): log.debug(au.deviceName()) + def set_base_stream(self): + if is_linux: + self.stream_cmd.setText(LINUX_STREAM) + elif is_win: + self.stream_cmd.setText(WIN_STREAM) + else: + self.stream_cmd.setText(OSX_STREAM) + def save(self): """ Save the settings @@ -123,8 +132,12 @@ class MediaTab(SettingsTab): setting_key = self.settings_section + '/media auto start' if Settings().value(setting_key) != self.auto_start_check_box.checkState(): Settings().setValue(setting_key, self.auto_start_check_box.checkState()) - Settings().setValue(self.settings_section + '/stream command', self.stream_edit.text()) + Settings().setValue(self.settings_section + '/stream command', self.stream_cmd.text()) Settings().setValue(self.settings_section + '/vlc arguments', self.vlc_arguments_edit.toPlainText()) + Settings().setValue(self.settings_section + '/video', self.video_edit.text()) + Settings().setValue(self.settings_section + '/audio', self.audio_edit.text()) + self.stream_cmd.setText(self.stream_cmd.text().format(video=self.video_edit.text(), + audio=self.audio_edit.text())) def post_set_up(self, post_update=False): """ diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 055d599aa..eb0f3dceb 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -40,18 +40,6 @@ from openlp.core.ui.media.mediaplayer import MediaPlayer log = logging.getLogger(__name__) # Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source -AUDIO_EXT = ('3ga', '669', 'a52', 'aac', 'ac3', 'adt', 'adts', 'aif', 'aifc', 'aiff', 'amr', 'aob', 'ape', 'awb', 'caf', - 'dts', 'flac', 'it', 'kar', 'm4a', 'm4b', 'm4p', 'm5p', 'mid', 'mka', 'mlp', 'mod', 'mpa', 'mp1', 'mp2', - 'mp3', 'mpc', 'mpga', 'mus', 'oga', 'ogg', 'oma', 'opus', 'qcp', 'ra', 'rmi', 's3m', 'sid', 'spx', 'thd', - 'tta', 'voc', 'vqf', 'w64', 'wav', 'wma', 'wv', 'xa', 'xm') - -VIDEO_EXT = ('3g2', '3gp', '3gp2', '3gpp', 'amv', 'asf', 'avi', 'bik', 'divx', 'drc', 'dv', 'f4v', 'flv', 'gvi', 'gxf', - 'iso', 'm1v', 'm2v', 'm2t', 'm2ts', 'm4v', 'mkv', 'mov', 'mp2', 'mp2v', 'mp4', 'mp4v', 'mpe', 'mpeg', - 'mpeg1', 'mpeg2', 'mpeg4', 'mpg', 'mpv2', 'mts', 'mtv', 'mxf', 'mxg', 'nsv', 'nuv', 'ogg', 'ogm', 'ogv', - 'ogx', 'ps', 'rec', 'rm', 'rmvb', 'rpl', 'thp', 'tod', 'ts', 'tts', 'txd', 'vob', 'vro', 'webm', 'wm', - 'wmv', 'wtv', 'xesc', - # These extensions was not in the official list, added manually. - 'nut', 'rv', 'xvid') def get_vlc(): @@ -159,16 +147,15 @@ class VlcPlayer(MediaPlayer): Load a video into VLC :param output_display: The display where the media is - :param file: file to be played + :param file: file to be played or None for live streaming :return: """ vlc = get_vlc() log.debug('load vid in Vlc Controller') - controller = output_display - volume = controller.media_info.volume - path = os.path.normcase(file) + if file: + path = os.path.normcase(file) # create the media - if controller.media_info.media_type == MediaType.CD: + if output_display.media_info.media_type == MediaType.CD: if is_win(): path = '/' + path output_display.vlc_media = output_display.vlc_instance.media_new_location('cdda://' + path) @@ -180,8 +167,8 @@ class VlcPlayer(MediaPlayer): audio_cd_tracks = output_display.vlc_media.subitems() if not audio_cd_tracks or audio_cd_tracks.count() < 1: return False - output_display.vlc_media = audio_cd_tracks.item_at_index(controller.media_info.title_track) - elif controller.media_info.media_type == MediaType.Stream: + output_display.vlc_media = audio_cd_tracks.item_at_index(output_display.media_info.title_track) + elif output_display.media_info.media_type == MediaType.Stream: stream_cmd = Settings().value('media/stream command') output_display.vlc_media = output_display.vlc_instance.media_new_location(stream_cmd) else: @@ -190,7 +177,7 @@ class VlcPlayer(MediaPlayer): output_display.vlc_media_player.set_media(output_display.vlc_media) # parse the metadata of the file output_display.vlc_media.parse() - self.volume(output_display, volume) + self.volume(output_display, output_display.media_info.volume) return True def media_state_wait(self, output_display, media_state): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 77c50e43c..92b34badd 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -48,7 +48,7 @@ from openlp.core.lib.plugin import PluginStatus from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box from openlp.core.ui.icons import UiIcons -from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT +from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT from openlp.core.ui.serviceitemeditform import ServiceItemEditForm from openlp.core.ui.servicenoteform import ServiceNoteForm from openlp.core.ui.starttimeform import StartTimeForm diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 183993500..82a71175f 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -32,7 +32,7 @@ from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry from openlp.core.lib.theme import BackgroundGradientType, BackgroundType from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media.vlcplayer import VIDEO_EXT +from openlp.core.ui.media import VIDEO_EXT from openlp.core.ui.themelayoutform import ThemeLayoutForm from openlp.core.ui.themewizard import Ui_ThemeWizard diff --git a/openlp/core/widgets/widgets.py b/openlp/core/widgets/widgets.py index fff21666e..0415d73a5 100644 --- a/openlp/core/widgets/widgets.py +++ b/openlp/core/widgets/widgets.py @@ -103,8 +103,8 @@ class ProxyWidget(QtWidgets.QGroupBox): :param QtWidgets.QRadioButton button: The button that has toggled :param bool checked: The buttons new state """ - id = self.radio_group.id(button) # The work around (see above comment) - enable_manual_edits = id == ProxyMode.MANUAL_PROXY and checked + group_id = self.radio_group.id(button) # The work around (see above comment) + enable_manual_edits = group_id == ProxyMode.MANUAL_PROXY and checked self.http_edit.setEnabled(enable_manual_edits) self.https_edit.setEnabled(enable_manual_edits) self.username_edit.setEnabled(enable_manual_edits) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 30962967a..4ccc55d36 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -37,8 +37,8 @@ from openlp.core.lib.mediamanageritem import MediaManagerItem from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.icons import UiIcons -from openlp.core.ui.media import parse_optical_path, format_milliseconds -from openlp.core.ui.media.vlcplayer import AUDIO_EXT, VIDEO_EXT, get_vlc +from openlp.core.ui.media import parse_optical_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT +from openlp.core.ui.media.vlcplayer import get_vlc if get_vlc() is not None: @@ -175,7 +175,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return False filename = str(item.data(QtCore.Qt.UserRole)) # Special handling if the filename is a optical clip - if filename.startswith('optical:'): + if filename == 'live': + service_item.processor = 'vlc' + service_item.title = filename + service_item.add_capability(ItemCapabilities.CanStream) + elif 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: @@ -232,9 +236,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ # self.populate_display_types() self.on_new_file_masks = translate('MediaPlugin.MediaItem', - 'Videos (*.{video});;Audio (*.{audio});;{files} ' - '(*)').format(video=' *.'.join(VIDEO_EXT), - audio=' *.'.join(AUDIO_EXT), + 'Videos ({video});;Audio ({audio});;{files} ' + '(*)').format(video=' '.join(VIDEO_EXT), + audio=' '.join(AUDIO_EXT), files=UiStrings().AllFiles) def on_delete_click(self): @@ -258,6 +262,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ # TODO needs to be fixed as no idea why this fails # media.sort(key=lambda file_path: get_natural_key(file_path.name)) + file_name = translate('MediaPlugin.MediaItem', 'Live Stream') + item_name = QtWidgets.QListWidgetItem(file_name) + item_name.setIcon(UiIcons().video) + item_name.setData(QtCore.Qt.UserRole, 'live') + item_name.setToolTip(translate('MediaPlugin.MediaItem', 'Show Live Stream')) + self.list_view.addItem(item_name) for track in media: track_str = str(track) track_info = QtCore.QFileInfo(track_str) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index fa0284cea..160b68ac1 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -129,7 +129,7 @@ class PresentationDocument(object): thumbnail_folder_path = self.get_thumbnail_folder() temp_folder_path = self.get_temp_folder() if thumbnail_folder_path.exists(): - thumbnail_folder_path.rmtree() + shutil.rmtree(thumbnail_folder_path) if temp_folder_path.exists(): shutil.rmtree(temp_folder_path) except OSError: diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index 23298d849..61bc71952 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -88,52 +88,72 @@ class SongReviewWidget(QtWidgets.QWidget): self.song_alternate_title_content.setText(self.song.alternate_title) self.song_alternate_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.song_alternate_title_content) + # Add last modified date. + self.song_last_modified_label = QtWidgets.QLabel(self) + self.song_last_modified_label.setObjectName('last_modified_label') + self.song_last_modified_label.setText('Last Modified:') + self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.song_last_modified_label) + self.song_last_modified_content = QtWidgets.QLabel(self) + self.song_last_modified_content.setObjectName('last_modified_content') + self.song_last_modified_content.setText(self.song.last_modified.strftime("%Y-%m-%d %H:%M:%S")) + self.song_last_modified_content.setWordWrap(True) + self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.song_last_modified_content) + # Add Theme widget. + self.song_theme_label = QtWidgets.QLabel(self) + self.song_theme_label.setObjectName('song_theme_label') + self.song_theme_label.setText('Theme:') + self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.song_theme_label) + self.song_theme_content = QtWidgets.QLabel(self) + self.song_theme_content.setObjectName('song_theme_content') + self.song_theme_content.setText(self.song.theme_name) + self.song_theme_content.setWordWrap(True) + self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.song_theme_content) # Add CCLI number widget. self.song_ccli_number_label = QtWidgets.QLabel(self) self.song_ccli_number_label.setObjectName('song_ccli_number_label') - self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.song_ccli_number_label) + self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.song_ccli_number_label) self.song_ccli_number_content = QtWidgets.QLabel(self) self.song_ccli_number_content.setObjectName('song_ccli_number_content') self.song_ccli_number_content.setText(self.song.ccli_number) self.song_ccli_number_content.setWordWrap(True) - self.song_info_form_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.song_ccli_number_content) + self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.song_ccli_number_content) # Add copyright widget. self.song_copyright_label = QtWidgets.QLabel(self) self.song_copyright_label.setObjectName('song_copyright_label') - self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.song_copyright_label) + self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.song_copyright_label) self.song_copyright_content = QtWidgets.QLabel(self) self.song_copyright_content.setObjectName('song_copyright_content') self.song_copyright_content.setWordWrap(True) self.song_copyright_content.setText(self.song.copyright) - self.song_info_form_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.song_copyright_content) + self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.song_copyright_content) # Add comments widget. self.song_comments_label = QtWidgets.QLabel(self) self.song_comments_label.setObjectName('song_comments_label') - self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.song_comments_label) + self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.song_comments_label) self.song_comments_content = QtWidgets.QLabel(self) self.song_comments_content.setObjectName('song_comments_content') self.song_comments_content.setText(self.song.comments) self.song_comments_content.setWordWrap(True) - self.song_info_form_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.song_comments_content) + self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.song_comments_content) # Add authors widget. self.song_authors_label = QtWidgets.QLabel(self) self.song_authors_label.setObjectName('song_authors_label') - self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.song_authors_label) + self.song_info_form_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.song_authors_label) self.song_authors_content = QtWidgets.QLabel(self) self.song_authors_content.setObjectName('song_authors_content') self.song_authors_content.setWordWrap(True) authors_text = ', '.join([author.display_name for author in self.song.authors]) self.song_authors_content.setText(authors_text) - self.song_info_form_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.song_authors_content) + self.song_info_form_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.song_authors_content) # Add verse order widget. self.song_verse_order_label = QtWidgets.QLabel(self) self.song_verse_order_label.setObjectName('song_verse_order_label') - self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.song_verse_order_label) + self.song_info_form_layout.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.song_verse_order_label) self.song_verse_order_content = QtWidgets.QLabel(self) self.song_verse_order_content.setObjectName('song_verse_order_content') self.song_verse_order_content.setText(self.song.verse_order) self.song_verse_order_content.setWordWrap(True) - self.song_info_form_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.song_verse_order_content) + self.song_info_form_layout.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.song_verse_order_content) self.song_group_box_layout.addLayout(self.song_info_form_layout) # Add verses widget. self.song_info_verse_list_widget = QtWidgets.QTableWidget(self.song_group_box) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index f9a1fd2f8..fdeacdd3f 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -423,7 +423,7 @@ class SongsPlugin(Plugin): """ Remove temporary songs from the database """ - songs = self.manager.get_all_objects(Song, Song.temporary is True) + songs = self.manager.get_all_objects(Song, Song.temporary == True) # noqa: E712 for song in songs: self.manager.delete_object(Song, song.id) diff --git a/tests/functional/openlp_core/ui/media/test_mediacontroller.py b/tests/functional/openlp_core/ui/media/test_mediacontroller.py index b0e41c19c..3c8eb17f1 100644 --- a/tests/functional/openlp_core/ui/media/test_mediacontroller.py +++ b/tests/functional/openlp_core/ui/media/test_mediacontroller.py @@ -27,6 +27,7 @@ from unittest.mock import MagicMock, patch from openlp.core.common.registry import Registry from openlp.core.ui.media.mediacontroller import MediaController +from openlp.core.ui.media import ItemMediaInfo from tests.helpers.testmixin import TestMixin from tests.utils.constants import RESOURCE_PATH @@ -57,7 +58,7 @@ class TestMediaController(TestCase, TestMixin): # THEN: The player's resize method should be called correctly mocked_player.resize.assert_called_with(mocked_display) - def test_check_file_type(self): + def test_check_file_type_null(self): """ Test that we don't try to play media when no players available """ @@ -71,7 +72,47 @@ class TestMediaController(TestCase, TestMixin): ret = media_controller._check_file_type(mocked_controller, mocked_display) # THEN: it should return False - assert ret is False, '_check_file_type should return False when no mediaplayers are available.' + assert ret is False, '_check_file_type should return False when no media file matches.' + + def test_check_file_video(self): + """ + Test that we process a file that is valid + """ + # GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item + media_controller = MediaController() + mocked_controller = MagicMock() + mocked_display = MagicMock() + media_controller.media_players = MagicMock() + mocked_controller.media_info = ItemMediaInfo() + mocked_controller.media_info.file_info = [TEST_PATH / 'mp3_file.mp3'] + media_controller.current_media_players = {} + media_controller.vlc_player = MagicMock() + + # WHEN: calling _check_file_type when no players exists + ret = media_controller._check_file_type(mocked_controller, mocked_display) + + # THEN: it should return False + assert ret is True, '_check_file_type should return True when audio file is present and matches.' + + def test_check_file_audio(self): + """ + Test that we process a file that is valid + """ + # GIVEN: A mocked UiStrings, get_used_players, controller, display and service_item + media_controller = MediaController() + mocked_controller = MagicMock() + mocked_display = MagicMock() + media_controller.media_players = MagicMock() + mocked_controller.media_info = ItemMediaInfo() + mocked_controller.media_info.file_info = [TEST_PATH / 'mp4_file.mp4'] + media_controller.current_media_players = {} + media_controller.vlc_player = MagicMock() + + # WHEN: calling _check_file_type when no players exists + ret = media_controller._check_file_type(mocked_controller, mocked_display) + + # THEN: it should return False + assert ret is True, '_check_file_type should return True when media file is present and matches.' def test_media_play_msg(self): """