openlp/openlp/core/lib/serviceitem.py

944 lines
41 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-02-06 09:10:17 +00:00
# Copyright (c) 2008-2022 OpenLP Developers #
2019-04-13 13:00:22 +00:00
# ---------------------------------------------------------------------- #
# 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
2010-06-10 21:30:50 +00:00
"""
The :mod:`serviceitem` provides the service item functionality including the
type and capability of an item.
"""
2011-02-13 10:37:27 +00:00
import datetime
2009-05-02 11:16:08 +00:00
import logging
2017-12-28 08:27:44 +00:00
import ntpath
import os
2009-11-04 17:48:46 +00:00
import uuid
from copy import deepcopy
from pathlib import Path
from shutil import copytree, copy, move
2009-07-10 13:16:15 +00:00
2015-11-07 00:49:40 +00:00
from PyQt5 import QtGui
2009-07-10 13:16:15 +00:00
2020-05-07 05:18:37 +00:00
from openlp.core.common import ThemeLevel, sha256_file_hash
2017-10-07 07:05:07 +00:00
from openlp.core.common.applocation import AppLocation
from openlp.core.common.enum import ServiceItemType
2018-04-10 19:26:56 +00:00
from openlp.core.common.i18n import translate
2017-10-23 22:09:57 +00:00
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.utils import wait_for
2019-08-30 14:43:22 +00:00
from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing
from openlp.core.lib import create_thumb, image_to_data_uri, ItemCapabilities
from openlp.core.lib.theme import BackgroundType, TransitionSpeed
from openlp.core.state import State
2018-10-02 04:39:42 +00:00
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.media import parse_stream_path
2018-10-02 04:39:42 +00:00
2009-07-10 13:16:15 +00:00
2010-02-27 15:31:23 +00:00
log = logging.getLogger(__name__)
2013-02-01 19:58:18 +00:00
2014-03-16 21:25:23 +00:00
class ServiceItem(RegistryProperties):
"""
The service item is a base class for the plugins to use to interact with
the service manager, the slide controller, and the projection screen
compositor.
"""
2013-08-31 18:17:38 +00:00
log.info('Service Item created')
2009-11-14 08:40:14 +00:00
def __init__(self, plugin=None):
"""
2009-07-10 13:16:15 +00:00
Set up the service item.
2014-03-17 19:05:55 +00:00
:param plugin: The plugin that this service item belongs to.
"""
2009-11-14 08:40:14 +00:00
if plugin:
self.name = plugin.name
self._rendered_slides = None
self._display_slides = None
self._print_slides = None
self._creating_slides = False
2013-08-31 18:17:38 +00:00
self.title = ''
self.slides = []
self.processor = None
2013-08-31 18:17:38 +00:00
self.audit = ''
2009-05-02 11:16:08 +00:00
self.items = []
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().default
2011-03-04 17:31:23 +00:00
self.raw_footer = []
# Plugins can set footer_html themselves. If they don't, it will be generated from raw_footer.
self.footer_html = ''
2009-05-02 11:16:08 +00:00
self.theme = None
self.service_item_type = None
2013-01-21 07:34:50 +00:00
self.unique_identifier = 0
2013-08-31 18:17:38 +00:00
self.notes = ''
2010-03-28 15:56:49 +00:00
self.from_plugin = False
2010-04-03 07:10:31 +00:00
self.capabilities = []
2010-05-07 18:29:17 +00:00
self.is_valid = True
2010-06-12 20:22:58 +00:00
self.icon = None
2010-07-25 08:58:08 +00:00
self.main = None
self.footer = None
2010-09-16 21:19:51 +00:00
self.bg_image_bytes = None
2013-08-31 18:17:38 +00:00
self.search_string = ''
self.data_string = ''
self.edit_id = None
self.xml_version = None
2011-02-13 13:11:15 +00:00
self.start_time = 0
2011-03-19 11:50:48 +00:00
self.end_time = 0
2011-02-14 17:54:09 +00:00
self.media_length = 0
self.from_service = False
2011-08-28 17:45:13 +00:00
self.background_audio = []
2011-11-04 20:09:35 +00:00
self.theme_overwritten = False
2011-12-10 08:45:17 +00:00
self.temporary_edit = False
2012-11-14 11:47:15 +00:00
self.auto_play_slides_once = False
self.auto_play_slides_loop = False
self.timed_slide_interval = 0
self.will_auto_start = False
self.has_original_file_path = True
2020-05-07 05:18:37 +00:00
self.sha256_file_hash = None
self.stored_filename = None
self._new_item()
2018-02-18 16:16:52 +00:00
self.metadata = []
2010-04-02 19:02:38 +00:00
def get_theme_data(self, theme_level=None):
"""
Get the theme appropriate for this item
:param theme_level: The theme_level to use,
the value in Settings is used when this value is missinig
"""
if theme_level is None:
theme_level = self.settings.value('themes/theme level')
theme_manager = Registry().get('theme_manager')
# Just assume we use the global theme.
theme = theme_manager.global_theme
if theme_level != ThemeLevel.Global:
service_theme = self.settings.value('servicemanager/service theme')
# Service or Song level, so assume service theme (if it exists and item in service)
# but use song theme if level is song (and it exists)
if service_theme and self.from_service:
theme = service_theme
if self.is_capable(ItemCapabilities.ProvidesOwnTheme) or theme_level == ThemeLevel.Song and self.theme:
theme = self.theme
theme = theme_manager.get_theme_data(theme)
# Clean up capabilities and reload from the theme.
if self.is_text():
# Cleanup capabilities
if self.is_capable(ItemCapabilities.CanStream):
self.remove_capability(ItemCapabilities.CanStream)
if self.is_capable(ItemCapabilities.HasBackgroundVideo):
self.remove_capability(ItemCapabilities.HasBackgroundVideo)
if self.is_capable(ItemCapabilities.HasBackgroundStream):
self.remove_capability(ItemCapabilities.HasBackgroundStream)
# Reload capabilities
if theme.background_type == BackgroundType.to_string(BackgroundType.Stream):
self.add_capability(ItemCapabilities.HasBackgroundStream)
self.stream_mrl = theme.background_filename
if theme.background_type == BackgroundType.to_string(BackgroundType.Video):
self.video_file_name = theme.background_filename
self.add_capability(ItemCapabilities.HasBackgroundVideo)
return theme
2010-08-02 19:08:55 +00:00
def _new_item(self):
2010-08-21 08:24:14 +00:00
"""
2014-03-17 19:05:55 +00:00
Method to set the internal id of the item. This is used to compare service items to see if they are the same.
2010-08-21 08:24:14 +00:00
"""
2013-08-31 18:17:38 +00:00
self.unique_identifier = str(uuid.uuid1())
self.validate_item()
2010-08-02 19:08:55 +00:00
2010-04-03 07:10:31 +00:00
def add_capability(self, capability):
2010-06-10 21:30:50 +00:00
"""
Add an ItemCapability to a ServiceItem
2014-03-17 19:05:55 +00:00
:param capability: The capability to add
2010-06-10 21:30:50 +00:00
"""
2010-04-03 07:10:31 +00:00
self.capabilities.append(capability)
2010-04-02 19:02:38 +00:00
def remove_capability(self, capability):
"""
Remove an ItemCapability from a ServiceItem
:param capability: The capability to remove
"""
self.capabilities.remove(capability)
2010-04-03 07:10:31 +00:00
def is_capable(self, capability):
2010-06-10 21:30:50 +00:00
"""
Tell the caller if a ServiceItem has a capability
2014-03-17 19:05:55 +00:00
:param capability: The capability to test for
2010-06-10 21:30:50 +00:00
"""
2010-04-03 07:10:31 +00:00
return capability in self.capabilities
2009-05-02 11:16:08 +00:00
2018-04-21 05:47:20 +00:00
def add_icon(self):
2009-07-10 13:16:15 +00:00
"""
2014-03-17 19:05:55 +00:00
Add an icon to the service item. This is used when displaying the service item in the service manager.
2009-07-10 13:16:15 +00:00
"""
2018-04-08 17:24:31 +00:00
if self.name == 'songs':
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().music
2018-04-08 17:24:31 +00:00
elif self.name == 'bibles':
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().bible
2018-04-08 17:24:31 +00:00
elif self.name == 'presentations':
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().presentation
2018-04-08 17:24:31 +00:00
elif self.name == 'images':
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().picture
2018-04-22 06:59:35 +00:00
elif self.name == 'media':
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().video
2018-04-08 17:24:31 +00:00
else:
2018-04-21 05:47:20 +00:00
self.icon = UiIcons().clone
def _create_slides(self):
"""
Create frames for rendering and display
"""
wait_for(lambda: not self._creating_slides)
self._creating_slides = True
self._rendered_slides = []
self._display_slides = []
# Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
# the dict instead of rendering them again.
previous_pages = {}
2019-02-02 21:02:33 +00:00
index = 0
if not self.footer_html:
self.footer_html = '<br>'.join([_f for _f in self.raw_footer if _f])
2019-02-02 21:02:33 +00:00
for raw_slide in self.slides:
verse_tag = raw_slide['verse']
2019-02-02 21:02:33 +00:00
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
pages = previous_pages[verse_tag][1]
else:
2019-02-02 21:02:33 +00:00
pages = self.renderer.format_slide(raw_slide['text'], self)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
rendered_slide = {
'title': raw_slide['title'],
'text': render_tags(page),
'chords': render_tags(page, can_render_chords=True),
2019-02-02 21:02:33 +00:00
'verse': index,
'footer': self.footer_html
2019-02-02 21:02:33 +00:00
}
self._rendered_slides.append(rendered_slide)
display_slide = {
'title': raw_slide['title'],
'text': remove_tags(page, can_remove_chords=True),
2019-02-02 21:02:33 +00:00
'verse': verse_tag,
}
self._display_slides.append(display_slide)
index += 1
self._creating_slides = False
@property
def rendered_slides(self):
"""
Render the frames and return them
"""
wait_for(lambda: not self._creating_slides)
if not self._rendered_slides:
self._create_slides()
return self._rendered_slides
@property
def display_slides(self):
"""
Render the frames and return them
"""
wait_for(lambda: not self._creating_slides)
if not self._display_slides:
self._create_slides()
return self._display_slides
@property
def print_slides(self):
"""
Render the frames for printing and return them
"""
if not self._print_slides:
self._print_slides = []
previous_pages = {}
index = 0
for raw_slide in self.slides:
verse_tag = raw_slide['verse']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
pages = previous_pages[verse_tag][1]
else:
pages = self.renderer.format_slide(raw_slide['text'], self)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
slide = {
'title': raw_slide['title'],
2019-08-30 14:43:22 +00:00
'text': render_chords_for_printing(remove_tags(page), '\n'),
'verse': index,
2019-08-30 14:43:22 +00:00
'footer': self.raw_footer,
}
self._print_slides.append(slide)
return self._print_slides
def add_from_image(self, path, title, thumbnail=None, file_hash=None):
2009-07-10 13:16:15 +00:00
"""
Add an image slide to the service item.
2014-03-17 19:05:55 +00:00
:param path: The directory in which the image file is located.
:param title: A title for the slide in the service item.
:param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
:param file_hash: Unique Reference to file .
2009-07-10 13:16:15 +00:00
"""
self.service_item_type = ServiceItemType.Image
2020-05-07 05:18:37 +00:00
if not file_hash:
file_hash = sha256_file_hash(path)
slide = {'title': title, 'path': path, 'file_hash': file_hash}
if thumbnail:
slide['thumbnail'] = thumbnail
self.slides.append(slide)
2010-08-02 19:08:55 +00:00
self._new_item()
def add_from_text(self, text, verse_tag=None):
2009-07-10 13:16:15 +00:00
"""
Add a text slide to the service item.
:param text: The raw text of the slide.
2014-03-17 19:05:55 +00:00
:param verse_tag:
2009-07-10 13:16:15 +00:00
"""
2011-05-11 22:32:25 +00:00
if verse_tag:
verse_tag = verse_tag.upper()
else:
# For items that don't have a verse tag, autoincrement the slide numbers
verse_tag = str(len(self.slides) + 1)
self.service_item_type = ServiceItemType.Text
title = text[:30].split('\n')[0]
self.slides.append({'title': title, 'text': text, 'verse': verse_tag})
2010-08-02 19:08:55 +00:00
self._new_item()
2020-05-07 05:18:37 +00:00
def add_from_command(self, path, file_name, image, display_title=None, notes=None, file_hash=None):
2009-07-10 13:16:15 +00:00
"""
Add a slide from a command.
2014-03-17 19:05:55 +00:00
:param path: The title of the slide in the service item.
:param file_name: The title of the slide in the service item.
:param image: The command of/for the slide.
:param display_title: Title to show in gui/webinterface, optional.
:param notes: Notes to show in the webinteface, optional.
2020-05-07 05:18:37 +00:00
:param file_hash: Sha256 hash checksum of the file.
2009-07-10 13:16:15 +00:00
"""
self.service_item_type = ServiceItemType.Command
# If the item should have a display title but this frame doesn't have one, we make one up
if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
display_title = translate('OpenLP.ServiceItem',
'[slide {frame:d}]').format(frame=len(self.slides) + 1)
2020-05-07 05:18:37 +00:00
if self.uses_file():
if file_hash:
self.sha256_file_hash = file_hash
else:
file_location = Path(path) / file_name
self.sha256_file_hash = sha256_file_hash(file_location)
self.stored_filename = '{hash}{ext}'.format(hash=self.sha256_file_hash, ext=os.path.splitext(file_name)[1])
# Update image path to match servicemanager location if file was loaded from service
if image and self.name == 'presentations':
2020-05-07 05:18:37 +00:00
image = AppLocation.get_section_data_path(self.name) / 'thumbnails' / self.sha256_file_hash / \
ntpath.basename(image)
self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
'notes': notes, 'thumbnail': image})
self._new_item()
2012-10-15 16:20:23 +00:00
def get_service_repr(self, lite_save):
"""
2014-03-17 19:05:55 +00:00
This method returns some text which can be saved into the service file to represent this item.
"""
2020-05-07 05:18:37 +00:00
if self.sha256_file_hash:
stored_filename = '{hash}{ext}'.format(hash=self.sha256_file_hash, ext=os.path.splitext(self.title)[1])
else:
stored_filename = None
2009-09-19 21:45:50 +00:00
service_header = {
2013-08-31 18:17:38 +00:00
'name': self.name,
'plugin': self.name,
'theme': self.theme,
'title': self.title,
'footer': self.raw_footer,
'type': self.service_item_type,
'audit': self.audit,
'notes': self.notes,
'from_plugin': self.from_plugin,
'capabilities': self.capabilities,
'search': self.search_string,
'data': self.data_string,
'xml_version': self.xml_version,
'auto_play_slides_once': self.auto_play_slides_once,
'auto_play_slides_loop': self.auto_play_slides_loop,
'timed_slide_interval': self.timed_slide_interval,
'start_time': self.start_time,
'end_time': self.end_time,
'media_length': self.media_length,
'background_audio': self.background_audio,
'theme_overwritten': self.theme_overwritten,
'will_auto_start': self.will_auto_start,
2018-02-18 16:16:52 +00:00
'processor': self.processor,
2020-05-07 05:18:37 +00:00
'metadata': self.metadata,
'sha256_file_hash': self.sha256_file_hash,
'stored_filename': stored_filename
2009-07-10 13:16:15 +00:00
}
2009-09-19 21:45:50 +00:00
service_data = []
if self.service_item_type == ServiceItemType.Text:
for slide in self.slides:
data_slide = deepcopy(slide)
data_slide['raw_slide'] = data_slide.pop('text')
data_slide['verseTag'] = data_slide.pop('verse')
service_data.append(data_slide)
elif self.service_item_type == ServiceItemType.Image:
2012-10-15 16:20:23 +00:00
if lite_save:
2018-10-28 16:34:17 +00:00
for slide in self.slides:
# When saving a service that originated from openlp 2.4 thumbnail might not be available
if 'thumbnail' in slide:
image_path = slide['thumbnail']
else:
# Check if (by chance) the thumbnails for this image is available on this machine
test_thumb = AppLocation.get_section_data_path(self.name) / 'thumbnails' / stored_filename
if test_thumb.exists():
image_path = test_thumb
else:
image_path = None
service_data.append({'title': slide['title'], 'image': image_path, 'path': slide['path'],
2020-05-07 05:18:37 +00:00
'file_hash': slide['file_hash']})
2012-10-15 16:20:23 +00:00
else:
2020-05-07 05:18:37 +00:00
for slide in self.slides:
# When saving a service that originated from openlp 2.4 thumbnail might not be available
if 'thumbnail' in slide:
image_path = slide['thumbnail'].relative_to(AppLocation().get_data_path())
else:
# Check if (by chance) the thumbnails for this image is available on this machine
test_thumb = AppLocation.get_section_data_path(self.name) / 'thumbnails' / stored_filename
if test_thumb.exists():
image_path = test_thumb
else:
image_path = None
2020-05-07 05:18:37 +00:00
service_data.append({'title': slide['title'], 'image': image_path, 'file_hash': slide['file_hash']})
elif self.service_item_type == ServiceItemType.Command:
2018-10-28 16:34:17 +00:00
for slide in self.slides:
if isinstance(slide['image'], QtGui.QIcon):
2020-05-07 05:18:37 +00:00
image = 'clapperboard'
elif lite_save:
image = slide['image']
else:
2020-05-07 05:18:37 +00:00
image = slide['image'].relative_to(AppLocation().get_data_path())
service_data.append({'title': slide['title'], 'image': image, 'path': slide['path'],
2013-12-30 08:35:05 +00:00
'display_title': slide['display_title'], 'notes': slide['notes']})
2013-08-31 18:17:38 +00:00
return {'header': service_header, 'data': service_data}
2019-08-30 11:28:10 +00:00
def render_text_items(self):
"""
This method forces the display to be regenerated
"""
self._display_slides = []
self._rendered_slides = []
2020-05-07 05:18:37 +00:00
def set_from_service(self, service_item, path=None, version=2):
"""
2014-03-17 19:05:55 +00:00
This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
data actually required.
2009-07-10 13:16:15 +00:00
2014-03-17 19:05:55 +00:00
:param service_item: The item to extract data from.
:param path: Defaults to *None*. This is the service manager path for things which have their files saved
2015-09-08 19:13:59 +00:00
with them or None when the saved service is lite and the original file paths need to be preserved.
2020-05-07 05:18:37 +00:00
:param version: Format version of the data.
"""
log.debug('set_from_service called with path {path}'.format(path=path))
2014-03-17 19:14:22 +00:00
header = service_item['serviceitem']['header']
2013-08-31 18:17:38 +00:00
self.title = header['title']
self.name = header['name']
self.service_item_type = header['type']
self.theme = header['theme']
2018-04-21 05:47:20 +00:00
self.add_icon()
2013-08-31 18:17:38 +00:00
self.raw_footer = header['footer']
self.audit = header['audit']
self.notes = header['notes']
self.from_plugin = header['from_plugin']
self.capabilities = header['capabilities']
2010-09-30 17:07:27 +00:00
# Added later so may not be present in older services.
2013-08-31 18:17:38 +00:00
self.search_string = header.get('search', '')
self.data_string = header.get('data', '')
self.xml_version = header.get('xml_version')
self.start_time = header.get('start_time', 0)
self.end_time = header.get('end_time', 0)
self.media_length = header.get('media_length', 0)
self.auto_play_slides_once = header.get('auto_play_slides_once', False)
self.auto_play_slides_loop = header.get('auto_play_slides_loop', False)
self.timed_slide_interval = header.get('timed_slide_interval', 0)
self.will_auto_start = header.get('will_auto_start', False)
self.processor = header.get('processor', None)
self.has_original_file_path = True
2018-02-18 16:16:52 +00:00
self.metadata = header.get('item_meta_data', [])
2020-05-07 05:18:37 +00:00
self.sha256_file_hash = header.get('sha256_file_hash', None)
self.stored_filename = header.get('stored_filename', None)
if 'background_audio' in header and State().check_preconditions('media'):
self.background_audio = []
for file_path in header['background_audio']:
2018-01-13 07:24:20 +00:00
# In OpenLP 3.0 we switched to storing Path objects in JSON files
2020-05-07 05:18:37 +00:00
if version >= 3:
if path:
file_path = path / file_path
else:
2018-01-13 07:24:20 +00:00
# Handle service files prior to OpenLP 3.0
# Windows can handle both forward and backward slashes, so we use ntpath to get the basename
2019-03-10 21:01:39 +00:00
file_path = path / ntpath.basename(file_path)
self.background_audio.append(file_path)
2013-08-31 18:17:38 +00:00
self.theme_overwritten = header.get('theme_overwritten', False)
if self.service_item_type == ServiceItemType.Text:
2014-03-17 19:14:22 +00:00
for slide in service_item['serviceitem']['data']:
2018-10-28 16:34:17 +00:00
self.add_from_text(slide['raw_slide'], slide['verseTag'])
2018-11-13 05:42:43 +00:00
self._create_slides()
elif self.service_item_type == ServiceItemType.Image:
2012-10-15 16:20:23 +00:00
if path:
self.has_original_file_path = False
2014-03-17 19:14:22 +00:00
for text_image in service_item['serviceitem']['data']:
text = None
2020-05-07 05:18:37 +00:00
file_hash = None
thumbnail = None
if version >= 3:
text = text_image['title']
file_hash = text_image['file_hash']
file_path = path / '{base}{ext}'.format(base=file_hash, ext=os.path.splitext(text)[1])
2021-02-06 15:48:08 +00:00
if text_image['image']:
thumbnail = AppLocation.get_data_path() / text_image['image']
# copy thumbnail from servicemanager path
copy(path / 'thumbnails' / os.path.basename(text_image['image']),
AppLocation.get_section_data_path(self.name) / 'thumbnails')
2020-05-07 05:18:37 +00:00
else:
text = text_image
org_file_path = path / text
# rename the extracted file so that it follows the sha256 based approach of openlp 3
self.sha256_file_hash = sha256_file_hash(org_file_path)
new_file = '{hash}{ext}'.format(hash=self.sha256_file_hash, ext=os.path.splitext(text)[1])
file_path = path / new_file
move(org_file_path, file_path)
# Check if (by chance) the thumbnails for this image is available on this machine
test_thumb = AppLocation.get_section_data_path(self.name) / 'thumbnails' / new_file
if test_thumb.exists():
thumbnail = test_thumb
self.add_from_image(file_path, text, thumbnail=thumbnail, file_hash=file_hash)
2012-10-15 16:20:23 +00:00
else:
2014-03-17 19:14:22 +00:00
for text_image in service_item['serviceitem']['data']:
2020-05-07 05:18:37 +00:00
file_hash = None
text = text_image['title']
thumbnail = None
2020-05-07 05:18:37 +00:00
if version >= 3:
file_path = text_image['path']
2020-05-07 05:18:37 +00:00
file_hash = text_image['file_hash']
2021-02-06 15:48:08 +00:00
if text_image['image']:
thumbnail = AppLocation.get_data_path() / text_image['image']
else:
file_path = Path(text_image['path'])
# Check if (by chance) the thumbnails for this image is available on this machine
file_hash = sha256_file_hash(file_path)
new_file = '{hash}{ext}'.format(hash=file_hash, ext=os.path.splitext(file_path)[1])
test_thumb = AppLocation.get_section_data_path(self.name) / 'thumbnails' / new_file
if test_thumb.exists():
thumbnail = test_thumb
self.add_from_image(file_path, text, thumbnail=thumbnail, file_hash=file_hash)
elif self.service_item_type == ServiceItemType.Command:
if version < 3:
# If this is an old servicefile with files included, we need to rename the bundled files to match
# the new sha256 based scheme
if path:
file_path = Path(path) / self.title
self.sha256_file_hash = sha256_file_hash(file_path)
new_file = path / '{hash}{ext}'.format(hash=self.sha256_file_hash,
ext=os.path.splitext(self.title)[1])
move(file_path, new_file)
else:
file_path = Path(service_item['serviceitem']['data'][0]['path']) / self.title
self.sha256_file_hash = sha256_file_hash(file_path)
# Loop over the slides
2014-03-17 19:14:22 +00:00
for text_image in service_item['serviceitem']['data']:
if not self.title:
2013-08-31 18:17:38 +00:00
self.title = text_image['title']
2020-05-07 05:18:37 +00:00
if self.is_capable(ItemCapabilities.IsOptical) or self.is_capable(ItemCapabilities.CanStream):
self.has_original_file_path = False
self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
elif path:
self.has_original_file_path = False
2020-05-07 05:18:37 +00:00
# Copy any bundled thumbnails into the plugin thumbnail folder
if version >= 3 and os.path.exists(path / self.sha256_file_hash) and \
os.path.isdir(path / self.sha256_file_hash):
try:
copytree(path / self.sha256_file_hash,
AppLocation.get_section_data_path(self.name) / 'thumbnails' /
self.sha256_file_hash)
except FileExistsError:
# Files already exists, just skip
pass
if text_image['image'] in ['clapperboard', ':/media/slidecontroller_multimedia.png']:
image_path = UiIcons().clapperboard
elif version < 3:
# convert the thumbnail path to new sha256 based
new_file = '{hash}{ext}'.format(hash=self.sha256_file_hash,
ext=os.path.splitext(text_image['image'])[1])
image_path = AppLocation.get_section_data_path(self.name) / 'thumbnails' / \
self.sha256_file_hash / os.path.split(text_image['image'])[1]
else:
image_path = text_image['image']
self.add_from_command(path, text_image['title'], image_path, text_image.get('display_title', ''),
text_image.get('notes', ''), file_hash=self.sha256_file_hash)
else:
if text_image['image'] in ['clapperboard', ':/media/slidecontroller_multimedia.png']:
image_path = UiIcons().clapperboard
elif version < 3:
# convert the thumbnail path to new sha256 based
image_path = AppLocation.get_section_data_path(self.name) / 'thumbnails' / \
self.sha256_file_hash / os.path.split(text_image['image'])[1]
else:
image_path = text_image['image']
self.add_from_command(Path(text_image['path']), str(text_image['title']), image_path)
self._new_item()
2009-11-05 17:03:37 +00:00
2011-01-29 07:53:21 +00:00
def get_display_title(self):
2011-01-28 19:46:55 +00:00
"""
Returns the title of the service item.
"""
2014-04-20 13:57:12 +00:00
if self.is_text() or self.is_capable(ItemCapabilities.IsOptical) \
2014-04-20 14:36:56 +00:00
or self.is_capable(ItemCapabilities.CanEditTitle):
2011-01-28 19:46:55 +00:00
return self.title
else:
if len(self.slides) > 1:
2011-01-28 19:46:55 +00:00
return self.title
else:
return self.slides[0]['title']
2011-01-28 19:46:55 +00:00
def get_transition_delay(self):
"""
Returns a approximate time in seconds for how long it will take to switch slides
"""
delay = 1
if self.is_capable(ItemCapabilities.ProvidesOwnDisplay):
delay = 0.5
else:
theme = self.get_theme_data()
transition_speed = theme.display_slide_transition_speed
if theme.display_slide_transition is False or transition_speed == TransitionSpeed.Fast:
delay = 0.5
elif transition_speed == TransitionSpeed.Normal:
delay = 1
elif transition_speed == TransitionSpeed.Slow:
delay = 2
return delay
2009-11-05 17:03:37 +00:00
def merge(self, other):
"""
2013-01-21 07:34:50 +00:00
Updates the unique_identifier with the value from the original one
2014-03-17 19:05:55 +00:00
The unique_identifier is unique for a given service item but this allows one to replace an original version.
2014-03-17 19:05:55 +00:00
:param other: The service item to be merged with
2009-11-05 17:03:37 +00:00
"""
2013-01-21 07:34:50 +00:00
self.unique_identifier = other.unique_identifier
self.notes = other.notes
2011-12-10 08:52:18 +00:00
self.temporary_edit = other.temporary_edit
2011-10-03 16:53:54 +00:00
# Copy theme over if present.
if other.theme is not None:
2011-10-03 16:53:54 +00:00
self.theme = other.theme
self._new_item()
2011-09-02 19:04:07 +00:00
if self.is_capable(ItemCapabilities.HasBackgroundAudio):
log.debug(self.background_audio)
2009-11-05 17:03:37 +00:00
def __eq__(self, other):
"""
Confirms the service items are for the same instance
"""
if not other:
return False
2013-01-21 07:34:50 +00:00
return self.unique_identifier == other.unique_identifier
2009-11-05 17:03:37 +00:00
def __ne__(self, other):
"""
Confirms the service items are not for the same instance
"""
2013-01-21 07:34:50 +00:00
return self.unique_identifier != other.unique_identifier
2009-11-05 17:03:37 +00:00
2013-06-30 18:33:41 +00:00
def __hash__(self):
"""
Return the hash for the service item.
"""
return self.unique_identifier
def is_media(self):
2010-06-10 21:30:50 +00:00
"""
Confirms if the ServiceItem is media
"""
2010-04-03 07:10:31 +00:00
return ItemCapabilities.RequiresMedia in self.capabilities
2009-11-11 19:10:38 +00:00
def is_command(self):
2010-06-10 21:30:50 +00:00
"""
Confirms if the ServiceItem is a command
"""
return self.service_item_type == ServiceItemType.Command
2009-11-11 19:10:38 +00:00
def is_image(self):
2010-06-10 21:30:50 +00:00
"""
Confirms if the ServiceItem is an image
"""
return self.service_item_type == ServiceItemType.Image
def uses_file(self):
2010-06-10 21:30:50 +00:00
"""
Confirms if the ServiceItem uses a file
"""
return self.service_item_type == ServiceItemType.Image or \
2020-05-07 05:18:37 +00:00
(self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)
and not self.is_capable(ItemCapabilities.CanStream))
def is_text(self):
2010-06-10 21:30:50 +00:00
"""
Confirms if the ServiceItem is text
"""
return self.service_item_type == ServiceItemType.Text
2009-11-14 08:40:14 +00:00
def set_media_length(self, length):
"""
Stores the media length of the item
2014-03-17 19:05:55 +00:00
:param length: The length of the media item
"""
self.media_length = length
if length > 0:
self.add_capability(ItemCapabilities.HasVariableStartTime)
def get_frames(self):
2010-06-10 21:30:50 +00:00
"""
Returns the frames for the ServiceItem
"""
2009-11-14 09:02:30 +00:00
if self.service_item_type == ServiceItemType.Text:
2018-11-13 05:42:43 +00:00
return self.display_slides
2009-11-14 09:02:30 +00:00
else:
return self.slides
2009-11-14 09:02:30 +00:00
def get_rendered_frame(self, row, clean=False):
2009-11-14 09:06:25 +00:00
"""
Returns the correct frame for a given list and renders it if required.
2014-03-17 19:05:55 +00:00
:param row: The service item slide to be returned
:param clean: do I want HTML tags or not
2009-11-14 09:06:25 +00:00
"""
2009-11-14 09:02:30 +00:00
if self.service_item_type == ServiceItemType.Text:
if clean:
return self.display_slides[row]['text']
else:
return self.rendered_slides[row]['text']
2010-10-24 06:26:07 +00:00
elif self.service_item_type == ServiceItemType.Image:
return self.slides[row]['path']
2010-10-24 06:26:07 +00:00
else:
return self.slides[row]['image']
def get_frame_title(self, row=0):
"""
Returns the title of the raw frame
"""
try:
2018-11-16 04:34:49 +00:00
return self.get_frames()[row]['title']
2011-01-22 12:48:16 +00:00
except IndexError:
2013-08-31 18:17:38 +00:00
return ''
def get_frame_path(self, row=0, frame=None):
2010-03-30 19:01:23 +00:00
"""
2010-10-15 08:35:00 +00:00
Returns the path of the raw frame
2010-03-30 19:01:23 +00:00
"""
if not frame:
try:
frame = self.slides[row]
except IndexError:
2013-08-31 18:17:38 +00:00
return ''
if self.is_image() or self.is_capable(ItemCapabilities.IsOptical):
2013-08-31 18:17:38 +00:00
path_from = frame['path']
elif self.is_command() and not self.has_original_file_path and self.sha256_file_hash:
2020-05-07 05:18:37 +00:00
path_from = os.path.join(frame['path'], self.stored_filename)
else:
2013-08-31 18:17:38 +00:00
path_from = os.path.join(frame['path'], frame['title'])
2018-11-01 20:51:42 +00:00
if isinstance(path_from, str):
# Handle service files prior to OpenLP 3.0
# Windows can handle both forward and backward slashes, so we use ntpath to get the basename
path_from = Path(path_from)
return path_from
def remove_frame(self, frame):
"""
2012-12-07 21:57:35 +00:00
Remove the specified frame from the item
"""
2018-10-28 16:34:17 +00:00
if frame in self.slides:
self.slides.remove(frame)
2011-02-13 10:37:27 +00:00
def get_media_time(self):
"""
Returns the start and finish time for a media item
"""
start = None
end = None
2011-02-13 13:11:15 +00:00
if self.start_time != 0:
time = str(datetime.timedelta(seconds=self.start_time))
start = translate('OpenLP.ServiceItem',
'<strong>Start</strong>: {start}').format(start=time)
2011-02-14 17:54:09 +00:00
if self.media_length != 0:
length = str(datetime.timedelta(seconds=self.media_length // 1000))
end = translate('OpenLP.ServiceItem', '<strong>Length</strong>: {length}').format(length=length)
2011-02-13 10:37:27 +00:00
if not start and not end:
2013-08-31 18:17:38 +00:00
return ''
2011-02-13 10:37:27 +00:00
elif start and not end:
return start
elif not start and end:
return end
else:
return '{start} <br>{end}'.format(start=start, end=end)
2011-04-26 17:03:19 +00:00
def update_theme(self, theme):
"""
updates the theme in the service item
2014-03-17 19:05:55 +00:00
:param theme: The new theme to be replaced in the service item
"""
2013-02-01 19:58:18 +00:00
self.theme_overwritten = (theme is None)
self.theme = theme
self._new_item()
2011-04-26 17:03:19 +00:00
2012-11-02 18:54:42 +00:00
def remove_invalid_frames(self, invalid_paths=None):
"""
Remove invalid frames, such as ones where the file no longer exists.
"""
if self.uses_file():
for frame in self.get_frames():
if self.get_frame_path(frame=frame) in invalid_paths:
self.remove_frame(frame)
def requires_media(self):
return self.is_capable(ItemCapabilities.HasBackgroundAudio) or \
self.is_capable(ItemCapabilities.HasBackgroundVideo) or \
self.is_capable(ItemCapabilities.HasBackgroundStream)
def missing_frames(self):
2012-11-02 18:54:42 +00:00
"""
Returns if there are any frames in the service item
2012-11-02 18:54:42 +00:00
"""
2018-10-28 16:34:17 +00:00
return not bool(self.slides)
def validate_item(self, suffixes=None):
"""
Validates a service item to make sure it is valid
:param set[str] suffixes: A set of valid suffixes
"""
self.is_valid = True
for slide in self.slides:
2018-12-01 05:52:49 +00:00
if self.is_image() and not os.path.exists(slide['path']):
self.is_valid = False
break
elif self.is_command():
if self.is_capable(ItemCapabilities.IsOptical) and State().check_preconditions('media'):
if not os.path.exists(slide['title']):
self.is_valid = False
break
elif self.is_capable(ItemCapabilities.CanStream):
(name, mrl, options) = parse_stream_path(slide['path'])
if not name or not mrl or not options:
self.is_valid = False
break
else:
if self.has_original_file_path:
2020-05-07 05:18:37 +00:00
file_name = Path(slide['path']) / slide['title']
else:
file_name = Path(slide['path']) / self.stored_filename
if not file_name.exists():
self.is_valid = False
break
if suffixes and not self.is_text():
file_suffix = "*.{suffx}".format(suffx=slide['title'].split('.')[-1])
if file_suffix.lower() not in suffixes:
self.is_valid = False
2014-06-17 07:27:12 +00:00
break
2020-05-07 05:18:37 +00:00
def get_thumbnail_path(self):
"""
Returns the thumbnail folder. Should only be used for items that support thumbnails.
"""
if self.is_capable(ItemCapabilities.HasThumbnails):
if self.is_command() and self.slides:
return os.path.dirname(self.slides[0]['image'])
elif self.is_image() and self.slides and 'thumbnail' in self.slides[0]:
2020-05-07 05:18:37 +00:00
return os.path.dirname(self.slides[0]['thumbnail'])
return None
2020-10-19 07:18:26 +00:00
def to_dict(self, active=False, item_no=0):
"""
Convert the service item into a dictionary
Images and thumbnails are put in dict as data uri strings.
2020-10-19 07:18:26 +00:00
:param boolean active: Do I filter list for only the active item
:param int item_no: the index of the active item
"""
data_dict = {
'title': self.title,
'name': self.name,
'type': str(self.service_item_type),
'theme': self.theme,
'footer': self.raw_footer,
'audit': self.audit,
'notes': self.notes,
2020-10-19 07:18:00 +00:00
'data': self.data_string or {},
'fromPlugin': self.from_plugin,
'capabilities': self.capabilities,
'backgroundAudio': [str(file_path) for file_path in self.background_audio],
'isThemeOverwritten': self.theme_overwritten,
'slides': []
}
for index, frame in enumerate(self.get_frames()):
2020-10-19 07:18:26 +00:00
if active and index is not item_no:
continue
else:
2020-10-19 07:18:26 +00:00
item = {
'tag': index + 1,
'title': self.title,
'selected': False
}
if self.is_text():
if frame['verse']:
item['tag'] = str(frame['verse'])
item['text'] = frame['text']
item['html'] = self.rendered_slides[index]['text']
item['chords'] = self.rendered_slides[index]['chords']
item['footer'] = self.rendered_slides[index]['footer']
elif self.is_image() and not frame.get('image', '') and \
2021-02-02 09:28:20 +00:00
Registry().get('settings_thread').value('api/thumbnails') and \
self.is_capable(ItemCapabilities.HasThumbnails):
thumbnail_path = frame['thumbnail']
if not thumbnail_path.exists():
create_thumb(Path(self.get_frame_path(index)), thumbnail_path, False)
item['img'] = image_to_data_uri(thumbnail_path)
2020-10-19 07:18:26 +00:00
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
else:
# presentations and other things
if self.is_capable(ItemCapabilities.HasDisplayTitle):
item['title'] = str(frame['display_title'])
if self.is_capable(ItemCapabilities.HasNotes):
item['slide_notes'] = str(frame['notes'])
if self.is_capable(ItemCapabilities.HasThumbnails) and \
Registry().get('settings_thread').value('api/thumbnails'):
# If the file is under our app directory tree send the portion after the match
data_path = str(AppLocation.get_data_path())
try:
relative_file = frame['image'].relative_to(data_path)
except ValueError:
log.warning('Service item "{title}" is missing a thumbnail or has an invalid thumbnail path'
.format(title=self.title))
else:
item['img'] = image_to_data_uri(AppLocation.get_data_path() / relative_file)
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
data_dict['slides'].append(item)
return data_dict