diff --git a/zukeUI/__init__.py b/zukeUI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zukeUI/assets/config.json b/zukeUI/assets/config.json new file mode 100644 index 0000000..abd1dd8 --- /dev/null +++ b/zukeUI/assets/config.json @@ -0,0 +1,5 @@ +{ +"zukebox_user": "MOHI", +"zukebox_ip": "10.30.255.175", +"zukebox_port": "5000" +} diff --git a/zukeUI/assets/default.jpg b/zukeUI/assets/default.jpg new file mode 100644 index 0000000..33e2794 Binary files /dev/null and b/zukeUI/assets/default.jpg differ diff --git a/zukeUI/assets/zukeUI.ui b/zukeUI/assets/zukeUI.ui new file mode 100644 index 0000000..d2fa9f7 --- /dev/null +++ b/zukeUI/assets/zukeUI.ui @@ -0,0 +1,305 @@ + + + Form + + + Qt::ApplicationModal + + + + 0 + 0 + 569 + 708 + + + + Qt::DefaultContextMenu + + + ZukeUI + + + + zukebox.jpgzukebox.jpg + + + + + 40 + 50 + 491 + 311 + + + + + + + QFrame::Sunken + + + + + + default.jpg + + + true + + + + + + + + + 30 + 9 + 511 + 31 + + + + + 12 + + + + + + + Qt::AlignCenter + + + true + + + + + + 40 + 480 + 491 + 171 + + + + QFrame::WinPanel + + + QFrame::Plain + + + + + 20 + 10 + 461 + 31 + + + + URL: + + + + + + 20 + 50 + 461 + 31 + + + + Message: + + + + + + 10 + 90 + 471 + 71 + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + + Play/Pause + + + + + + + Send + + + + + + + + + + 130 + 410 + 381 + 20 + + + + + + + false + + + Qt::Horizontal + + + + + + 130 + 440 + 381 + 20 + + + + + + + 100 + + + false + + + Qt::Horizontal + + + + + + 142 + 380 + 381 + 21 + + + + + DejaVu Sans + 12 + 50 + false + false + + + + + + + Qt::AlignCenter + + + true + + + + + + 60 + 670 + 441 + 31 + + + + + 12 + 75 + true + + + + + + + Qt::AlignCenter + + + + + + 65 + 410 + 61 + 20 + + + + Volume: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 65 + 440 + 61 + 20 + + + + Seek: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 65 + 380 + 61 + 21 + + + + Sent by: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + frame_2 + widget + track_title + volume_slider + seek_slider + track_sender + error_message + volume_label + label_2 + sentby_label + + + + diff --git a/zukeUI/assets/zukebox.jpg b/zukeUI/assets/zukebox.jpg new file mode 100644 index 0000000..1b6a4ed Binary files /dev/null and b/zukeUI/assets/zukebox.jpg differ diff --git a/zukeUI/assets/zukebox.svg b/zukeUI/assets/zukebox.svg new file mode 100644 index 0000000..720a50c --- /dev/null +++ b/zukeUI/assets/zukebox.svg @@ -0,0 +1,3 @@ + + + diff --git a/zukeUI/driver/__init__.py b/zukeUI/driver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zukeUI/driver/__pycache__/__init__.cpython-35.pyc b/zukeUI/driver/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..f9ef826 Binary files /dev/null and b/zukeUI/driver/__pycache__/__init__.cpython-35.pyc differ diff --git a/zukeUI/driver/driver.iml b/zukeUI/driver/driver.iml new file mode 100644 index 0000000..19dbd15 --- /dev/null +++ b/zukeUI/driver/driver.iml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/zukeUI/driver/utils/__init__.py b/zukeUI/driver/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zukeUI/driver/utils/__pycache__/__init__.cpython-35.pyc b/zukeUI/driver/utils/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..5915eed Binary files /dev/null and b/zukeUI/driver/utils/__pycache__/__init__.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/configuration/__init__.py b/zukeUI/driver/utils/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zukeUI/driver/utils/configuration/__pycache__/__init__.cpython-35.pyc b/zukeUI/driver/utils/configuration/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..bd2ddeb Binary files /dev/null and b/zukeUI/driver/utils/configuration/__pycache__/__init__.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/configuration/__pycache__/zukeconfiguration.cpython-35.pyc b/zukeUI/driver/utils/configuration/__pycache__/zukeconfiguration.cpython-35.pyc new file mode 100644 index 0000000..a984579 Binary files /dev/null and b/zukeUI/driver/utils/configuration/__pycache__/zukeconfiguration.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/configuration/zukeconfiguration.py b/zukeUI/driver/utils/configuration/zukeconfiguration.py new file mode 100644 index 0000000..510cd6a --- /dev/null +++ b/zukeUI/driver/utils/configuration/zukeconfiguration.py @@ -0,0 +1,74 @@ +import json +import os +from json import JSONDecodeError + + +class ZukeConfigNotExistsError(Exception): + pass + + +class InvalidZukeConfigFormatError(Exception): + pass + + +class InvalidZukeConfigError(Exception): + pass + +_ZUKE_IP = "zukebox_ip" +_ZUKE_PORT = "zukebox_port" +_ZUKE_USER = "zukebox_user" + +class ZukeConfigManager: + + def __init__(self, config_path: str): + self.__config_path = config_path + self.__config = {} + + def init_config(self): + self.__config = ZukeConfigInitializer.init_config(self.__config_path) + + @property + def config(self): + return self.__config + + @property + def zuke_ip(self): + return self.__config[_ZUKE_IP] + + @property + def zuke_port(self): + return self.__config[_ZUKE_PORT] + + @property + def zuke_user(self): + return self.__config[_ZUKE_USER] + + @property + def config_keys(self): + return self.config.keys() + + +class ZukeConfigInitializer: + + @staticmethod + def init_config(config_path: str) -> dict: + try: + config = json.load(open(config_path, 'r')) + ZukeConfigValidator.validate_zuke_config(config) + return config + except JSONDecodeError: + raise InvalidZukeConfigFormatError("Invalid zuke configuration format (not json)") + except IOError: + raise ZukeConfigNotExistsError("Zuke configuration not exists {path}".format(path=config_path)) + + +class ZukeConfigValidator: + + @staticmethod + def validate_zuke_config(config: dict): + if sorted( + (_ZUKE_USER, + _ZUKE_PORT, + _ZUKE_IP) + ) != sorted(config.keys()): + raise InvalidZukeConfigError("Config contains invalid keys") diff --git a/zukeUI/driver/utils/connection/__init__.py b/zukeUI/driver/utils/connection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zukeUI/driver/utils/connection/__pycache__/__init__.cpython-35.pyc b/zukeUI/driver/utils/connection/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..a431554 Binary files /dev/null and b/zukeUI/driver/utils/connection/__pycache__/__init__.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/connection/__pycache__/request_managing.cpython-35.pyc b/zukeUI/driver/utils/connection/__pycache__/request_managing.cpython-35.pyc new file mode 100644 index 0000000..e172e8b Binary files /dev/null and b/zukeUI/driver/utils/connection/__pycache__/request_managing.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/connection/__pycache__/zukeconnection.cpython-35.pyc b/zukeUI/driver/utils/connection/__pycache__/zukeconnection.cpython-35.pyc new file mode 100644 index 0000000..a9a3b24 Binary files /dev/null and b/zukeUI/driver/utils/connection/__pycache__/zukeconnection.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/connection/__pycache__/zukerequest_managing.cpython-35.pyc b/zukeUI/driver/utils/connection/__pycache__/zukerequest_managing.cpython-35.pyc new file mode 100644 index 0000000..39162d0 Binary files /dev/null and b/zukeUI/driver/utils/connection/__pycache__/zukerequest_managing.cpython-35.pyc differ diff --git a/zukeUI/driver/utils/connection/request_managing.py b/zukeUI/driver/utils/connection/request_managing.py new file mode 100644 index 0000000..0a75e1c --- /dev/null +++ b/zukeUI/driver/utils/connection/request_managing.py @@ -0,0 +1,118 @@ +import json + +import time +from PyQt5 import QtCore + +from driver.utils.configuration.zukeconfiguration import ZukeConfigManager +from driver.utils.connection.zukeconnection import ZukeRequest, ZukeRequestThread + + +class RequestManager(QtCore.QThread): + + def __init__(self, config_manager: ZukeConfigManager): + QtCore.QThread.__init__(self) + self._config_manager = config_manager + self._requests = [] + self._refreshing = False + self._callback = None + self._control = None + + def send_request(self, request: ZukeRequest, response_callback=None): + request = ZukeRequestThread(request, response_callback) + request.start() + self._requests.append(request) + + def send_track(self, url, response_callback=None, message: str=None): + + body = {'url': url, 'user': self._config_manager.zuke_user} + if message: + body.update({'message': message, 'lang': 'hu'}) + self.send_request( + ZukeRequest( + self._config_manager.zuke_ip, + self._config_manager.zuke_port, + "POST", + "/player/tracks", + body=json.dumps(body), + headers={'content-type': 'application/json'} + ), + response_callback + ) + + def set_volume(self, volume_value, response_callback=None): + self.send_request( + ZukeRequest( + self._config_manager.zuke_ip, + self._config_manager.zuke_port, + "PATCH", + "/player/control", + body=json.dumps({'volume': volume_value}), + headers={'content-type': 'application/json'} + ), + response_callback + ) + + def set_seek(self, seek_value, response_callback=None): + self.send_request( + ZukeRequest( + self._config_manager.zuke_ip, + self._config_manager.zuke_port, + "PATCH", + "/player/control", + body=json.dumps({'time': seek_value}), + headers={'content-type': 'application/json'} + ), + response_callback + ) + + + manager_id = "playpause_manager" + response_signal = stop_signal = QtCore.pyqtSignal(dict, name=manager_id) + + def play_or_pause(self, play_or_pause, response_callback=None): + self.send_request( + ZukeRequest( + self._config_manager.zuke_ip, + self._config_manager.zuke_port, + "PATCH", + "/player/control", + body=json.dumps({'playing': play_or_pause}), + headers={'content-type': 'application/json'} + ), + response_callback + ) + + def get_control(self, callback=None): + self.send_request( + ZukeRequest( + self._config_manager.zuke_ip, + self._config_manager.zuke_port, + "GET", + "/player/control" + ), + callback + ) + + def run(self): + while self._refreshing: + self.get_control(self._callback) + time.sleep(1) + + def _refresh_callback(self, response): + self._control = response + self._callback(response) + + def start_refreshing(self, callback=None): + if not self._refreshing: + self._callback = callback + self._refreshing = True + self.start() + + def stop_refreshing(self): + if self._refreshing: + self._refreshing = False + self.quit() + + @property + def control(self): + return self._control \ No newline at end of file diff --git a/zukeUI/driver/utils/connection/zukeconnection.py b/zukeUI/driver/utils/connection/zukeconnection.py new file mode 100644 index 0000000..c5066e2 --- /dev/null +++ b/zukeUI/driver/utils/connection/zukeconnection.py @@ -0,0 +1,56 @@ +import http.client +import json +from PyQt5 import QtCore +from json import JSONDecodeError + + +class InvalidZukeResponseError(Exception): + pass + + +class ZukeAvailabilityError(Exception): + pass + + +class ZukeRequest: + + def __init__(self, ip: str, port: str, command: str, url: str, *, body: dict=None, headers: dict=None): + self.ip = ip + self.port = port + self.command = command + self.url = url + self.body = body if body else "" + self.headers = headers if headers else {} + + def do_request(self): + conn = http.client.HTTPConnection(self.ip, self.port) + try: + conn.request( + self.command, + self.url, + self.body, + self.headers + ) + return json.loads(conn.getresponse().read().decode('utf-8')) + except JSONDecodeError: + raise InvalidZukeResponseError("Invalid zukeresponse") + except: + raise ZukeAvailabilityError("Zukebox unavailable") + finally: + conn.close() + + +class ZukeRequestThread(QtCore.QThread): + response_signal = QtCore.pyqtSignal(dict) + + def __init__(self, zukerequest: ZukeRequest, callback): + QtCore.QThread.__init__(self) + self._zr = zukerequest + self._callback = callback + + def run(self): + if self._callback: + self.response_signal.connect(self._callback) + self.response_signal.emit(self._zr.do_request()) + else: + self._zr.do_request() diff --git a/zukeUI/runner.sh b/zukeUI/runner.sh new file mode 100755 index 0000000..64b117a --- /dev/null +++ b/zukeUI/runner.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +BASEDIR=$(dirname "$0") + +if [[ *"zukeUI"* != $PYTHONPATH ]]; then + export PYTHONPATH=${BASEDIR} +fi +python3 ./${BASEDIR}/ui/zukeui.py ./${BASEDIR}/assets/ \ No newline at end of file diff --git a/zukeUI/ui/__init__.py b/zukeUI/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zukeUI/ui/__pycache__/__init__.cpython-35.pyc b/zukeUI/ui/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..e5869cf Binary files /dev/null and b/zukeUI/ui/__pycache__/__init__.cpython-35.pyc differ diff --git a/zukeUI/ui/zukeui.py b/zukeUI/ui/zukeui.py new file mode 100644 index 0000000..4e90ec8 --- /dev/null +++ b/zukeUI/ui/zukeui.py @@ -0,0 +1,131 @@ +import os +import sys + +import urllib.request + +from PyQt5 import QtWidgets + +from PyQt5 import uic +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QStyle + +from driver.utils.configuration.zukeconfiguration import ZukeConfigManager +from driver.utils.connection.zukeconnection import ZukeAvailabilityError, InvalidZukeResponseError +from driver.utils.connection.request_managing import RequestManager +from zukeui_manager import ZukeUiManager + + +class Ui(QtWidgets.QDialog): + def __init__(self, path, zuc: ZukeUiManager): + super(Ui, self).__init__() + uic.loadUi(path, self) + self.__zuc = zuc + self.track_picture = self.findChild(QtWidgets.QLabel, 'track_picture') + self.track_sender = self.findChild(QtWidgets.QLabel, 'track_sender') + self.track_title = self.findChild(QtWidgets.QLabel, 'track_title') + self.error_message = self.findChild(QtWidgets.QLabel, 'error_message') + self.send_button = self.findChild(QtWidgets.QPushButton, 'send_button') + self.playorpause_button = self.findChild(QtWidgets.QPushButton, 'playorpause_button') + self.link_edit = self.findChild(QtWidgets.QTextEdit, 'link_edit') + self.message_edit = self.findChild(QtWidgets.QTextEdit, 'message_edit') + self.volume_slider = self.findChild(QtWidgets.QSlider, 'volume_slider') + self.seek_slider = self.findChild(QtWidgets.QSlider, 'seek_slider') + + self.send_button.clicked.connect(self.send_link) + self.playorpause_button.clicked.connect(self.play_or_pause) + self.volume_slider.valueChanged.connect(self.set_volume) + self.seek_slider.valueChanged.connect(self.set_seek) + self.__is_refreshing = False + self.__prev_title = None + self.__prev_url = None + try: + self.__zuc.start_refreshing(self._refresh_callback) + except (ZukeAvailabilityError, InvalidZukeResponseError) as ex: + self.__zuc.stop_refreshing() + print("Exception during starting refreshing") + self.error_message.setText("Error! Ui refreshing turned off") + self.__zuc.start_refreshing(self._refresh_callback) + + def __del__(self): + self.__zuc.stop_refreshing() + + def send_link(self): + try: + self.__zuc.send_url(self.link_edit.toPlainText(), self.message_edit.toPlainText(), self._send_callback) + except (ZukeAvailabilityError, InvalidZukeResponseError) as ex: + self.__zuc.stop_refreshing() + print("Exception during sending link") + self.error_message.setText("Error! Ui refreshing turned off") + + def _send_callback(self, response): + if self.link_edit.toPlainText(): + self.link_edit.setText("") + if self.message_edit.toPlainText(): + self.message_edit.setText("") + + def play_or_pause(self): + try: + if self.__zuc.control: + self.__zuc.play_or_pause(0 if self.__zuc.control["playing"] else 1) + else: + self.__zuc.get_control(self.control_to_play_or_pause_callback) + except (ZukeAvailabilityError, InvalidZukeResponseError) as ex: + self.__zuc.stop_refreshing() + print("Exception during play or pause") + self.error_message.setText("Error! Ui refreshing turned off") + + def set_volume(self): + if not self.__is_refreshing: + try: + self.__zuc.set_volume(self.volume_slider.sliderPosition()) + except (ZukeAvailabilityError, InvalidZukeResponseError) as ex: + self.__zuc.stop_refreshing() + print("Exception during setting volume") + self.error_message.setText("Error! Ui refreshing turned off") + + def set_seek(self): + if not self.__is_refreshing: + try: + self.__zuc.set_seek(self.seek_slider.sliderPosition()) + except (ZukeAvailabilityError, InvalidZukeResponseError) as ex: + self.__zuc.stop_refreshing() + print("Exception during setting seek") + self.error_message.setText("Error! Ui refreshing turned off") + + def _refresh_callback(self, control: dict): + track = control.get("track", {}) + title = track.get("title", None) if track else None + if title: + if title != self.__prev_title: + self.track_picture.setPixmap(QPixmap(urllib.request.urlretrieve(track["thumbnail"])[0])) + self.track_sender.setText(track["user"]) + self.track_title.setText(title) + self.__prev_title = title + if not self.seek_slider.isSliderDown(): + self.__is_refreshing = True + self.seek_slider.setMaximum(track["duration"]) + self.seek_slider.setSliderPosition(control["time"]) + self.__is_refreshing = False + if not self.volume_slider.isSliderDown(): + self.__is_refreshing = True + self.volume_slider.setSliderPosition(control["volume"]) + self.__is_refreshing = False + + def control_to_play_or_pause_callback(self, response): + self.__zuc.play_or_pause(0 if response["playing"] else 1) + + +if __name__ == "__main__": + assets_path = sys.argv[1] + zcm = ZukeConfigManager(os.path.join(assets_path, 'config.json')) + zcm.init_config() + app = QApplication(sys.argv) + w = Ui( + os.path.join(assets_path, 'zukeUI.ui'), + ZukeUiManager( + RequestManager(zcm) + ) + ) + w.show() + sys.exit(app.exec_()) diff --git a/zukeUI/ui/zukeui_manager.py b/zukeUI/ui/zukeui_manager.py new file mode 100644 index 0000000..b6b3d66 --- /dev/null +++ b/zukeUI/ui/zukeui_manager.py @@ -0,0 +1,33 @@ +from driver.utils.connection.request_managing import RequestManager + + +class ZukeUiManager: + + def __init__(self, rm: RequestManager): + self.__rm = rm + + def send_url(self, url: str, message: str, callback=None): + if url: + self.__rm.send_track(url, callback, message) + + def set_volume(self, volume, callback=None): + self.__rm.set_volume(volume, callback) + + def set_seek(self, seek, callback=None): + self.__rm.set_seek(seek, callback) + + def play_or_pause(self, play_or_pause, callback=None): + self.__rm.play_or_pause(play_or_pause, callback) + + def stop_refreshing(self): + self.__rm.stop_refreshing() + + def start_refreshing(self, ui_callback): + self.__rm.start_refreshing(ui_callback) + + def get_control(self, callback=None): + self.__rm.get_control(callback) + + @property + def control(self): + return self.__rm.control diff --git a/zukeUI/zukeUI.iml b/zukeUI/zukeUI.iml new file mode 100644 index 0000000..c2feb10 --- /dev/null +++ b/zukeUI/zukeUI.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file