diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 6407f7a78..674204925 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -25,12 +25,15 @@ The :mod:`db` module provides the core database functionality for OpenLP """ import logging import os +from copy import copy from urllib.parse import quote_plus as urlquote from sqlalchemy import Table, MetaData, Column, types, create_engine +from sqlalchemy.engine.url import make_url from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError from sqlalchemy.orm import scoped_session, sessionmaker, mapper from sqlalchemy.pool import NullPool + from alembic.migration import MigrationContext from alembic.operations import Operations @@ -40,6 +43,66 @@ from openlp.core.lib.ui import critical_error_message_box log = logging.getLogger(__name__) +def database_exists(url): + """Check if a database exists. + + :param url: A SQLAlchemy engine URL. + + Performs backend-specific testing to quickly determine if a database + exists on the server. :: + + database_exists('postgres://postgres@localhost/name') #=> False + create_database('postgres://postgres@localhost/name') + database_exists('postgres://postgres@localhost/name') #=> True + + Supports checking against a constructed URL as well. :: + + engine = create_engine('postgres://postgres@localhost/name') + database_exists(engine.url) #=> False + create_database(engine.url) + database_exists(engine.url) #=> True + + Borrowed from SQLAlchemy_Utils since we only need this one function. + """ + + url = copy(make_url(url)) + database = url.database + if url.drivername.startswith('postgresql'): + url.database = 'template1' + else: + url.database = None + + engine = create_engine(url) + + if engine.dialect.name == 'postgresql': + text = "SELECT 1 FROM pg_database WHERE datname='{db}'".format(db=database) + return bool(engine.execute(text).scalar()) + + elif engine.dialect.name == 'mysql': + text = ("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA " + "WHERE SCHEMA_NAME = '{db}'".format(db=database)) + return bool(engine.execute(text).scalar()) + + elif engine.dialect.name == 'sqlite': + if database: + return database == ':memory:' or os.path.exists(database) + else: + # The default SQLAlchemy database is in memory, + # and :memory is not required, thus we should support that use-case + return True + + else: + text = 'SELECT 1' + try: + url.database = database + engine = create_engine(url) + engine.execute(text) + return True + + except (ProgrammingError, OperationalError): + return False + + def init_db(url, auto_flush=True, auto_commit=False, base=None): """ Initialise and return the session and metadata for a database @@ -144,7 +207,12 @@ def upgrade_db(url, upgrade): :param url: The url of the database to upgrade. :param upgrade: The python module that contains the upgrade instructions. """ + if not database_exists(url): + log.warn("Database {db} doesn't exist - skipping upgrade checks".format(db=url)) + return (0, 0) + log.debug('Checking upgrades for DB {db}'.format(db=url)) + session, metadata = init_db(url) class Metadata(BaseModel): diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index 38331f500..d4e6904e4 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -118,7 +118,7 @@ PJLINK_VALID_CMD = { }, 'LKUP': {'version': ['2', ], 'description': translate('OpenLP.PJLinkConstants', - 'UDP Status notify. Includes MAC address.') + 'UDP Status - Projector is now available on network. Includes MAC address.') }, 'MVOL': {'version': ['2', ], 'description': translate('OpenLP.PJLinkConstants', diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index dfc261f0a..99a1ed685 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -80,25 +80,8 @@ class PJLink(QtNetwork.QTcpSocket): projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar - # New commands available in PJLink Class 2 - pjlink_future = [ - 'ACKN', # UDP Reply to 'SRCH' - 'FILT', # Get current filter usage time - 'FREZ', # Set freeze/unfreeze picture being projected - 'INNM', # Get Video source input terminal name - 'IRES', # Get Video source resolution - 'LKUP', # UPD Linkup status notification - 'MVOL', # Set microphone volume - 'RFIL', # Get replacement air filter model number - 'RLMP', # Get lamp replacement model number - 'RRES', # Get projector recommended video resolution - 'SNUM', # Get projector serial number - 'SRCH', # UDP broadcast search for available projectors on local network - 'SVER', # Get projector software version - 'SVOL', # Set speaker volume - 'TESTMEONLY' # For testing when other commands have been implemented - ] + # New commands available in PJLink Class 2 pjlink_udp_commands = [ 'ACKN', 'ERST', # Class 1 or 2 @@ -130,6 +113,7 @@ class PJLink(QtNetwork.QTcpSocket): self.port = port self.pin = pin super().__init__() + self.mac_adx = None if 'mac_adx' not in kwargs else kwargs['mac_adx'] self.dbid = None self.location = None self.notes = None diff --git a/openlp/core/lib/projector/pjlink2.py b/openlp/core/lib/projector/pjlink2.py new file mode 100644 index 000000000..65f2de336 --- /dev/null +++ b/openlp/core/lib/projector/pjlink2.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" + :mod:`openlp.core.lib.projector.pjlink2` module provides the PJLink Class 2 + updates from PJLink Class 1. + + This module only handles the UDP socket functionality. Command/query/status + change messages will still be processed by the PJLink 1 module. + + Currently, the only variance is the addition of a UDP "search" command to + query the local network for Class 2 capable projectors, + and UDP "notify" messages from projectors to connected software of status + changes (i.e., power change, input change, error changes). + + Differences between Class 1 and Class 2 PJLink specifications are as follows. + + New Functionality: + * Search - UDP Query local network for Class 2 capabable projector(s). + * Status - UDP Status change with connected projector(s). Status change + messages consist of: + * Initial projector power up when network communication becomes available + * Lamp off/standby to warmup or on + * Lamp on to cooldown or off/standby + * Input source select change completed + * Error status change (i.e., fan/lamp/temp/cover open/filter/other error(s)) + + New Commands: + * Query serial number of projector + * Query version number of projector software + * Query model number of replacement lamp + * Query model number of replacement air filter + * Query current projector screen resolution + * Query recommended screen resolution + * Query name of specific input terminal (video source) + * Adjust projector microphone in 1-step increments + * Adjust projector speacker in 1-step increments + + Extended Commands: + * Addition of INTERNAL terminal (video source) for a total of 6 types of terminals. + * Number of terminals (video source) has been expanded from [1-9] + to [1-9a-z] (Addition of 26 terminals for each type of input). + + See PJLink Class 2 Specifications for details. + http://pjlink.jbmia.or.jp/english/dl_class2.html + + Section 5-1 PJLink Specifications + + Section 5-5 Guidelines for Input Terminals +""" +import logging +log = logging.getLogger(__name__) + +log.debug('pjlink2 loaded') + +from PyQt5 import QtCore, QtNetwork + + +class PJLinkUDP(QtNetwork.QTcpSocket): + """ + Socket service for handling datagram (UDP) sockets. + """ + log.debug('PJLinkUDP loaded') + # Class varialbe for projector list. Should be replaced by ProjectorManager's + # projector list after being loaded there. + projector_list = None + projectors_found = None # UDP search found list diff --git a/openlp/core/lib/projector/upgrade.py b/openlp/core/lib/projector/upgrade.py index 4d2f4532e..913d54d2d 100644 --- a/openlp/core/lib/projector/upgrade.py +++ b/openlp/core/lib/projector/upgrade.py @@ -26,7 +26,8 @@ backend for the projector setup. import logging # Not all imports used at this time, but keep for future upgrades -from sqlalchemy import Column, types +from sqlalchemy import Table, Column, types, inspect +from sqlalchemy.exc import NoSuchTableError from sqlalchemy.sql.expression import null from openlp.core.common.db import drop_columns @@ -44,7 +45,7 @@ def upgrade_1(session, metadata): """ Version 1 upgrade - old db might/might not be versioned. """ - pass + log.debug('Skipping upgrade_1 of projector DB - not used') def upgrade_2(session, metadata): @@ -53,6 +54,7 @@ def upgrade_2(session, metadata): Update Projector() table to include new data defined in PJLink version 2 changes + mac_adx: Column(String(18)) serial_no: Column(String(30)) sw_version: Column(String(30)) model_filter: Column(String(30)) @@ -61,10 +63,10 @@ def upgrade_2(session, metadata): :param session: DB session instance :param metadata: Metadata of current DB """ - - new_op = get_upgrade_op(session) - if 'serial_no' not in [t.name for t in metadata.tables.values()]: + projector_table = Table('projector', metadata, autoload=True) + if 'mac_adx' not in [col.name for col in projector_table.c.values()]: log.debug("Upgrading projector DB to version '2'") + new_op = get_upgrade_op(session) new_op.add_column('projector', Column('mac_adx', types.String(18), server_default=null())) new_op.add_column('projector', Column('serial_no', types.String(30), server_default=null())) new_op.add_column('projector', Column('sw_version', types.String(30), server_default=null())) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index e47b3c1f9..d14da30e4 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -39,6 +39,7 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP from openlp.core.lib.projector.db import ProjectorDB from openlp.core.lib.projector.pjlink1 import PJLink +from openlp.core.lib.projector.pjlink2 import PJLinkUDP from openlp.core.ui.projector.editform import ProjectorEditForm from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle @@ -278,6 +279,10 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto """ Manage the projectors. """ + projector_list = [] + pjlink_udp = PJLinkUDP() + pjlink_udp.projector_list = projector_list + def __init__(self, parent=None, projectordb=None): """ Basic initialization. @@ -289,7 +294,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto super().__init__(parent) self.settings_section = 'projector' self.projectordb = projectordb - self.projector_list = [] + self.projector_list = self.__class__.projector_list self.source_select_form = None def bootstrap_initialise(self): @@ -987,7 +992,7 @@ class ProjectorItem(QtCore.QObject): self.poll_time = None self.socket_timeout = None self.status = S_NOT_CONNECTED - super(ProjectorItem, self).__init__() + super().__init__() def not_implemented(function): diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py index 1871ca718..b14e98321 100644 --- a/openlp/plugins/songs/lib/upgrade.py +++ b/openlp/plugins/songs/lib/upgrade.py @@ -32,7 +32,7 @@ from openlp.core.common.db import drop_columns from openlp.core.lib.db import get_upgrade_op log = logging.getLogger(__name__) -__version__ = 6 +__version__ = 7 # TODO: When removing an upgrade path the ftw-data needs updating to the minimum supported version @@ -52,6 +52,7 @@ def upgrade_1(session, metadata): :param metadata: """ op = get_upgrade_op(session) + metadata.reflect() if 'media_files_songs' in [t.name for t in metadata.tables.values()]: op.drop_table('media_files_songs') op.add_column('media_files', Column('song_id', types.Integer(), server_default=null())) @@ -119,7 +120,7 @@ def upgrade_6(session, metadata): """ Version 6 upgrade - This version corrects the errors in upgrades 4 and 5 + This version corrects the errors in upgrade 4 """ op = get_upgrade_op(session) # Move upgrade 4 to here and correct it (authors_songs table, not songs table) @@ -137,7 +138,17 @@ def upgrade_6(session, metadata): op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs') op.drop_table('authors_songs') op.rename_table('authors_songs_tmp', 'authors_songs') + + +def upgrade_7(session, metadata): + """ + Version 7 upgrade + + Corrects table error in upgrade 5 + """ # Move upgrade 5 here to correct it + op = get_upgrade_op(session) + metadata.reflect() if 'songs_songbooks' not in [t.name for t in metadata.tables.values()]: # Create the mapping table (songs <-> songbooks) op.create_table( diff --git a/openlp/plugins/songusage/lib/upgrade.py b/openlp/plugins/songusage/lib/upgrade.py index 377cc8a6d..f83022d84 100644 --- a/openlp/plugins/songusage/lib/upgrade.py +++ b/openlp/plugins/songusage/lib/upgrade.py @@ -25,17 +25,26 @@ backend for the SongsUsage plugin """ import logging -from sqlalchemy import Column, types +from sqlalchemy import Table, Column, types from openlp.core.lib.db import get_upgrade_op log = logging.getLogger(__name__) -__version__ = 1 +__version__ = 2 def upgrade_1(session, metadata): """ - Version 1 upgrade. + Version 1 upgrade + + Skip due to possible missed update from a 2.4-2.6 upgrade + """ + pass + + +def upgrade_2(session, metadata): + """ + Version 2 upgrade. This upgrade adds two new fields to the songusage database @@ -43,5 +52,7 @@ def upgrade_1(session, metadata): :param metadata: SQLAlchemy MetaData object """ op = get_upgrade_op(session) - op.add_column('songusage_data', Column('plugin_name', types.Unicode(20), server_default='')) - op.add_column('songusage_data', Column('source', types.Unicode(10), server_default='')) + songusage_table = Table('songusage_data', metadata, autoload=True) + if 'plugin_name' not in [col.name for col in songusage_table.c.values()]: + op.add_column('songusage_data', Column('plugin_name', types.Unicode(20), server_default='')) + op.add_column('songusage_data', Column('source', types.Unicode(10), server_default=''))