From 14552bd3da25a4957d8f4fa89c3fc2bd92f76cfb Mon Sep 17 00:00:00 2001 From: arne Date: Sat, 13 Feb 2021 22:27:50 +0100 Subject: [PATCH 001/606] Inital commit, test to use zero mq as IPC --- components/gpio_control/function_calls.py | 47 +++-- components/gpio_control/gpio_control.py | 9 +- htdocs/api/player.php | 17 +- .../api/playlist/moveDownSongInPlaylist.php | 0 htdocs/api/playlist/moveUpSongInPlaylist.php | 0 .../api/playlist/removeSongFromPlaylist.php | 0 htdocs/api/volume.php | 10 +- htdocs/api/zmq.php | 38 ++++ htdocs/inc.addSystemInfo.php | 0 htdocs/inc.navigation.php | 4 + htdocs/inc.setSecondSwipePause.php | 0 htdocs/inc.setSecondSwipePauseControls.php | 0 htdocs/lang/lang-de-DE.php | 53 +---- htdocs/phpinfo.php | 5 + scripts/Reader.py | 189 ++++++++++++++---- scripts/Reader.py.experimental | 168 ---------------- scripts/phonie_access_objects.py | 54 +++++ scripts/playout_controls.sh | 8 +- scripts/startup-scripts.sh | 43 ++-- scripts/startup_sound.sh | 11 + scripts/test_zmq.py | 160 +++++++++++++++ shared/audiofolders/placeholder | 0 22 files changed, 518 insertions(+), 298 deletions(-) mode change 100644 => 100755 htdocs/api/playlist/moveDownSongInPlaylist.php mode change 100644 => 100755 htdocs/api/playlist/moveUpSongInPlaylist.php mode change 100644 => 100755 htdocs/api/playlist/removeSongFromPlaylist.php create mode 100644 htdocs/api/zmq.php mode change 100644 => 100755 htdocs/inc.addSystemInfo.php mode change 100644 => 100755 htdocs/inc.setSecondSwipePause.php mode change 100644 => 100755 htdocs/inc.setSecondSwipePauseControls.php create mode 100644 htdocs/phpinfo.php delete mode 100755 scripts/Reader.py.experimental create mode 100644 scripts/phonie_access_objects.py create mode 100755 scripts/startup_sound.sh create mode 100644 scripts/test_zmq.py delete mode 100755 shared/audiofolders/placeholder diff --git a/components/gpio_control/function_calls.py b/components/gpio_control/function_calls.py index b87f96b8b..f1ff7eacc 100644 --- a/components/gpio_control/function_calls.py +++ b/components/gpio_control/function_calls.py @@ -4,6 +4,20 @@ import os import pathlib + + +currentdir = os.path.dirname(os.path.realpath(__file__)) +parentdir = os.path.dirname(currentdir) +parentdir = os.path.dirname(parentdir) +print (parentdir) + +sys.path.append(parentdir+"/scripts") + +print(sys.path) + +from phonie_access_objects import phoniebox_object_access_queue + + logger = logging.getLogger(__name__) playout_control_relative_path = "../../scripts/playout_controls.sh" @@ -15,21 +29,29 @@ def functionCallShutdown(*args): def functionCallVolU(steps=None): - if steps is None: - function_call("{command} -c=volumeup".format(command=playout_control), shell=True) - else: - function_call("{command} -c=volumeup -v={steps}".format(steps=steps, - command=playout_control), - shell=True) + queue = phoniebox_object_access_queue() + queue.connect() + resp = queue.phonie_enqueue({'obj':'volume','cmd':'inc','param':None}) + + #if steps is None: + # function_call("{command} -c=volumeup".format(command=playout_control), shell=True) + #else: + # function_call("{command} -c=volumeup -v={steps}".format(steps=steps, + # command=playout_control), + # shell=True) def functionCallVolD(steps=None): - if steps is None: - function_call("{command} -c=volumedown".format(command=playout_control), shell=True) - else: - function_call("{command} -c=volumedown -v={steps}".format(steps=steps, - command=playout_control), - shell=True) + queue = phoniebox_object_access_queue() + queue.connect() + resp = queue.phonie_enqueue({'obj':'volume','cmd':'dec','param':None}) + + #if steps is None: + # function_call("{command} -c=volumedown".format(command=playout_control), shell=True) + #else: + # function_call("{command} -c=volumedown -v={steps}".format(steps=steps, + # command=playout_control), + # shell=True) def functionCallVol0(*args): @@ -85,3 +107,4 @@ def getFunctionCall(functionName): logger.error('Get FunctionCall: {} {}'.format(functionName, functionName in locals())) getattr(sys.modules[__name__], str) return locals().get(functionName, None) + diff --git a/components/gpio_control/gpio_control.py b/components/gpio_control/gpio_control.py index 3e6134495..7164f0356 100755 --- a/components/gpio_control/gpio_control.py +++ b/components/gpio_control/gpio_control.py @@ -102,9 +102,14 @@ def get_all_devices(config): if __name__ == "__main__": - logging.basicConfig(level='INFO') + lf = '%(asctime)s %(message)s' + lp = '/home/pi/RPi-Jukebox-RFID/logs/gpio.log' + #logging.basicConfig(filename=lp, level=logging.DEBUG,format=lf) + logging.basicConfig(level=logging.INFO,format=lf) logger = logging.getLogger() - logger.setLevel('INFO') + #logger.setLevel('INFO') + + logger.info('GPIO Started') config = configparser.ConfigParser(inline_comment_prefixes=";") config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') diff --git a/htdocs/api/player.php b/htdocs/api/player.php index 9d2d92f16..f0672b5d2 100755 --- a/htdocs/api/player.php +++ b/htdocs/api/player.php @@ -1,6 +1,8 @@ 'player','cmd'=>$inputCommand,'param'=>'')); + } + else + { + $controlsCommand = determineCommand($inputCommand); + $controlsValue = $inputValue !== "" ? " -v=" . ((float)$inputValue) : ""; + $execCommand = "playout_controls.sh {$controlsCommand}{$controlsValue}"; + execScript($execCommand); + } } else { echo "Body is missing command"; http_response_code(400); diff --git a/htdocs/api/playlist/moveDownSongInPlaylist.php b/htdocs/api/playlist/moveDownSongInPlaylist.php old mode 100644 new mode 100755 diff --git a/htdocs/api/playlist/moveUpSongInPlaylist.php b/htdocs/api/playlist/moveUpSongInPlaylist.php old mode 100644 new mode 100755 diff --git a/htdocs/api/playlist/removeSongFromPlaylist.php b/htdocs/api/playlist/removeSongFromPlaylist.php old mode 100644 new mode 100755 diff --git a/htdocs/api/volume.php b/htdocs/api/volume.php index 0ba69365c..f2355cd07 100755 --- a/htdocs/api/volume.php +++ b/htdocs/api/volume.php @@ -1,6 +1,8 @@ 'volume','cmd'=>'set','param'=>array('volume'=>(int)$body))); + + #if ($response == NULL) {$response = "No Response receivd";}; + #echo($response); } else { http_response_code(400); } diff --git a/htdocs/api/zmq.php b/htdocs/api/zmq.php new file mode 100644 index 000000000..d15c38161 --- /dev/null +++ b/htdocs/api/zmq.php @@ -0,0 +1,38 @@ +($nanotime)), $request); + } + + $queue = new ZMQSocket(new ZMQContext(), ZMQ::SOCKET_REQ); + $queue->connect("tcp://127.0.0.1:5555"); + + $queue->setSockOpt(ZMQ::SOCKOPT_RCVTIMEO,200); + $queue->setSockOpt(ZMQ::SOCKOPT_LINGER,200); + + $queue->send(json_encode($request)); + + try { + $message = $queue->recv(); + } catch (ZMQSocketException $e) { + /* EAGAIN means that the operation would have blocked, retry */ + /*if ($e->getCode() === ZMQ::ERR_EAGAIN) { + $message = "Got EAGAIN, retrying \n"; + } else { + $message = "Error: " . $e->getMessage(); + }*/ + $message = "timeout"; + } + + ##$queue->close(); + + return $message; +} + +?> \ No newline at end of file diff --git a/htdocs/inc.addSystemInfo.php b/htdocs/inc.addSystemInfo.php old mode 100644 new mode 100755 diff --git a/htdocs/inc.navigation.php b/htdocs/inc.navigation.php index 0bd279791..59d2a9f22 100755 --- a/htdocs/inc.navigation.php +++ b/htdocs/inc.navigation.php @@ -52,8 +52,12 @@ + + +
diff --git a/htdocs/inc.setSecondSwipePause.php b/htdocs/inc.setSecondSwipePause.php old mode 100644 new mode 100755 diff --git a/htdocs/inc.setSecondSwipePauseControls.php b/htdocs/inc.setSecondSwipePauseControls.php old mode 100644 new mode 100755 diff --git a/htdocs/lang/lang-de-DE.php b/htdocs/lang/lang-de-DE.php index 676620772..05ba1f2b2 100755 --- a/htdocs/lang/lang-de-DE.php +++ b/htdocs/lang/lang-de-DE.php @@ -1,6 +1,5 @@ Du kannst Karten auch manuell mit Ordnern verbinden. Das Handbuch erklärt, wie man sich mit der Phoniebox verbindet und Karten registriert.

"; $lang['cardRegisterTriggerSuccess'] = "Die Karte ist jetzt verknüpft um die Funktion auszuführen:"; - -/* -* "Karten bearbeiten"-Formular -*/ $lang['cardFormFolderLegend'] = "RFID-Karte verlinken mit:"; $lang['cardFormFolderLabel'] = "Einen Audio-Ordner auswählen"; $lang['cardFormFolderSelectDefault'] = "Keiner (--Wählen-- zur Auswahl eines Ordners)"; @@ -147,7 +130,6 @@ $lang['cardFormTriggerLabel'] = "... eine Phoniebox Funktion auswählen"; $lang['cardFormTriggerHelp'] = "Wähle eine Funktion aus der Liste aus (z.B. 'pause', 'volume up', 'shutdown'). Bestehende Verknüpfungen werden im Pulldown-Menü angezeigt."; $lang['cardFormTriggerSelectDefault'] = "Wähle eine Phoniebox Funktion"; - $lang['cardFormStreamLegend'] = "Stream verlinken / erstellen"; $lang['cardFormStreamLabel'] = "Stream URL (benötigt immer einen neuen Ordner - s.o.)"; $lang['cardFormStreamPlaceholderClassic'] = "http(...).mp3 / .m3u / .ogg / .rss / .xml / ..."; @@ -155,19 +137,14 @@ $lang['cardFormStreamHelp'] = "Füge die URL für spotify, Podcast, Webradio, Stream oder andere Online-Medien hinzu"; $lang['cardFormStreamTypeSelectDefault'] = "Wähle den Typ"; $lang['cardFormStreamTypeHelp'] = "Wähle die Art des Streams, den du hinzufügen möchtest"; - $lang['cardFormYTLegend'] = "Von YouTube Herunterladen"; $lang['cardFormYTLabel'] = "YouTube URL (einzelner Track oder Playlist)"; $lang['cardFormYTPlaceholder'] = "z.B. https://www.youtube.com/watch?v=7GI0VdPehQI"; $lang['cardFormYTSelectDefault'] = "--Wählen--, um einen Ordner auszuwählen oder einen neuen darunter zu erstellen"; $lang['cardFormYTHelp'] = "Füge die volle YouTube-URL wie im Beispiel hinzu"; $lang['cardFormRemoveCard'] = "Karten-ID entfernen"; - -// Karten IDs als .csv-Datei exportieren $lang['cardExportAnchorLink'] = "Alle RFID Verknüpfungen exportieren (Audio und Systembefehle)"; $lang['cardExportButtonLink'] = ".csv-Datei aller verfügbaren RFID-Verknüpfungen erstellen"; - -// Karten IDs aus .csv-Datei importieren $lang['cardImportAnchorLink'] = "RFID Verknüpfungen aus .csv-Datei importieren"; $lang['cardImportFileLabel'] = ".csv-Datei auswählen um RFID-Verknüpfungen zu erstellen"; $lang['cardImportFileSuccessUpload'] = "Datei erfolgreich hochgeladen: "; @@ -188,10 +165,6 @@ $lang['cardImportFormDeleteHelp'] = "Welche der bestehenden RFID-Verknüpfungen sollen behalten werden, welche gelöscht?"; $lang['cardImportFileDeleteMessageCommands'] = "

Systembefehle gelöscht.

"; $lang['cardImportFileDeleteMessageAudio'] = "

Audio Verknüpfungen gelöscht.

"; - -/* -* "Track bearbeiten"-Formular -*/ $lang['trackEditTitle'] = "Track-Management"; $lang['trackEditInformation'] = "Track-Informationen"; $lang['trackEditMove'] = "Track verschieben"; @@ -202,10 +175,6 @@ $lang['trackEditDeleteHelp'] = "Es gibt kein Rückgängigmachen für gelöschte Dateien. Sie sind weg! Bist du sicher?"; $lang['trackEditDeleteNo'] = "Diesen Track NICHT löschen"; $lang['trackEditDeleteYes'] = "Ja, diesen Track LÖSCHEN"; - -/* -* Einstellungen -*/ $lang['settingsVolChangePercent'] = "Lautst. Änderung"; $lang['settingsMaxVol'] = "Max. Lautstärke"; $lang['settingsStartupVol'] = "Start-Lautstärke"; @@ -238,17 +207,12 @@ $lang['settingsWlanSendEmail'] = "E-Mail Adr."; $lang['settingsWlanSendON'] = "Ja, E-Mail senden."; $lang['settingsWlanSendOFF'] = "Nein, E-Mail nicht senden."; - - +$lang['settingsVolumeManager'] = "Select volume manager"; $lang['settingsWlanReadNav'] = "Wlan IP vorlesen"; $lang['settingsWlanReadInfo'] = "Wlan IP bei jedem Systemstart vorlesen? (nützlich wenn du deine Phoniebox in ein neues Wlan-Netzwerk mit dynamischer IP verbindest)"; $lang['settingsWlanReadQuest'] = "Wlan IP vorlesen?"; $lang['settingsWlanReadON'] = "Ja, Wlan IP vorlesen."; $lang['settingsWlanReadOFF'] = "Nein, Wlan IP nicht vorlesen."; - -/* -* Systeminformationen -*/ $lang['infoOsDistrib'] = "Betriebssystem"; $lang['infoOsCodename'] = "Codename"; $lang['infoOsTemperature'] = "Temperatur"; @@ -259,10 +223,6 @@ $lang['infoDebugLogTail'] = "DEBUG Logdatei: Letzte 40 Zeilen"; $lang['infoDebugLogClear'] = "Lösche Inhalt von debug.log"; $lang['infoDebugLogSettings'] = "Debug Log Einstellungen"; - -/* -* Ordnerverwaltung und Dateien hochladen -*/ $lang['manageFilesFoldersTitle'] = "Ordner & Dateien"; $lang['manageFilesFoldersUploadFilesLabel'] = "Dateien von deinem Laufwerk auswählen"; $lang['manageFilesFoldersUploadLegend'] = "Dateien hochladen"; @@ -278,22 +238,13 @@ $lang['manageFilesFoldersErrorNewFolderNotParent'] = "

Der übergeordnete Ordner existiert nicht.

"; $lang['manageFilesFoldersSuccessNewFolder'] = "Neuer Ordner erstellt: "; $lang['manageFilesFoldersSelectDefault'] = "--Wählen--, um einen Ordner auszuwählen und/oder einen neuen Unterordner zu erstellen"; - $lang['manageFilesFoldersRenewDB'] = "Datenbank erneuern"; $lang['manageFilesFoldersLocalScan'] = "Musikbibliothek scannen"; $lang['manageFilesFoldersRenewDBinfo'] = "Bitte scanne deine Musikbibliothek, nachdem du neue Dateien hochgeladen oder Ordner verschoben hast. Der Scan ist nicht notwendig, um Musik zu hören, aber es ist notwendig, um Track-Informationen in der Web-Oberfläche zu sehen. Es werden nur neue oder verschobene Dateien gescannt. Während der Scan läuft, wird Mopidy gestoppt. Nach Abschluss des Scans startet Mopidy automatisch neu. Den Serverstatus siehst du im Abschnitt Info."; - -/* -* Dateisuche -*/ $lang['searchTitle'] = "Audiodateien suchen"; $lang['searchExample'] = "z.B. Moonlight"; $lang['searchSend'] = "Suchen"; $lang['searchResult'] = "Suchergebnisse:"; - -/* -* Filter -*/ $lang['filterall'] = "Zeige alle"; $lang['filterfile'] = "Dateien"; $lang['filterlivestream'] = "Livestream"; diff --git a/htdocs/phpinfo.php b/htdocs/phpinfo.php new file mode 100644 index 000000000..554a22c8b --- /dev/null +++ b/htdocs/phpinfo.php @@ -0,0 +1,5 @@ + diff --git a/scripts/Reader.py b/scripts/Reader.py index 3b583da4b..1db4af167 100755 --- a/scripts/Reader.py +++ b/scripts/Reader.py @@ -1,63 +1,168 @@ #!/usr/bin/env python3 -# There are a variety of RFID readers out there, USB and non-USB variants. -# This might create problems in recognizing the reader you are using. -# We haven't found the silver bullet yet. If you can contribute to this -# quest, please comment in the issue thread or create pull requests. -# ALTERNATIVE SCRIPTS: -# If you encounter problems with this script Reader.py -# consider and test one of the alternatives in the same scripts folder. -# Replace the Reader.py file with one of the following files: -# * Reader.py.experimental -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522, RDM6300 or PN532. -# * Reader.py.kkmoonRFIDreader -# KKMOON RFID Reader which appears twice in the devices list as HID 413d:2107 -# and this required to check "if" the device is a keyboard. - -# import string -# import csv +# This alternative Reader.py script was meant to cover not only USB readers but more. +# It can be used to replace Reader.py if you have readers such as +# MFRC522, RDM6300 or PN532. +# Please use the github issue threads to share bugs and improvements +# or create pull requests. + import os.path import sys +import serial +import string +import RPi.GPIO as GPIO +import logging + +from evdev import InputDevice, categorize, ecodes, list_devices +# Workaround: when using RC522 reader with pirc522 pkg the py532lib pkg may not be installed and vice-versa +try: + import pirc522 + from py532lib.i2c import * + from py532lib.mifare import * +except ImportError: + pass -from evdev import InputDevice, ecodes, list_devices -from select import select +logger = logging.getLogger(__name__) def get_devices(): - return [InputDevice(fn) for fn in list_devices()] + devices = [InputDevice(fn) for fn in list_devices()] + devices.append(NonUsbDevice('MFRC522')) + devices.append(NonUsbDevice('RDM6300')) + devices.append(NonUsbDevice('PN532')) + return devices -class Reader: - reader = None +class NonUsbDevice(object): + name = None - def __init__(self): - self.reader = self - path = os.path.dirname(os.path.realpath(__file__)) + def __init__(self, name): + self.name = name + + +class UsbReader(object): + def __init__(self, device): self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - deviceName = f.read() - devices = get_devices() - for device in devices: - if device.name == deviceName: - self.dev = device - break - try: - self.dev - except: - sys.exit('Could not find the device %s\n. Make sure is connected' % deviceName) + self.dev = device def readCard(self): + from select import select stri = '' key = '' while key != 'KEY_ENTER': - r, w, x = select([self.dev], [], []) + select([self.dev], [], []) for event in self.dev.read(): if event.type == 1 and event.value == 1: stri += self.keys[event.code] - # print( keys[ event.code ] ) key = ecodes.KEY[event.code] return stri[:-1] + + +class Mfrc522Reader(object): + def __init__(self): + self.device = pirc522.RFID() + + def readCard(self): + # Scan for cards + self.device.wait_for_tag() + (error, tag_type) = self.device.request() + + if not error: + logger.info("Card detected.") + # Perform anti-collision detection to find card uid + (error, uid) = self.device.anticoll() + if not error: + card_id = ''.join((str(x) for x in uid)) + logger.info(card_id) + return card_id + logger.debug("No Device ID found.") + return None + + @staticmethod + def cleanup(): + GPIO.cleanup() + + +class Rdm6300Reader: + def __init__(self): + device = '/dev/ttyS0' + baudrate = 9600 + ser_timeout = 0.1 + self.last_card_id = '' + try: + self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) + except serial.SerialException as e: + logger.error(e) + exit(1) + + def readCard(self): + byte_card_id = b'' + + try: + while True: + try: + read_byte = self.rfid_serial.read() + + if read_byte == b'\x02': # start byte + while read_byte != b'\x03': # end bye + read_byte = self.rfid_serial.read() + byte_card_id += read_byte + + card_id = byte_card_id.decode('utf-8') + byte_card_id = '' + card_id = ''.join(x for x in card_id if x in string.printable) + + # Only return UUIDs with correct length + if len(card_id) == 12 and card_id != self.last_card_id: + self.last_card_id = card_id + self.rfid_serial.reset_input_buffer() + return self.last_card_id + + else: # wrong UUID length or already send that UUID last time + self.rfid_serial.reset_input_buffer() + + except ValueError as ve: + logger.errror(ve) + + except serial.SerialException as se: + logger.error(se) + + def cleanup(self): + self.rfid_serial.close() + + +class Pn532Reader: + def __init__(self): + pn532 = Pn532_i2c() + self.device = Mifare() + self.device.SAMconfigure() + self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY) + + def readCard(self): + return str(+int('0x' + self.device.scan_field().hex(), 0)) + + def cleanup(self): + # Not sure if something needs to be done here. + logger.debug("PN532Reader clean up.") + + +class Reader(object): + def __init__(self): + path = os.path.dirname(os.path.realpath(__file__)) + if not os.path.isfile(path + '/deviceName.txt'): + sys.exit('Please run RegisterDevice.py first') + else: + with open(path + '/deviceName.txt', 'r') as f: + device_name = f.read() + + if device_name == 'MFRC522': + self.reader = Mfrc522Reader() + elif device_name == 'RDM6300': + self.reader = Rdm6300Reader() + elif device_name == 'PN532': + self.reader = Pn532Reader() + else: + try: + device = [device for device in get_devices() if device.name == device_name][0] + self.reader = UsbReader(device) + except IndexError: + sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) diff --git a/scripts/Reader.py.experimental b/scripts/Reader.py.experimental deleted file mode 100755 index 1db4af167..000000000 --- a/scripts/Reader.py.experimental +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python3 -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522, RDM6300 or PN532. -# Please use the github issue threads to share bugs and improvements -# or create pull requests. - -import os.path -import sys -import serial -import string -import RPi.GPIO as GPIO -import logging - -from evdev import InputDevice, categorize, ecodes, list_devices -# Workaround: when using RC522 reader with pirc522 pkg the py532lib pkg may not be installed and vice-versa -try: - import pirc522 - from py532lib.i2c import * - from py532lib.mifare import * -except ImportError: - pass - -logger = logging.getLogger(__name__) - - -def get_devices(): - devices = [InputDevice(fn) for fn in list_devices()] - devices.append(NonUsbDevice('MFRC522')) - devices.append(NonUsbDevice('RDM6300')) - devices.append(NonUsbDevice('PN532')) - return devices - - -class NonUsbDevice(object): - name = None - - def __init__(self, name): - self.name = name - - -class UsbReader(object): - def __init__(self, device): - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - self.dev = device - - def readCard(self): - from select import select - stri = '' - key = '' - while key != 'KEY_ENTER': - select([self.dev], [], []) - for event in self.dev.read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - key = ecodes.KEY[event.code] - return stri[:-1] - - -class Mfrc522Reader(object): - def __init__(self): - self.device = pirc522.RFID() - - def readCard(self): - # Scan for cards - self.device.wait_for_tag() - (error, tag_type) = self.device.request() - - if not error: - logger.info("Card detected.") - # Perform anti-collision detection to find card uid - (error, uid) = self.device.anticoll() - if not error: - card_id = ''.join((str(x) for x in uid)) - logger.info(card_id) - return card_id - logger.debug("No Device ID found.") - return None - - @staticmethod - def cleanup(): - GPIO.cleanup() - - -class Rdm6300Reader: - def __init__(self): - device = '/dev/ttyS0' - baudrate = 9600 - ser_timeout = 0.1 - self.last_card_id = '' - try: - self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) - except serial.SerialException as e: - logger.error(e) - exit(1) - - def readCard(self): - byte_card_id = b'' - - try: - while True: - try: - read_byte = self.rfid_serial.read() - - if read_byte == b'\x02': # start byte - while read_byte != b'\x03': # end bye - read_byte = self.rfid_serial.read() - byte_card_id += read_byte - - card_id = byte_card_id.decode('utf-8') - byte_card_id = '' - card_id = ''.join(x for x in card_id if x in string.printable) - - # Only return UUIDs with correct length - if len(card_id) == 12 and card_id != self.last_card_id: - self.last_card_id = card_id - self.rfid_serial.reset_input_buffer() - return self.last_card_id - - else: # wrong UUID length or already send that UUID last time - self.rfid_serial.reset_input_buffer() - - except ValueError as ve: - logger.errror(ve) - - except serial.SerialException as se: - logger.error(se) - - def cleanup(self): - self.rfid_serial.close() - - -class Pn532Reader: - def __init__(self): - pn532 = Pn532_i2c() - self.device = Mifare() - self.device.SAMconfigure() - self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY) - - def readCard(self): - return str(+int('0x' + self.device.scan_field().hex(), 0)) - - def cleanup(self): - # Not sure if something needs to be done here. - logger.debug("PN532Reader clean up.") - - -class Reader(object): - def __init__(self): - path = os.path.dirname(os.path.realpath(__file__)) - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - device_name = f.read() - - if device_name == 'MFRC522': - self.reader = Mfrc522Reader() - elif device_name == 'RDM6300': - self.reader = Rdm6300Reader() - elif device_name == 'PN532': - self.reader = Pn532Reader() - else: - try: - device = [device for device in get_devices() if device.name == device_name][0] - self.reader = UsbReader(device) - except IndexError: - sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) diff --git a/scripts/phonie_access_objects.py b/scripts/phonie_access_objects.py new file mode 100644 index 000000000..c3c34e056 --- /dev/null +++ b/scripts/phonie_access_objects.py @@ -0,0 +1,54 @@ +import zmq +import json + +class phoniebox_object_access_queue: + + def __init__(self): + #self.objects = objects + self.context = None + + def connect(self,addr= None): + if addr == None: + addr = "tcp://127.0.0.1:5555" + self.context = zmq.Context() + self.queue = self.context.socket(zmq.REQ) + self.queue.setsockopt(zmq.RCVTIMEO,200) + self.queue.setsockopt(zmq.LINGER, 200) + self.queue.connect(addr) + + def phonie_enqueue(self, request): + #todo check reqest + print (request) + self.queue.send_string(json.dumps(request)) + + print ("send:", request) + + try: + server_response = self.queue.recv() + except: + print ("somethng went wrong") + server_response = None + + return server_response + + +if __name__ == "__main__": + import time + test_objects = [{'obj':'volume','cmd':'get','param':None}, + {'obj':'volume','cmd':'set','param':{'volume':30}}, + {'obj':'volume','cmd':'set','param':{'volume':33}}, + {'obj':'volume','cmd':'set','param':{'volume':36}}] + + print ("Test Phonibox Object Acces Client") + queue = phoniebox_object_access_queue() + print ("connect") + queue.connect() + + print ("test") + for req in test_objects: + #print (req) + resp = queue.phonie_enqueue(req) + print (resp) + time.sleep(0.5) + + diff --git a/scripts/playout_controls.sh b/scripts/playout_controls.sh index 11517a065..483f1410f 100755 --- a/scripts/playout_controls.sh +++ b/scripts/playout_controls.sh @@ -10,6 +10,9 @@ # Set the date and time of now NOW=`date +%Y-%m-%d.%H:%M:%S` +mtime=`date +%s` + + # USAGE EXAMPLES: # # shutdown RPi: @@ -574,6 +577,7 @@ case $COMMAND in echo ${AUDIOVOLSTARTUP} ;; setvolumetostartup) + echo "setvolumetostartup $(expr `date +%s` - $mtime) s" if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi # check if startup-volume is disabled if [ "${AUDIOVOLSTARTUP}" == 0 ]; then @@ -582,13 +586,15 @@ case $COMMAND in # set volume level in percent if [ "${VOLUMEMANAGER}" == "amixer" ]; then # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' ${AUDIOVOLSTARTUP}% + amixer -q sset \'$AUDIOIFACENAME\' ${AUDIOVOLSTARTUP}% + echo "after amixer setvolumetostartup $(expr `date +%s` - $mtime) s" else # manage volume with mpd echo -e setvol ${AUDIOVOLSTARTUP}\\nclose | nc -w 1 localhost 6600 fi fi + echo "after setvolumetostartup $(expr `date +%s` - $mtime) s" ;; playerstop) # stop the player diff --git a/scripts/startup-scripts.sh b/scripts/startup-scripts.sh index 503c165cf..6de44f52b 100755 --- a/scripts/startup-scripts.sh +++ b/scripts/startup-scripts.sh @@ -1,5 +1,7 @@ #!/bin/bash +start_time=`date +%s` + # The absolute path to the folder whjch contains all the scripts. # Unless you are working with symlinks, leave the following line untouched. PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -12,43 +14,52 @@ if [ ! -f $PATHDATA/../settings/global.conf ]; then fi . $PATHDATA/../settings/global.conf ########################################################### -echo "Phoniebox is starting..." +echo "Phoniebox is starting...$(expr `date +%s` - $start_time) s" -cat $PATHDATA/../settings/version-number +#cat $PATHDATA/../settings/version-number -cat $PATHDATA/../settings/global.conf +#cat $PATHDATA/../settings/global.conf -echo "${AUDIOVOLSTARTUP} is the mpd startup volume" +#echo "${AUDIOVOLSTARTUP} is the mpd startup volume $(expr `date +%s` - $start_time) s" #################################### # make playists, files and folders # and shortcuts # readable and writable to all -sudo chmod -R 777 ${AUDIOFOLDERSPATH} -sudo chmod -R 777 ${PLAYLISTSFOLDERPATH} -sudo chmod -R 777 $PATHDATA/../shared/shortcuts +#sudo chmod -R 777 ${AUDIOFOLDERSPATH} +#sudo chmod -R 777 ${PLAYLISTSFOLDERPATH} +#sudo chmod -R 777 $PATHDATA/../shared/shortcuts + + +#echo "before mpd status $(expr `date +%s` - $start_time) s" ######################################### # wait until mopidy/MPD server is running STATUS=0 -while [ "$STATUS" != "ACTIVE" ]; do STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep 'OK MPD'| sed 's/^.*$/ACTIVE/'); done +while [ "$STATUS" != "ACTIVE" ]; do STATUS=$(echo -e status\\nclose | nc -w 0 localhost 6600 | grep 'OK MPD'| sed 's/^.*$/ACTIVE/'); done + + +#echo "before playout $(expr `date +%s` - $start_time) s" #################################### # check if and set volume on startup -/home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=setvolumetostartup +#/home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=setvolumetostartup + + +#echo "after vol $(expr `date +%s` - $start_time) s" #################### # play startup sound -mpgvolume=$((32768*${AUDIOVOLSTARTUP}/100)) -echo "${mpgvolume} is the mpg123 startup volume" -/usr/bin/mpg123 -f -${mpgvolume} /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 +#mpgvolume=$((32768*${AUDIOVOLSTARTUP}/100)) +#echo "${mpgvolume} is the mpg123 startup volume" +#/usr/bin/mpg123 -f -${mpgvolume} /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 ####################### # re-scan music library -mpc rescan +#mpc rescan ####################### # read out wifi config? -if [ "${READWLANIPYN}" == "ON" ]; then - /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=readwifiipoverspeaker -fi +#if [ "${READWLANIPYN}" == "ON" ]; then +# /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=readwifiipoverspeaker +#fi diff --git a/scripts/startup_sound.sh b/scripts/startup_sound.sh new file mode 100755 index 000000000..3ca5da787 --- /dev/null +++ b/scripts/startup_sound.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +#sleep 1.5 + +#################### +# play startup sound +mpgvolume=$((32768*50/100)) +echo "${mpgvolume} is the mpg123 startup volume" +/usr/bin/mpg123 -f -${mpgvolume} /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 + +####################### diff --git a/scripts/test_zmq.py b/scripts/test_zmq.py new file mode 100644 index 000000000..c79df0614 --- /dev/null +++ b/scripts/test_zmq.py @@ -0,0 +1,160 @@ +import nanotime +import zmq +import json +import time + +import alsaaudio +from mpd import MPDClient + +class player_control: + def __init__(self): + self.mpd_client = MPDClient() # create client object + self.mpd_client.timeout = 0.5 # network timeout in seconds (floats allowed), default: None + self.mpd_client.idletimeout = 0.5 # timeout for fetching the result of the idle command is handled seperately, default: None + #self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 + self.connect() + print("Connected to MPD Version: "+self.mpd_client.mpd_version) + + def connect(self): + self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 + + def get_player_type_and_version(self, param): + return ({'tpye':'mpd','version':self.mpd_client.mpd_version}) + + def play(self, param): + try: + self.mpd_client.play() + except ConnectionError: + print ("MPD Connection Error, retry") + self.conncet() + self.mpd_client.play() + except Exception as e: + print(e) + song = self.mpd_client.currentsong() + return ({'song':song}) + + def get_current_song(self, param): + song = self.mpd_client.currentsong() + #resp = {'resp': self.mpd_client.currentsong()} + return song + +class volume_control_mpd: + def __init__(self): + print ("not yet implemented\n") + +class volume_control_alsa: + def __init__(self): + self.mixer = alsaaudio.Mixer('PCM', 0) + self.volume = 0 + #self.mixer.getvolume() + + def get(self, param): + return ({'volume':self.volume}) + + def set(self, param): + volume = param.get('volume') + if isinstance(volume, int): + if (volume < 0): volume = 0; + if (volume > 100): volume = 100; + self.volume = volume + self.mixer.setvolume(self.volume) + else: + volume = -1 + return ({'volume':volume}) + + def inc(self, param): + volume = self.volume +3 + if (volume > 100): volume = 100 + self.volume = volume + self.mixer.setvolume(self.volume) + return ({'volume':self.volume}) + + def dec(self, param): + volume = self.volume -3 + if (volume < 0): volume = 0 + self.volume = volume + self.mixer.setvolume(self.volume) + return ({'volume':self.volume}) + +class phoniebox_control: + + def __init__(self,objects): + self.objects = objects + self.context = None + + def connect(self,addr= None): + if addr == None: + addr = "tcp://127.0.0.1:5555" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(addr) + self.socket.setsockopt(zmq.LINGER, 200) + + def run(self, obj, cmd, param): + run_obj = self.objects.get(obj) + + if (run_obj is not None): + run_func = getattr(run_obj,cmd,None) + if (run_func is not None): # is callable() ?? + resp = run_func(param) + print (resp) + else: + resp = {'resp': "no valid commad"} + print (resp) + else: + resp = {'resp': "no valid obj"} + print (resp) + return resp + + def process_queue(self): + #while True: + # Wait for next request from client + message = self.socket.recv() + nt = nanotime.now().nanoseconds() + + client_request=json.loads(message) + client_response = {} + + print (client_request) + + client_object = client_request.get('obj') + if (client_object != None): + client_command = client_request.get('cmd') + if (client_command != None): + client_param = client_request.get('param') + client_response['resp'] = self.run(client_object,client_command,client_param) + + client_tsp = client_request.get('tsp') + if (client_tsp != None): + client_response['total_processing_time'] = (nt - int(client_request['tsp'])) / 1000000 + print ("processing time: {:2.3f} ms".format(client_response['total_processing_time'])) + + print(client_response) + # Send reply back to client + self.socket.send_string(json.dumps(client_response)) + + return (1) + + +#def get(self): +# def func_not_found(): # just in case we dont have the function +# print 'No Function '+self.i+' Found!' +# func_name = 'function' + self.i +# func = getattr(self,func_name,func_not_found) +# func() # <-- this should work! + + +if __name__ == "__main__": + #initialize objcts + objects = {'volume':volume_control_alsa(), + 'player':player_control()} + + print ("Start Phonibox Control") + pc = phoniebox_control(objects) + pc.connect() + + print ("Start loop") + ret_ok = 1 + while (ret_ok): + ret_ok = pc.process_queue() + \ No newline at end of file diff --git a/shared/audiofolders/placeholder b/shared/audiofolders/placeholder deleted file mode 100755 index e69de29bb..000000000 From 6bb27511705d57c68c5d16b58f0bb9bd1e6898a4 Mon Sep 17 00:00:00 2001 From: arne Date: Mon, 15 Feb 2021 23:53:38 +0100 Subject: [PATCH 002/606] refactored gpio-control to class --- .../gpio_control/GPIODevices/VolumeControl.py | 4 +- components/gpio_control/function_calls.py | 118 +++++----- components/gpio_control/gpio_control.py | 214 +++++++++--------- 3 files changed, 169 insertions(+), 167 deletions(-) diff --git a/components/gpio_control/GPIODevices/VolumeControl.py b/components/gpio_control/GPIODevices/VolumeControl.py index af40b0395..53f5be950 100644 --- a/components/gpio_control/GPIODevices/VolumeControl.py +++ b/components/gpio_control/GPIODevices/VolumeControl.py @@ -1,9 +1,9 @@ from GPIODevices import TwoButtonControl, RotaryEncoder -from gpio_control import logger, getFunctionCall +#from gpio_control import logger, getFunctionCall class VolumeControl: - def __new__(self, config): + def __new__(self, config,getFunctionCall,logger): if config.get('Type') == 'TwoButtonControl': logger.info('VolumeControl as TwoButtonControl') return TwoButtonControl( diff --git a/components/gpio_control/function_calls.py b/components/gpio_control/function_calls.py index f1ff7eacc..735962c23 100644 --- a/components/gpio_control/function_calls.py +++ b/components/gpio_control/function_calls.py @@ -5,106 +5,104 @@ import pathlib - currentdir = os.path.dirname(os.path.realpath(__file__)) parentdir = os.path.dirname(currentdir) parentdir = os.path.dirname(parentdir) print (parentdir) - sys.path.append(parentdir+"/scripts") - print(sys.path) from phonie_access_objects import phoniebox_object_access_queue +class phoniebox_function_calls: + def __init__(self): + self.logger = logging.getLogger(__name__) + + playout_control_relative_path = "../../scripts/playout_controls.sh" + function_calls_absolute_path = str(pathlib.Path(__file__).parent.absolute()) + self.playout_control = os.path.abspath(os.path.join(function_calls_absolute_path, playout_control_relative_path)) -logger = logging.getLogger(__name__) + def functionCallShutdown(self,*args): + function_call("{command} -c=shutdown".format(command=self.playout_control), shell=True) -playout_control_relative_path = "../../scripts/playout_controls.sh" -function_calls_absolute_path = str(pathlib.Path(__file__).parent.absolute()) -playout_control = os.path.abspath(os.path.join(function_calls_absolute_path, playout_control_relative_path)) -def functionCallShutdown(*args): - function_call("{command} -c=shutdown".format(command=playout_control), shell=True) + def functionCallVolU(self,steps=None): + queue = phoniebox_object_access_queue() + queue.connect() + resp = queue.phonie_enqueue({'obj':'volume','cmd':'inc','param':None}) + #if steps is None: + # function_call("{command} -c=volumeup".format(command=self.playout_control), shell=True) + #else: + # function_call("{command} -c=volumeup -v={steps}".format(steps=steps, + # command=self.playout_control), + # shell=True) -def functionCallVolU(steps=None): - queue = phoniebox_object_access_queue() - queue.connect() - resp = queue.phonie_enqueue({'obj':'volume','cmd':'inc','param':None}) - - #if steps is None: - # function_call("{command} -c=volumeup".format(command=playout_control), shell=True) - #else: - # function_call("{command} -c=volumeup -v={steps}".format(steps=steps, - # command=playout_control), - # shell=True) + def functionCallVolD(self,steps=None): + queue = phoniebox_object_access_queue() + queue.connect() + resp = queue.phonie_enqueue({'obj':'volume','cmd':'dec','param':None}) -def functionCallVolD(steps=None): - queue = phoniebox_object_access_queue() - queue.connect() - resp = queue.phonie_enqueue({'obj':'volume','cmd':'dec','param':None}) - - #if steps is None: - # function_call("{command} -c=volumedown".format(command=playout_control), shell=True) - #else: - # function_call("{command} -c=volumedown -v={steps}".format(steps=steps, - # command=playout_control), - # shell=True) + #if steps is None: + # function_call("{command} -c=volumedown".format(command=self.playout_control), shell=True) + #else: + # function_call("{command} -c=volumedown -v={steps}".format(steps=steps, + # command=self.playout_control), + # shell=True) -def functionCallVol0(*args): - function_call("{command} -c=mute".format(command=playout_control), shell=True) + def functionCallVol0(self,*args): + function_call("{command} -c=mute".format(command=self.playout_control), shell=True) -def functionCallPlayerNext(*args): - function_call("{command} -c=playernext".format(command=playout_control), shell=True) + def functionCallPlayerNext(self,*args): + function_call("{command} -c=playernext".format(command=self.playout_control), shell=True) -def functionCallPlayerPrev(*args): - function_call("{command} -c=playerprev".format(command=playout_control), shell=True) + def functionCallPlayerPrev(self,*args): + function_call("{command} -c=playerprev".format(command=self.playout_control), shell=True) -def functionCallPlayerPauseForce(*args): - function_call("{command} -c=playerpauseforce".format(command=playout_control), shell=True) + def functionCallPlayerPauseForce(self,*args): + function_call("{command} -c=playerpauseforce".format(command=self.playout_control), shell=True) -def functionCallPlayerPause(*args): - function_call("{command} -c=playerpause".format(command=playout_control), shell=True) + def functionCallPlayerPause(self,*args): + function_call("{command} -c=playerpause".format(command=self.playout_control), shell=True) -def functionCallRecordStart(*args): - function_call("{command} -c=recordstart".format(command=playout_control), shell=True) + def functionCallRecordStart(self,*args): + function_call("{command} -c=recordstart".format(command=self.playout_control), shell=True) -def functionCallRecordStop(*args): - function_call("{command} -c=recordstop".format(command=playout_control), shell=True) + def functionCallRecordStop(self,*args): + function_call("{command} -c=recordstop".format(command=self.playout_control), shell=True) -def functionCallRecordPlayLatest(*args): - function_call("{command} -c=recordplaylatest".format(command=playout_control), shell=True) + def functionCallRecordPlayLatest(self,*args): + function_call("{command} -c=recordplaylatest".format(command=self.playout_control), shell=True) -def functionCallToggleWifi(*args): - function_call("{command} -c=togglewifi".format(command=playout_control), shell=True) + def functionCallToggleWifi(self,*args): + function_call("{command} -c=togglewifi".format(command=self.playout_control), shell=True) -def functionCallPlayerStop(*args): - function_call("{command} -c=playerstop".format(command=playout_control), - shell=True) + def functionCallPlayerStop(self,*args): + function_call("{command} -c=playerstop".format(command=self.playout_control), + shell=True) -def functionCallPlayerSeekFwd(*args): - function_call("{command} -c=playerseek -v=+10".format(command=playout_control), shell=True) + def functionCallPlayerSeekFwd(self,*args): + function_call("{command} -c=playerseek -v=+10".format(command=self.playout_control), shell=True) -def functionCallPlayerSeekBack(*args): - function_call("{command} -c=playerseek -v=-10".format(command=playout_control), shell=True) + def functionCallPlayerSeekBack(self,*args): + function_call("{command} -c=playerseek -v=-10".format(command=self.playout_control), shell=True) -def getFunctionCall(functionName): - logger.error('Get FunctionCall: {} {}'.format(functionName, functionName in locals())) - getattr(sys.modules[__name__], str) - return locals().get(functionName, None) + def getFunctionCall(self,functionName): + self.logger.error('Get FunctionCall: {} {}'.format(functionName, functionName in locals())) + getattr(sys.modules[__name__], str) + return locals().get(functionName, None) diff --git a/components/gpio_control/gpio_control.py b/components/gpio_control/gpio_control.py index 7164f0356..8235f263d 100755 --- a/components/gpio_control/gpio_control.py +++ b/components/gpio_control/gpio_control.py @@ -6,120 +6,124 @@ from GPIODevices import * import function_calls from signal import pause - from RPi import GPIO - # from GPIODevices.VolumeControl import VolumeControl # from GPIODevices.led import LED, MPDStatusLED -GPIO.setmode(GPIO.BCM) - -logger = logging.getLogger(__name__) - - -def getFunctionCall(function_name): - try: - if function_name != 'None': - return getattr(function_calls, function_name) - except AttributeError: - logger.error('Could not find FunctionCall {function_name}'.format(function_name=function_name)) - return lambda *args: None - - -def generate_device(config, deviceName): - print(deviceName) - device_type = config.get('Type') - if deviceName.lower() == 'VolumeControl'.lower(): - return VolumeControl(config) - elif device_type == 'TwoButtonControl': - logger.info('adding TwoButtonControl') - return TwoButtonControl( - config.getint('Pin1'), - config.getint('Pin2'), - getFunctionCall(config.get('functionCall1')), - getFunctionCall(config.get('functionCall2')), - functionCallTwoBtns=getFunctionCall(config.get('functionCallTwoButtons')), - pull_up=config.getboolean('pull_up', fallback=True), - hold_repeat=config.getboolean('hold_repeat', False), - hold_time=config.getfloat('hold_time', fallback=0.3), - name=deviceName) - elif device_type in ('Button', 'SimpleButton'): - return SimpleButton(config.getint('Pin'), - action=getFunctionCall(config.get('functionCall')), - name=deviceName, - bouncetime=config.getint('bouncetime', fallback=500), - edge=config.get('edge', fallback='FALLING'), - hold_repeat=config.getboolean('hold_repeat', False), - hold_time=config.getfloat('hold_time', fallback=0.3), - pull_up_down=config.get('pull_up_down', fallback=GPIO.PUD_UP)) - elif device_type == 'LED': - return LED(config.getint('Pin'), - name=deviceName, - initial_value=config.getboolean('initial_value', fallback=True)) - elif device_type == 'MPDStatusLED': - return MPDStatusLED(config.getint('Pin'), - host=config.get('host', fallback='localhost'), - port=config.getint('port', fallback=6600), - name=deviceName - ) - elif device_type == 'RotaryEncoder': - return RotaryEncoder(config.getint('pinUp'), - config.getint('pinDown'), - getFunctionCall(config.get('functionCallUp')), - getFunctionCall(config.get('functionCallDown')), - config.getfloat('timeBase', fallback=0.1), +class gpio_control(): + + def __init__(self,function_calls): + self.devices = [] + self.function_calls = function_calls + + GPIO.setmode(GPIO.BCM) + + lf = '%(asctime)s %(message)s' + lp = '/home/pi/RPi-Jukebox-RFID/logs/gpio.log' + logging.basicConfig(filename=lp, level=logging.DEBUG,format=lf) + #logging.basicConfig(level=logging.INFO,format=lf) + self.logger = logging.getLogger(__name__) + self.logger.setLevel('INFO') + self.logger.info('GPIO Started') + + def getFunctionCall(self,function_name): + try: + if function_name != 'None': + return getattr(self.function_calls, function_name) + except AttributeError: + self.logger.error('Could not find FunctionCall {function_name}'.format(function_name=function_name)) + return lambda *args: None + + def generate_device(self,config, deviceName): + print(deviceName) + device_type = config.get('Type') + if deviceName.lower() == 'VolumeControl'.lower(): + return VolumeControl(config,self.getFunctionCall,logger) + elif device_type == 'TwoButtonControl': + self.logger.info('adding TwoButtonControl') + return TwoButtonControl( + config.getint('Pin1'), + config.getint('Pin2'), + self.getFunctionCall(config.get('functionCall1')), + self.getFunctionCall(config.get('functionCall2')), + functionCallTwoBtns=getFunctionCall(config.get('functionCallTwoButtons')), + pull_up=config.getboolean('pull_up', fallback=True), + hold_repeat=config.getboolean('hold_repeat', False), + hold_time=config.getfloat('hold_time', fallback=0.3), name=deviceName) - elif device_type == 'ShutdownButton': - return ShutdownButton(pin=config.getint('Pin'), - action=getFunctionCall(config.get('functionCall',fallback='functionCallShutdown')), - name=deviceName, - bouncetime=config.getint('bouncetime', fallback=500), - edge=config.get('edge', fallback='FALLING'), - hold_repeat=config.getboolean('hold_repeat', False), - hold_time=config.getfloat('hold_time', fallback=0.3), - pull_up_down=config.get('pull_up_down', fallback=GPIO.PUD_UP)) - logger.warning('cannot find {}'.format(deviceName)) - return None - - -def get_all_devices(config): - devices = [] - logger.info(config.sections()) - for section in config.sections(): - if config.getboolean(section, 'enabled', fallback=False): - logger.info('adding GPIO-Device, {}'.format(section)) - device = generate_device(config[section], section) - if device is not None: - devices.append(device) + elif device_type in ('Button', 'SimpleButton'): + return SimpleButton(config.getint('Pin'), + action=self.getFunctionCall(config.get('functionCall')), + name=deviceName, + bouncetime=config.getint('bouncetime', fallback=500), + edge=config.get('edge', fallback='FALLING'), + hold_repeat=config.getboolean('hold_repeat', False), + hold_time=config.getfloat('hold_time', fallback=0.3), + pull_up_down=config.get('pull_up_down', fallback=GPIO.PUD_UP)) + elif device_type == 'LED': + return LED(config.getint('Pin'), + name=deviceName, + initial_value=config.getboolean('initial_value', fallback=True)) + elif device_type == 'MPDStatusLED': + return MPDStatusLED(config.getint('Pin'), + host=config.get('host', fallback='localhost'), + port=config.getint('port', fallback=6600), + name=deviceName + ) + elif device_type == 'RotaryEncoder': + return RotaryEncoder(config.getint('pinUp'), + config.getint('pinDown'), + self.getFunctionCall(config.get('functionCallUp')), + self.getFunctionCall(config.get('functionCallDown')), + config.getfloat('timeBase', fallback=0.1), + name=deviceName) + elif device_type == 'ShutdownButton': + return ShutdownButton(pin=config.getint('Pin'), + action=self.getFunctionCall(config.get('functionCall',fallback='functionCallShutdown')), + name=deviceName, + bouncetime=config.getint('bouncetime', fallback=500), + edge=config.get('edge', fallback='FALLING'), + hold_repeat=config.getboolean('hold_repeat', False), + hold_time=config.getfloat('hold_time', fallback=0.3), + pull_up_down=config.get('pull_up_down', fallback=GPIO.PUD_UP)) + self.logger.warning('cannot find {}'.format(deviceName)) + return None + + def get_all_devices(self,config): + self.logger.info(config.sections()) + for section in config.sections(): + if config.getboolean(section, 'enabled', fallback=False): + self.logger.info('adding GPIO-Device, {}'.format(section)) + device = self.generate_device(config[section], section) + if device is not None: + self.devices.append(device) + else: + self.logger.warning('Could not add Device {} with {}'.format(section, config.items(section))) else: - logger.warning('Could not add Device {} with {}'.format(section, config.items(section))) - else: - logger.info('Device {} not enabled'.format(section)) - for dev in devices: - print(dev) - return devices - + self.logger.info('Device {} not enabled'.format(section)) + return self.devices + + def print_all_devices(self): + for dev in self.devices: + print(dev) + + def gpio_loop(self): + self.logger.info('Ready for taking actions') + try: + pause() + except KeyboardInterrupt: + pass + self.logger.info('Exiting GPIO Control') + if __name__ == "__main__": - - lf = '%(asctime)s %(message)s' - lp = '/home/pi/RPi-Jukebox-RFID/logs/gpio.log' - #logging.basicConfig(filename=lp, level=logging.DEBUG,format=lf) - logging.basicConfig(level=logging.INFO,format=lf) - logger = logging.getLogger() - #logger.setLevel('INFO') - - logger.info('GPIO Started') - config = configparser.ConfigParser(inline_comment_prefixes=";") config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') config.read(config_path) - devices = get_all_devices(config) - print(devices) - logger.info('Ready for taking actions') - try: - pause() - except KeyboardInterrupt: - pass - logger.info('Exiting GPIO Control') + phoniebox_function_calls = function_calls.phoniebox_function_calls() + gpio_controler = gpio_control(phoniebox_function_calls) + + devices = gpio_controler.get_all_devices(config) + gpio_controler.print_all_devices() + gpio_controler.gpio_loop() \ No newline at end of file From 53695ca95ad77e8cbfd53d389caa7f0a6f8d677e Mon Sep 17 00:00:00 2001 From: arne Date: Sat, 20 Feb 2021 00:04:37 +0100 Subject: [PATCH 003/606] RDM6300 cheksum + number formats --- scripts/Reader.py | 105 ++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/scripts/Reader.py b/scripts/Reader.py index 1db4af167..45373bed7 100755 --- a/scripts/Reader.py +++ b/scripts/Reader.py @@ -7,23 +7,14 @@ import os.path import sys -import serial -import string + import RPi.GPIO as GPIO import logging from evdev import InputDevice, categorize, ecodes, list_devices -# Workaround: when using RC522 reader with pirc522 pkg the py532lib pkg may not be installed and vice-versa -try: - import pirc522 - from py532lib.i2c import * - from py532lib.mifare import * -except ImportError: - pass logger = logging.getLogger(__name__) - def get_devices(): devices = [InputDevice(fn) for fn in list_devices()] devices.append(NonUsbDevice('MFRC522')) @@ -59,6 +50,7 @@ def readCard(self): class Mfrc522Reader(object): def __init__(self): + import pirc522 self.device = pirc522.RFID() def readCard(self): @@ -81,49 +73,88 @@ def readCard(self): def cleanup(): GPIO.cleanup() - class Rdm6300Reader: - def __init__(self): + def __init__(self,param = None): + import serial device = '/dev/ttyS0' baudrate = 9600 ser_timeout = 0.1 self.last_card_id = '' try: self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) + self.serial_SerialException = serial.SerialException except serial.SerialException as e: logger.error(e) exit(1) + + self.number_format = '' + if param is not None: + nf = param.get("numberformat") + if nf is not None: + self.number_format = nf + + def convert_to_weigand26_when_checksum_ok(self,raw_card_id): + weigand26 = [] + xor = 0 + for i in range(0, len(raw_card_id)>>1): + val = int(raw_card_id[i*2:i*2+2],16) + if (i < 5): + xor = xor ^ val + weigand26.append(val) + else: + chk = val + if (chk == val): + return weigand26 + else: + return None def readCard(self): - byte_card_id = b'' + byte_card_id = bytearray() try: while True: try: - read_byte = self.rfid_serial.read() - - if read_byte == b'\x02': # start byte - while read_byte != b'\x03': # end bye - read_byte = self.rfid_serial.read() - byte_card_id += read_byte - - card_id = byte_card_id.decode('utf-8') - byte_card_id = '' - card_id = ''.join(x for x in card_id if x in string.printable) - - # Only return UUIDs with correct length - if len(card_id) == 12 and card_id != self.last_card_id: - self.last_card_id = card_id - self.rfid_serial.reset_input_buffer() - return self.last_card_id - - else: # wrong UUID length or already send that UUID last time - self.rfid_serial.reset_input_buffer() + wait_for_start_byte = True + while True: + read_byte = self.rfid_serial.read() + + if (wait_for_start_byte): + if read_byte == b'\x02': + wait_for_start_byte = False + else: + if read_byte != b'\x03': #could stuck here, check len? check timeout by len == 0?? + byte_card_id.extend(read_byte) + else: + wait_for_start_byte = True + break + + raw_card_id = byte_card_id.decode('ascii') + byte_card_id.clear() + self.rfid_serial.reset_input_buffer() + + if len(raw_card_id) == 12 : + w26 = self.convert_to_weigand26_when_checksum_ok(raw_card_id) + if (w26 is not None): + #print ("factory code is ignored" ,w26[0]) + + if self.number_format == 'card_id_dec': + #this will return a 10 Digit card ID e.g. 0006762840 + card_id = '{0:010d}'.format( (w26[1] << 24) + (w26[2] << 16) + (w26[3] << 8) + w26[4]) + elif self.number_format == 'card_id_float': + #this will return a fractional card ID e.g. 103,12632 + card_id='{0:d},{1:05d}'.format( ((w26[1] << 8) + w26[2]) , ((w26[3] << 8) + w26[4])) + else: + #this will return the raw (original) card ID e.g. 070067315809 + card_id = raw_card_id + + if card_id != self.last_card_id: + self.last_card_id = card_id + return self.last_card_id except ValueError as ve: - logger.errror(ve) + logger.error(ve) - except serial.SerialException as se: + except self.serial_SerialException as se: logger.error(se) def cleanup(self): @@ -132,6 +163,8 @@ def cleanup(self): class Pn532Reader: def __init__(self): + from py532lib.i2c import Pn532_i2c + from py532lib.mifare import Mifare pn532 = Pn532_i2c() self.device = Mifare() self.device.SAMconfigure() @@ -144,7 +177,6 @@ def cleanup(self): # Not sure if something needs to be done here. logger.debug("PN532Reader clean up.") - class Reader(object): def __init__(self): path = os.path.dirname(os.path.realpath(__file__)) @@ -157,7 +189,8 @@ def __init__(self): if device_name == 'MFRC522': self.reader = Mfrc522Reader() elif device_name == 'RDM6300': - self.reader = Rdm6300Reader() + self.reader = Rdm6300Reader({'numberformat':'card_id_float'}) + #self.reader = Rdm6300Reader() elif device_name == 'PN532': self.reader = Pn532Reader() else: From ea1539a34fe9f0f9c18082b39eab0e819c7ff3dd Mon Sep 17 00:00:00 2001 From: arne123 Date: Mon, 22 Feb 2021 09:51:04 +0100 Subject: [PATCH 004/606] inital class concept for phonibox - initial commit, zeromq based box RPC Server listening right now# --- components/gpio_control/gpio_control.py | 10 + components/rfid-reader/PhonieboxRfidReader.py | 200 ++++++++++++++++ components/rfid-reader/RfidReader_PN532.py | 25 ++ components/rfid-reader/RfidReader_RC522.py | 33 +++ components/rfid-reader/RfidReader_RDM6300.py | 93 ++++++++ components/rfid-reader/__init__.py | 0 scripts/Reader.py | 1 - scripts/python-phoniebox/PhonieboxDaemon.py | 217 +++++++++++------- .../python-phoniebox/PhonieboxNvManager.py | 35 +++ scripts/python-phoniebox/PhonieboxPlayer.py | 101 ++++++++ .../python-phoniebox/PhonieboxRpcServer.py | 83 +++++++ scripts/python-phoniebox/PhonieboxVolume.py | 51 ++++ .../python-phoniebox/PhonieboxZmqServer.py | 81 +++++++ 13 files changed, 843 insertions(+), 87 deletions(-) create mode 100644 components/rfid-reader/PhonieboxRfidReader.py create mode 100644 components/rfid-reader/RfidReader_PN532.py create mode 100644 components/rfid-reader/RfidReader_RC522.py create mode 100644 components/rfid-reader/RfidReader_RDM6300.py create mode 100644 components/rfid-reader/__init__.py create mode 100644 scripts/python-phoniebox/PhonieboxNvManager.py create mode 100644 scripts/python-phoniebox/PhonieboxPlayer.py create mode 100644 scripts/python-phoniebox/PhonieboxRpcServer.py create mode 100644 scripts/python-phoniebox/PhonieboxVolume.py create mode 100644 scripts/python-phoniebox/PhonieboxZmqServer.py diff --git a/components/gpio_control/gpio_control.py b/components/gpio_control/gpio_control.py index 8235f263d..b14234e35 100755 --- a/components/gpio_control/gpio_control.py +++ b/components/gpio_control/gpio_control.py @@ -25,6 +25,7 @@ def __init__(self,function_calls): self.logger = logging.getLogger(__name__) self.logger.setLevel('INFO') self.logger.info('GPIO Started') + self._keep_running = 1 def getFunctionCall(self,function_name): try: @@ -107,6 +108,15 @@ def print_all_devices(self): for dev in self.devices: print(dev) + def gpio_run(self): + self._keep_running = 1 + while self._keep_running: + time.sleep(1) + print ("Exiting GPIO") + + def gpio_terminate(self): + self._keep_running = 0 + def gpio_loop(self): self.logger.info('Ready for taking actions') try: diff --git a/components/rfid-reader/PhonieboxRfidReader.py b/components/rfid-reader/PhonieboxRfidReader.py new file mode 100644 index 000000000..04fcad442 --- /dev/null +++ b/components/rfid-reader/PhonieboxRfidReader.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# This alternative Reader.py script was meant to cover not only USB readers but more. +# It can be used to replace Reader.py if you have readers such as +# MFRC522, RDM6300 or PN532. +# Please use the github issue threads to share bugs and improvements +# or create pull requests. + +import os.path +import sys + +import RPi.GPIO as GPIO +import logging + +from evdev import InputDevice, categorize, ecodes, list_devices + +logger = logging.getLogger(__name__) + +def get_devices(): + devices = [InputDevice(fn) for fn in list_devices()] + devices.append(NonUsbDevice('MFRC522')) + devices.append(NonUsbDevice('RDM6300')) + devices.append(NonUsbDevice('PN532')) + return devices + + +class NonUsbDevice(object): + name = None + + def __init__(self, name): + self.name = name + + +class UsbReader(object): + def __init__(self, device): + self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" + self.dev = device + + def readCard(self): + from select import select + stri = '' + key = '' + while key != 'KEY_ENTER': + select([self.dev], [], []) + for event in self.dev.read(): + if event.type == 1 and event.value == 1: + stri += self.keys[event.code] + key = ecodes.KEY[event.code] + return stri[:-1] + + +class RFID_Reader(object): + def __init__(self,device_name,param=None): + + if device_name == 'MFRC522': + import RfidReader_RC522 + self.reader = Mfrc522Reader() + elif device_name == 'RDM6300': + import RfidReader_RDM6300 + self.reader = Rdm6300Reader(param) + elif device_name == 'PN532': + import RfidReader_PN532 + self.reader = Pn532Reader() + else: + try: + device = [device for device in get_devices() if device.name == device_name][0] + self.reader = UsbReader(device) + except IndexError: + sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) + + self.queue = phoniebox_object_access_queue() + self.queue.connect() + self._keep_running = True + + + + def set_cardid_db(self,cardid_db): + ##potentially dangerous for runtime updates, needs look? + if cardid_db != None: + self.cardid_db = cardid_db + + def get_card_assignment(self,cardid) + ##potentially dangerous for runtime updates, needs look? + card_assignment = self.cardid_db.get(cardid) + + def get_last_card_id(self): + return self.last_card_id + + def set_cardnotification(self, callback) + if is callable(callback): + self.cardnotification = callback + + def set_valid_cardnotification(self, callback) + if is callable(callback): + self.valid_cardnotification = callback + + + def set_invalid_cardnotification(self, callback) + if is callable(callback): + self.invalid_cardnotification = callback + + + def terminate(self): + self._keep_running = False + + def run(self): + + self._keep_running = True + ##card_detection_sound = self.get_setting("phoniebox", "card_detection_sound") <-- this module should not deciede about sound + debounce_time = self.get_setting("phoniebox", "debounce_time") + if debounce_time == -1: + debounce_time = 0.5 + second_swipe_delay = self.get_setting("phoniebox", "second_swipe_delay") + if second_swipe_delay == -1: + second_swipe_delay = 0 + store_card_assignments = self.get_setting("phoniebox", "store_card_assignments") + if store_card_assignments == -1: + store_card_assignments = 30 + last_swipe = 0 + last_write_card_assignments = 0 + + ## who does know about card ids? + ##the reader? + ## -> reader would know about titles? how much can this be? none will deal with more then100 cards? + ## -> would be ok or? How das webui trigger this? how is mpd triggered, lets look up + ##core? + ## -> core would need to ditribute functions -> bad + ## -> Reader knows about IDs eecuitng a command, everything else is treted -> maybe + + while self._keep_running: #since readCard is a blocking call, this will not work + cardid = reader.reader.readCard() + self.last_card_id = cardid + + if self.cardnotification is not None: + self.cardnotification(cardid) + + card_assignment = self.get_card_assignment(cardid) + + if card_assignment is not None: + + #probably deal with 2nd wipe here + + if self.valid_cardnotification is not None: + self.valid_cardnotification() + #queue = phoniebox_object_access_queue() + #queue.connect() + resp = self.queue.phonie_enqueue(card_assignment) + + #hm, what to do with response here? + + else: + if self.invalid_cardnotification is not None: + self.invalid_cardnotification() + + + #ok, card not in database, + + + + #try: + # start the player script and pass on the cardid + # if cardid is not None: + # print("Card ID: {}".format(int(cardid))) + # filename = self.get_setting("phoniebox", "Latest_RFID_file") + # if filename != -1: + # self.print_to_file(filename, "\'{}\' was used at {}".format(cardid, time())) + # if card_detection_sound != -1: + # self.play_alsa(card_detection_sound) + # if cardid in self.cardAssignments.sections(): + # # second swipe detection + # if int(cardid) == int(self.lastplayedID) and time()-last_swipe > second_swipe_delay: + # self.log("Second swipe for {}".format(cardid), 3) + # self.do_second_swipe() + # # if first swipe, just play + # else: + # last_swipe = time() + # self.do_start_playlist(cardid) + # # do not react for debounce_time + # sleep(debounce_time) + # else: + # self.log("Card with ID {} not mapped yet.".format(cardid), 1) +# + #except OSError as e: + # print("Execution failed:", e) +# + # check if it is time for the next update of the cardAssignments and do it + # Note: this is purely time-based and not clever at all. Find a + # TODO: find a better way to check for changes in the files on disk to trigger the update + ## <- this module should not deceide about card_id updates + #if time()-last_write_card_assignments > store_card_assignments and store_card_assignments != False: + # # store card assignments + # if self.get_setting("phoniebox", "translate_legacy_cardassignments", "bool") == True: + # legacy_cardAssignments = self.translate_legacy_cardAssignments(last_write_card_assignments) + # self.update_cardAssignments(legacy_cardAssignments) + # else: + # self.update_cardAssignments(self.read_cardAssignments) + + # self.write_new_cardAssignments() + # last_write_card_assignments = time() + return 1 + diff --git a/components/rfid-reader/RfidReader_PN532.py b/components/rfid-reader/RfidReader_PN532.py new file mode 100644 index 000000000..658f9568d --- /dev/null +++ b/components/rfid-reader/RfidReader_PN532.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# This alternative Reader.py script was meant to cover not only USB readers but more. +# It can be used to replace Reader.py if you have readers such as +# MFRC522, RDM6300 or PN532. +# Please use the github issue threads to share bugs and improvements +# or create pull requests. +import logging + +logger = logging.getLogger(__name__) + +class Pn532Reader: + def __init__(self): + from py532lib.i2c import Pn532_i2c + from py532lib.mifare import Mifare + pn532 = Pn532_i2c() + self.device = Mifare() + self.device.SAMconfigure() + self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY) + + def readCard(self): + return str(+int('0x' + self.device.scan_field().hex(), 0)) + + def cleanup(self): + # Not sure if something needs to be done here. + logger.debug("PN532Reader clean up.") \ No newline at end of file diff --git a/components/rfid-reader/RfidReader_RC522.py b/components/rfid-reader/RfidReader_RC522.py new file mode 100644 index 000000000..2f4250d93 --- /dev/null +++ b/components/rfid-reader/RfidReader_RC522.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# This alternative Reader.py script was meant to cover not only USB readers but more. +# It can be used to replace Reader.py if you have readers such as +# MFRC522 +import RPi.GPIO as GPIO +import logging + +logger = logging.getLogger(__name__) + +class Mfrc522Reader(object): + def __init__(self): + import pirc522 + self.device = pirc522.RFID() + + def readCard(self): + # Scan for cards + self.device.wait_for_tag() + (error, tag_type) = self.device.request() + + if not error: + logger.info("Card detected.") + # Perform anti-collision detection to find card uid + (error, uid) = self.device.anticoll() + if not error: + card_id = ''.join((str(x) for x in uid)) + logger.info(card_id) + return card_id + logger.debug("No Device ID found.") + return None + + @staticmethod + def cleanup(): + GPIO.cleanup() \ No newline at end of file diff --git a/components/rfid-reader/RfidReader_RDM6300.py b/components/rfid-reader/RfidReader_RDM6300.py new file mode 100644 index 000000000..5e5379b9d --- /dev/null +++ b/components/rfid-reader/RfidReader_RDM6300.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# This alternative Reader.py script was meant to cover not only USB readers but more. +# It can be used to replace Reader.py if you have readers such as +# RDM6300 +import logging + +logger = logging.getLogger(__name__) + +class Rdm6300Reader: + def __init__(self,param = None): + import serial + device = '/dev/ttyS0' + baudrate = 9600 + ser_timeout = 0.1 + self.last_card_id = '' + try: + self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) + self.serial_SerialException = serial.SerialException + except serial.SerialException as e: + logger.error(e) + exit(1) + + self.number_format = '' + if param is not None: + nf = param.get("numberformat") + if nf is not None: + self.number_format = nf + + def convert_to_weigand26_when_checksum_ok(self,raw_card_id): + weigand26 = [] + xor = 0 + for i in range(0, len(raw_card_id)>>1): + val = int(raw_card_id[i*2:i*2+2],16) + if (i < 5): + xor = xor ^ val + weigand26.append(val) + else: + chk = val + if (chk == val): + return weigand26 + else: + return None + + def readCard(self): + byte_card_id = bytearray() + + try: + while True: + try: + wait_for_start_byte = True + while True: + read_byte = self.rfid_serial.read() + + if (wait_for_start_byte): + if read_byte == b'\x02': + wait_for_start_byte = False + else: + if read_byte != b'\x03': #could stuck here, check len? check timeout by len == 0?? + byte_card_id.extend(read_byte) + else: + break + + raw_card_id = byte_card_id.decode('ascii') + byte_card_id.clear() + self.rfid_serial.reset_input_buffer() + + if len(raw_card_id) == 12 : + w26 = self.convert_to_weigand26_when_checksum_ok(raw_card_id) + if (w26 is not None): + #print ("factory code is ignored" ,w26[0]) + + if self.number_format == 'card_id_dec': + #this will return a 10 Digit card ID e.g. 0006762840 + card_id = '{0:010d}'.format( (w26[1] << 24) + (w26[2] << 16) + (w26[3] << 8) + w26[4]) + elif self.number_format == 'card_id_float': + #this will return a fractional card ID e.g. 103,12632 + card_id='{0:d},{1:05d}'.format( ((w26[1] << 8) + w26[2]) , ((w26[3] << 8) + w26[4])) + else: + #this will return the raw (original) card ID e.g. 070067315809 + card_id = raw_card_id + + if card_id != self.last_card_id: + self.last_card_id = card_id + return self.last_card_id + + except ValueError as ve: + logger.error(ve) + + except self.serial_SerialException as se: + logger.error(se) + + def cleanup(self): + self.rfid_serial.close() \ No newline at end of file diff --git a/components/rfid-reader/__init__.py b/components/rfid-reader/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/Reader.py b/scripts/Reader.py index 45373bed7..50dceb057 100755 --- a/scripts/Reader.py +++ b/scripts/Reader.py @@ -125,7 +125,6 @@ def readCard(self): if read_byte != b'\x03': #could stuck here, check len? check timeout by len == 0?? byte_card_id.extend(read_byte) else: - wait_for_start_byte = True break raw_card_id = byte_card_id.decode('ascii') diff --git a/scripts/python-phoniebox/PhonieboxDaemon.py b/scripts/python-phoniebox/PhonieboxDaemon.py index a5739bbe0..8963cf060 100755 --- a/scripts/python-phoniebox/PhonieboxDaemon.py +++ b/scripts/python-phoniebox/PhonieboxDaemon.py @@ -1,12 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# import threading +import threading import sys, os.path import signal from Phoniebox import Phoniebox from time import sleep, time +#import gpio_control + +import PhonieboxVolume +import PhonieboxPlayer +from PhonieboxRpcServer import phoniebox_rpc_server + # get absolute path of this script dir_path = os.path.dirname(os.path.realpath(__file__)) defaultconfigFilePath = os.path.join(dir_path, 'phoniebox.conf') @@ -67,89 +73,54 @@ def run(self): # tells the PhonieboxDaemon to reload the config whenever needed. # card_assignments_file = daemon.get_setting("phoniebox","card_assignments_file") - # cardAssignmentsWatchdog = FileModifiedHandler(card_assignments_file, self.update_cardAssignments) + # cardAssignmentsWatchdog = FileModifiedH check for process existandler(card_assignments_file, self.update_cardAssignments) # ConfigWatchdog = FileModifiedHandler(configFilePath, self.read_config) # # start_reader runs an endless loop, nothing will be executed afterwards daemon.start_reader() - def start_reader(self): - from Reader import Reader - reader = Reader() - - card_detection_sound = self.get_setting("phoniebox", "card_detection_sound") - debounce_time = self.get_setting("phoniebox", "debounce_time") - if debounce_time == -1: - debounce_time = 0.5 - second_swipe_delay = self.get_setting("phoniebox", "second_swipe_delay") - if second_swipe_delay == -1: - second_swipe_delay = 0 - store_card_assignments = self.get_setting("phoniebox", "store_card_assignments") - if store_card_assignments == -1: - store_card_assignments = 30 - last_swipe = 0 - last_write_card_assignments = 0 - - while True: - # reading the card id - cardid = reader.reader.readCard() -# cardid = None -# sleep(debounce_time) - try: - # start the player script and pass on the cardid - if cardid is not None: - print("Card ID: {}".format(int(cardid))) - filename = self.get_setting("phoniebox", "Latest_RFID_file") - if filename != -1: - self.print_to_file(filename, "\'{}\' was used at {}".format(cardid, time())) - if card_detection_sound != -1: - self.play_alsa(card_detection_sound) - if cardid in self.cardAssignments.sections(): - # second swipe detection - if int(cardid) == int(self.lastplayedID) and time()-last_swipe > second_swipe_delay: - self.log("Second swipe for {}".format(cardid), 3) - self.do_second_swipe() - # if first swipe, just play - else: - last_swipe = time() - self.do_start_playlist(cardid) - # do not react for debounce_time - sleep(debounce_time) - else: - self.log("Card with ID {} not mapped yet.".format(cardid), 1) - - except OSError as e: - print("Execution failed:", e) - - # check if it is time for the next update of the cardAssignments and do it - # Note: this is purely time-based and not clever at all. Find a - # TODO: find a better way to check for changes in the files on disk to trigger the update - if time()-last_write_card_assignments > store_card_assignments and store_card_assignments != False: - # store card assignments - if self.get_setting("phoniebox", "translate_legacy_cardassignments", "bool") == True: - legacy_cardAssignments = self.translate_legacy_cardAssignments(last_write_card_assignments) - self.update_cardAssignments(legacy_cardAssignments) - else: - self.update_cardAssignments(self.read_cardAssignments) - - self.write_new_cardAssignments() - last_write_card_assignments = time() - - def signal_handler(self, signal, frame): - """ catches signal and triggers the graceful exit """ - print("Caught signal {}, exiting...".format(signal)) - self.exit_gracefully() - - def exit_gracefully(self): - """ stop mpd and write cardAssignments to disk if daemon is stopped """ - self.mpd_connect_timeout() - self.client.stop() - self.client.disconnect() - # write config to update playstate - self.write_new_cardAssignments() - - # exit script - sys.exit(0) + +def signal_handler(signal, frame): + """ catches signal and triggers the graceful exit """ + print("Caught signal {}, exiting...".format(signal)) + exit_gracefully(signal, frame) + +def exit_gracefully(esignal, frame): + print ("\nGot Signal {} ({}) \n {}".format(signal.Signals(esignal).name, esignal, frame)) + + #stop all threads + + #save all nv + #play stop (maybe) + #shutdown () + + + #""" stop mpd and write cardAssignments to disk if daemon is stopped """ + #self.mpd_connect_timeout() + #self.client.stop() + #self.client.disconnect() + # write config to update playstate + #self.write_new_cardAssignments() + # exit script + + print ("Exiting") + + sys.exit(0) + #trigger halt of system? + + +def startsound(): + print ("Play Start Sound") + ##play start sound + ##use dub to play sound? -> benchmark could be used to play 440 hz or other music things + + # import required libraries + from pydub import AudioSegment + from pydub.playback import play + + # Import an audio file + wav_file = AudioSegment.from_file(file = "../../shared/startupsound.wav", format = "wav") + play(wav_file) if __name__ == "__main__": @@ -161,11 +132,85 @@ def exit_gracefully(self): else: configFilePath = sys.argv[1] - daemon = PhonieboxDaemon(configFilePath) - + #sys.path.append(parentdir+"/scripts") + sys.path.insert(0,'../../gpio_control') + print(sys.path) + + #parse config + #gpio_config = configparser.ConfigParser(inline_comment_prefixes=";") + #gpio_config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') + #gpio_config.read(config_path) + + # Play Startup Sound + startsound_thread = threading.Thread(target=startsound) + startsound_thread.start() + + + PhonieboxVolume.list_cards() + PhonieboxVolume.list_mixers({ 'cardindex': 0 }) + + ##run zeromq_server as thread + #initialize Phonibox objcts + objects = {'volume':PhonieboxVolume.volume_control_alsa(), + 'player':PhonieboxPlayer.player_control()} + + print ("Init Phonibox ZMQ Server ") + rpcs = phoniebox_rpc_server(objects) + if rpcs != None: + rpcs.connect() + #pc_t = threading.Thread(target=pc.process_queue) + #pc_t.start() + + + ##rfid + #card id will be linked directly with object call which are feeded into the mq + cardid_db = {'104,49914':{'object':'','method':'','params':{}}, + '103,12632':{'object':'','method':'','params':{}}, + '104,29698':{'object':'','method':'','params':{}}, + '108,07437':{'object':'','method':'','params':{}}, + '107,60360':{'object':'','method':'','params':{}}, + '106,64513':{'object':'','method':'','params':{}}, + '104,14891':{'object':'','method':'','params':{}}, + '103,24033':{'object':'','method':'','params':{}}, + '104,32860':{'object':'','method':'','params':{}} } + + #rfid_reader = RFID_Reader("RDM6300",{'numberformat':'card_id_float'}) + rfid_reader = None + if rfid_reader is not None: + rfid_reader.set_cardid_db() + rfid_thread = threading.Thread(target=rfid_reader.run) + #rfid_t.start() + else: + rfid_thread = None + + ##initialize gpio + #gpio_config = configparser.ConfigParser(inline_comment_prefixes=";") + gpio_config = None + if gpio_config is not None: + gpio_config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') + gpio_config.read(config_path) + + phoniebox_function_calls = function_calls.phoniebox_function_calls() + gpio_controler = gpio_control(phoniebox_function_calls) + + devices = gpio_controler.get_all_devices(config) + gpio_controler.print_all_devices() + gpio_thread = threading.Thread(target=gpio_controler.gpio_loop) + else: + gpio_thread = None + + # signal.raise_signal(signum) # setup the signal listeners - signal.signal(signal.SIGINT, daemon.exit_gracefully) - signal.signal(signal.SIGTERM, daemon.exit_gracefully) - - # start the daemon (blocking) - daemon.run() + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + + #Start threads and RPC Server + if rpcs is not None: + if gpio_thread is not None: + print ("Starting GPIO Thread") + gpio_thread.start() + if rfid_thread is not None: + print ("Starting RFID Thread") + rfid_thread.start() + print ("Starting ZMQ Server") + rpcs.server() \ No newline at end of file diff --git a/scripts/python-phoniebox/PhonieboxNvManager.py b/scripts/python-phoniebox/PhonieboxNvManager.py new file mode 100644 index 000000000..817b8097d --- /dev/null +++ b/scripts/python-phoniebox/PhonieboxNvManager.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import json + +#Non Volatile data storage +# to avoid frequent file system writes to prevent sd card wereout this module should take care of non volatile data and their storage +#preferably as homan readable +# the idea is that submodules create an nv object, for data they like to store, these are then interacting with a singletonm. + +#do we need data type awareness? + +class nv_object(): + __init__(instance_name,object_name) + check ifexist + nv_manager register new object + + def get() + + sef set(val) + + +class nv_manager + __init__(file_name) + + register new object: + + set storage frequncex: + + read + # Read data from file: + data = json.load( open( "file_name.json" ) ) + + write + # Serialize data into file: + json.dump( data, open( "file_name.json", 'w' ) ) \ No newline at end of file diff --git a/scripts/python-phoniebox/PhonieboxPlayer.py b/scripts/python-phoniebox/PhonieboxPlayer.py new file mode 100644 index 000000000..8a036f191 --- /dev/null +++ b/scripts/python-phoniebox/PhonieboxPlayer.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from mpd import MPDClient + + +class player_control: + def __init__(self): + self.mpd_client = MPDClient() # create client object + self.mpd_client.timeout = 0.5 # network timeout in seconds (floats allowed), default: None + self.mpd_client.idletimeout = 0.5 # timeout for fetching the result of the idle command is handled seperately, default: None + #self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 + self.connect() + print("Connected to MPD Version: "+self.mpd_client.mpd_version) + + def connect(self): + self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 + + def _mpd_retry(self,mpd_cmd,params=None): + try: + mpd_cmd(params) + except ConnectionError: + print ("MPD Connection Error, retry") + self.conncet() + mpd_cmd(params) + except Exception as e: + print(e) + + + def get_player_type_and_version(self, param): + return ({'tpye':'mpd','version':self.mpd_client.mpd_version}) + + + + def play(self, param): + try: + self.mpd_client.play() + except ConnectionError: + print ("MPD Connection Error, retry") + self.conncet() + self.mpd_client.play() + except Exception as e: + print(e) + song = self.mpd_client.currentsong() + return ({'song':song}) + + def get_current_song(self, param): + song = self.mpd_client.currentsong() + #resp = {'resp': self.mpd_client.currentsong()} + return song + + def playlistaddplay(self, param): + + # add to playlist (and play) + # this command clears the playlist, loads a new playlist and plays it. It also handles the resume play feature. + # FOLDER = rel path from audiofolders + # VALUE = name of playlist + + # NEW VERSION: + # Read the current config file (include will execute == read) + #. "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" + + # load playlist + #mpc clear + #mpc load "${VALUE//\//SLASH}" + #if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "mpc load "${VALUE//\//SLASH} >> ${PATHDATA}/../logs/debug.log; fi + + # Change some settings according to current folder IF the folder.conf exists + #. ${PATHDATA}/inc.settingsFolderSpecific.sh + + # check if we switch to single file playout + #${PATHDATA}/single_play.sh -c=single_check -d="${FOLDER}" + + # check if we shuffle the playlist + #${PATHDATA}/shuffle_play.sh -c=shuffle_check -d="${FOLDER}" + + # Unmute if muted + #if [ -f $VOLFILE ]; then + # $VOLFILE DOES exist == audio off + # read volume level from $VOLFILE and set as percent + # echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 + # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) + # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% + # delete $VOLFILE + # rm -f $VOLFILE + #fi + + # Now load and play + #if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "mpc load "${VALUE//\//SLASH}" && ${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}"" >> ${PATHDATA}/../logs/debug.log; fi + #${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}" + + # write latest folder played to settings file + #sudo echo ${FOLDER} > ${PATHDATA}/../settings/Latest_Folder_Played + #sudo chown pi:www-data ${PATHDATA}/../settings/Latest_Folder_Played + #sudo chmod 777 ${PATHDATA}/../settings/Latest_Folder_Played + + self.mpd_client.add(uri) + self.mpd_client.load(name[123, start:end]) + + song = self.mpd_client.currentsong() + return ({'song':song}) \ No newline at end of file diff --git a/scripts/python-phoniebox/PhonieboxRpcServer.py b/scripts/python-phoniebox/PhonieboxRpcServer.py new file mode 100644 index 000000000..ba8782277 --- /dev/null +++ b/scripts/python-phoniebox/PhonieboxRpcServer.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import nanotime +import zmq +import json +import time + +class phoniebox_rpc_server: + + def __init__(self,objects): + self.objects = objects + self.context = None + self._keep_running = True + + def connect(self,addr= None): + if addr == None: + addr = "tcp://127.0.0.1:5555" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(addr) + self.socket.setsockopt(zmq.LINGER, 200) + + def execute(self, obj, cmd, param): + call_obj = self.objects.get(obj) + + if (call_obj is not None): + call_function = getattr(call_obj,cmd,None) + if (call_function is not None): # is callable() ?? + response = call_function(param) + print (response) + else: + response = {'resp': "no valid commad"} + print (response) + else: + response = {'resp': "no valid obj"} + print (response) + return response + + def terminate(self): + self._keep_running = False + + def server(self): + self._keep_running = True + #todo: check if connected, otherwise connect or exit? + + while self._keep_running: + # Wait for next request from client + message = self.socket.recv() + nt = nanotime.now().nanoseconds() + + client_request=json.loads(message) + client_response = {} + + print (client_request) + + #lets make it jsonrpc https://www.jsonrpc.org/specification + #{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3} + #{"jsonrpc": "2.0", "result": 19, "id": 3} + + #hm, overhead, + strucure, we should takeover id + + #{'object':'','method':'','params':{}} + + client_object = client_request.get('obj') #lets call it object + if (client_object != None): + client_command = client_request.get('cmd') #lets call it method + client_id = client_request.get('id') + if (client_command != None): + client_param = client_request.get('param') #lets call it params + client_response['resp'] = self.execute(client_object,client_command,client_param) + client_response['id'] = client_id + + client_tsp = client_request.get('tsp') + if (client_tsp != None): + client_response['total_processing_time'] = (nt - int(client_request['tsp'])) / 1000000 + print ("processing time: {:2.3f} ms".format(client_response['total_processing_time'])) + + print(client_response) + # Send reply back to client + self.socket.send_string(json.dumps(client_response)) + + return (1) \ No newline at end of file diff --git a/scripts/python-phoniebox/PhonieboxVolume.py b/scripts/python-phoniebox/PhonieboxVolume.py new file mode 100644 index 000000000..33856ffec --- /dev/null +++ b/scripts/python-phoniebox/PhonieboxVolume.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import alsaaudio + +class volume_control_alsa: + def __init__(self): + self.mixer = alsaaudio.Mixer('Master', 0) + self.volume = 0 + #self.mixer.getvolume() + + def get(self, param): + return ({'volume':self.volume}) + + def set(self, param): + volume = param.get('volume') + if isinstance(volume, int): + if (volume < 0): volume = 0; + if (volume > 100): volume = 100; + self.volume = volume + self.mixer.setvolume(self.volume) + else: + volume = -1 + return ({'volume':volume}) + + def inc(self, param): + volume = self.volume +3 + if (volume > 100): volume = 100 + self.volume = volume + self.mixer.setvolume(self.volume) + return ({'volume':self.volume}) + + def dec(self, param): + volume = self.volume -3 + if (volume < 0): volume = 0 + self.volume = volume + self.mixer.setvolume(self.volume) + return ({'volume':self.volume}) + + + +def list_cards(): + print("Available sound cards:") + for i in alsaaudio.card_indexes(): + (name, longname) = alsaaudio.card_name(i) + print(" %d: %s (%s)" % (i, name, longname)) + +def list_mixers(kwargs): + print("Available mixer controls:") + for m in alsaaudio.mixers(**kwargs): + print(" '%s'" % m) + diff --git a/scripts/python-phoniebox/PhonieboxZmqServer.py b/scripts/python-phoniebox/PhonieboxZmqServer.py new file mode 100644 index 000000000..16867b2ed --- /dev/null +++ b/scripts/python-phoniebox/PhonieboxZmqServer.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import nanotime +import zmq +import json +import time + +class phoniebox_control: + + def __init__(self,objects): + self.objects = objects + self.context = None + self._keep_running = True + + def connect(self,addr= None): + if addr == None: + addr = "tcp://127.0.0.1:5555" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(addr) + self.socket.setsockopt(zmq.LINGER, 200) + + def execute(self, obj, cmd, param): + run_obj = self.objects.get(obj) + + if (run_obj is not None): + run_func = getattr(run_obj,cmd,None) + if (run_func is not None): # is callable() ?? + resp = run_func(param) + print (resp) + else: + resp = {'resp': "no valid commad"} + print (resp) + else: + resp = {'resp': "no valid obj"} + print (resp) + return resp + + def terminate(self): + self._keep_running = False: + + def process_queue(self): + self._keep_running = True + while self._keep_running: + # Wait for next request from client + message = self.socket.recv() + nt = nanotime.now().nanoseconds() + + client_request=json.loads(message) + client_response = {} + + print (client_request) + + #lets make it jsonrpc https://www.jsonrpc.org/specification + #{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3} + #{"jsonrpc": "2.0", "result": 19, "id": 3} + + #hm, overhead, + strucure, we should takeover id + + #{'object':'','method':'','params':{}} + + client_object = client_request.get('obj') #lets call it object + if (client_object != None): + client_command = client_request.get('cmd') #lets call it method + client_id = client_request.get('id') + if (client_command != None): + client_param = client_request.get('param') #lets call it params + client_response['resp'] = self.execute(client_object,client_command,client_param) + client_response['id'] = client_id + + client_tsp = client_request.get('tsp') + if (client_tsp != None): + client_response['total_processing_time'] = (nt - int(client_request['tsp'])) / 1000000 + print ("processing time: {:2.3f} ms".format(client_response['total_processing_time'])) + + print(client_response) + # Send reply back to client + self.socket.send_string(json.dumps(client_response)) + + return (1) \ No newline at end of file From 94f4a3a3bab39000c2344f3a7221dac04185d52c Mon Sep 17 00:00:00 2001 From: arne123 Date: Mon, 1 Mar 2021 00:30:59 +0100 Subject: [PATCH 005/606] rename ZMQ Server to RPC Server --- .../python-phoniebox/PhonieboxZmqServer.py | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 scripts/python-phoniebox/PhonieboxZmqServer.py diff --git a/scripts/python-phoniebox/PhonieboxZmqServer.py b/scripts/python-phoniebox/PhonieboxZmqServer.py deleted file mode 100644 index 16867b2ed..000000000 --- a/scripts/python-phoniebox/PhonieboxZmqServer.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import nanotime -import zmq -import json -import time - -class phoniebox_control: - - def __init__(self,objects): - self.objects = objects - self.context = None - self._keep_running = True - - def connect(self,addr= None): - if addr == None: - addr = "tcp://127.0.0.1:5555" - self.context = zmq.Context() - self.socket = self.context.socket(zmq.REP) - self.socket.bind(addr) - self.socket.setsockopt(zmq.LINGER, 200) - - def execute(self, obj, cmd, param): - run_obj = self.objects.get(obj) - - if (run_obj is not None): - run_func = getattr(run_obj,cmd,None) - if (run_func is not None): # is callable() ?? - resp = run_func(param) - print (resp) - else: - resp = {'resp': "no valid commad"} - print (resp) - else: - resp = {'resp': "no valid obj"} - print (resp) - return resp - - def terminate(self): - self._keep_running = False: - - def process_queue(self): - self._keep_running = True - while self._keep_running: - # Wait for next request from client - message = self.socket.recv() - nt = nanotime.now().nanoseconds() - - client_request=json.loads(message) - client_response = {} - - print (client_request) - - #lets make it jsonrpc https://www.jsonrpc.org/specification - #{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3} - #{"jsonrpc": "2.0", "result": 19, "id": 3} - - #hm, overhead, + strucure, we should takeover id - - #{'object':'','method':'','params':{}} - - client_object = client_request.get('obj') #lets call it object - if (client_object != None): - client_command = client_request.get('cmd') #lets call it method - client_id = client_request.get('id') - if (client_command != None): - client_param = client_request.get('param') #lets call it params - client_response['resp'] = self.execute(client_object,client_command,client_param) - client_response['id'] = client_id - - client_tsp = client_request.get('tsp') - if (client_tsp != None): - client_response['total_processing_time'] = (nt - int(client_request['tsp'])) / 1000000 - print ("processing time: {:2.3f} ms".format(client_response['total_processing_time'])) - - print(client_response) - # Send reply back to client - self.socket.send_string(json.dumps(client_response)) - - return (1) \ No newline at end of file From 905e5cc26aba50bacc22436d18c0e2401f14891c Mon Sep 17 00:00:00 2001 From: arne123 Date: Wed, 3 Mar 2021 00:07:09 +0100 Subject: [PATCH 006/606] added cli client proposal --- components/cli_client/pbc.c | 272 ++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 components/cli_client/pbc.c diff --git a/components/cli_client/pbc.c b/components/cli_client/pbc.c new file mode 100644 index 000000000..787c26661 --- /dev/null +++ b/components/cli_client/pbc.c @@ -0,0 +1,272 @@ +/** + \file rfg.c + + rfg cli and main + + Copyright (C) 2008 Arne Pagel + + 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 . + +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + + +//#include + +#include + +//apt-get install libczmq-dev + +// build this gcc pbc.c -o pbc -lzmq + +#define MAX_STRLEN 256 +#define MAX_REQEST_STRLEN (MAX_STRLEN * 3) + 256 + +int g_verbose = 0; + + +typedef struct +{ + char object [MAX_STRLEN]; + char method [MAX_STRLEN]; + char params [MAX_STRLEN]; +} t_request; + +void * connect_and_send_request(t_request * tr) +{ + char json_request[MAX_REQEST_STRLEN]; + size_t json_len; + printf ("Connecting to hello world server…\n"); + + + + /*printf ("object %s'\n", tr->object); + printf ("object %s'\n", tr->method); + printf ("object %s'\n", tr->params);*/ + + snprintf(json_request,MAX_REQEST_STRLEN,"{\"OBJECT\": %s, \"METHOD\": %s, \"PARAMS\": {%s},\"id\":%d}",tr->object,tr->method,tr->params,123); + json_len = strlen(json_request); + + + if (g_verbose) printf("Sending Request (%ld Bytes):\n%s\n",json_len,json_request); + + void *context = zmq_ctx_new (); + void *requester = zmq_socket (context, ZMQ_REQ); + + + zmq_connect (requester, "tcp://localhost:5555"); + + zmq_send (requester, json_request, json_len, 0); + //zmq_recv (requester, buffer, 10, 0); + //printf ("Received World %d\n", request_nbr); + + zmq_close (requester); + zmq_ctx_destroy (context); + return 0; +} + + + + +void usage(void) +{ + fprintf(stderr,"\nusage: rfg -i inputfile -o outputfile -t outpufiletype\n\n"); + //fprintf(stderr," -b startsegment for firmwarefile\n"); + //fprintf(stderr," -c generate crc checksum struct within the firmware at 0x0C10200 \n"); + fprintf(stderr," -d swtich debugmesages on\n"); + fprintf(stderr," -h this screen\n"); + fprintf(stderr," -i input firmware-file\n"); + //fprintf(stderr," -l fpga-file\n"); + + //fprintf(stderr," -f outputformat\n"); + //fprintf(stderr," -g gui-override, displays a dialog wich is asking you what to do\n"); + + //fprintf(stderr," -n create name.txt, a file with version and date\n"); + //fprintf(stderr," -s create sw_info struct (only with -c)\n"); + fprintf(stderr," -t Outfiletype: e = elf, h = ihex, \n"); + + fprintf(stderr," -v verbose\n"); + //fprintf(stderr," -x generate intel hex format (128 byte per row)\n"); + //fprintf(stderr," -z compress-firmware (default without compression)\n"); + + fprintf(stderr,"\nrfg, written by Arne Pagel 01.Feb.2007\n"); + fprintf(stderr,"last change %s\n\n",__DATE__); + exit (1); +} + +/** + returns the index of the first argument that is not an option; i.e. + does not start with a dash or a slash +*/ +int HandleOptions(int argc,char *argv[], t_request * tr) +{ + int i,c,firstnonoption=0; + int oft_cnt = 0,of_cnt = 0; + + const struct option long_options[] = + { + /* These options set a flag. */ + //{"verbose", no_argument, &verbose_flag, 1}, + //{"brief", no_argument, &verbose_flag, 0}, + /* These options don't set a flag. + We distinguish them by their indices. */ + {"help", no_argument, 0, 'h'}, + {"object", required_argument, 0, 'o'}, + {"method", required_argument, 0, 'm'}, + {"params", optional_argument, 0, 'p'}, + {0, 0, 0, 0} + }; + + const char short_options[] = {"o:m:p:?hv"}; + + while (1) + { + int option_index = 0; // getopt_long stores the option index here. + + c = getopt_long (argc, argv,short_options,long_options, &option_index); + + // Detect the end of the options. + if (c == -1) break; + + switch (c) + { + case '?': + case 'h': + usage(); + puts ("option -a\n"); + break; + + case 'o': + strncpy (tr->object,optarg,MAX_STRLEN); + break; + + case 'm': + strncpy (tr->method,optarg,MAX_STRLEN); + break; + + case 'p': + strncpy (tr->params,optarg,MAX_STRLEN); + break; + + case 'v': + g_verbose = '1'; + break; + + default: + usage(); + abort (); + } + } + + /* Print any remaining command line arguments (not options). */ + if (optind < argc) + { + printf ("non-option ARGV-elements: "); + while (optind < argc) printf ("%s ", argv[optind++]); + putchar ('\n'); + } + + return firstnonoption; +} + + + +#ifdef WIN32 + #include "windows.h" +#endif + +#define CC_BLACK 0 +#define CC_BLUE 1 +#define CC_RED 2 + +void set_color(int color) +{ + #ifdef WIN32 + int w_col; + HANDLE hConsole; + static CONSOLE_SCREEN_BUFFER_INFO ConsoleInfo; + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); // Get handle to standard output + static int init; + + if (init == 0) + { + init = 1; + GetConsoleScreenBufferInfo(hConsole, &ConsoleInfo); + } + + switch (color) + { + case CC_BLACK: + w_col = ConsoleInfo.wAttributes; + break; + case CC_BLUE: + w_col = FOREGROUND_BLUE | FOREGROUND_INTENSITY; + break; + case CC_RED: + w_col = FOREGROUND_RED | FOREGROUND_INTENSITY; + break; + } + SetConsoleTextAttribute(hConsole,w_col); // set the text attribute of the previous handle + #else + switch (color) + { + case CC_BLACK: + printf ("\033[0;00m"); + break; + case CC_BLUE: + printf ("\033[1;34m"); + break; + case CC_RED: + printf ("\033[1;33m"); + break; + } + #endif +} + + + +int main(int argc,char *argv[]) +{ + time_t timestamp; + struct tm * tmtime; + t_request tr; + + bzero(&tr, sizeof(t_request)); + + + + +//t_data_sec_descriptor sd; + + //Zeitstempel holen + time(×tamp); + + //Zeitstempel konvertieren + tmtime = localtime(×tamp); + + //Programmoptionen interpretieren + HandleOptions(argc,argv,&tr); + + connect_and_send_request(&tr); + + return 0; +} From 54de3a34e1a60ef73d44a36ccbca781c85a1e47b Mon Sep 17 00:00:00 2001 From: arne123 Date: Sat, 6 Mar 2021 00:45:34 +0100 Subject: [PATCH 007/606] added response for pbc --- .gitignore | 4 + components/cli_client/pbc.c | 269 ++++++++---------- scripts/python-phoniebox/PhonieboxDaemon.py | 84 +----- .../python-phoniebox/PhonieboxNvManager.py | 29 +- scripts/python-phoniebox/PhonieboxPlayer.py | 16 +- .../python-phoniebox/PhonieboxRpcServer.py | 14 +- scripts/python-phoniebox/PhonieboxVolume.py | 3 +- 7 files changed, 156 insertions(+), 263 deletions(-) diff --git a/.gitignore b/.gitignore index 61fd2b9b6..5085d9879 100755 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ composer.phar # composer.lock # End of https://www.gitignore.io/api/composer + + +components/cli_client/pbc + diff --git a/components/cli_client/pbc.c b/components/cli_client/pbc.c index 787c26661..c8920a923 100644 --- a/components/cli_client/pbc.c +++ b/components/cli_client/pbc.c @@ -1,114 +1,155 @@ /** - \file rfg.c + \file pbc.c - rfg cli and main + MIT License - Copyright (C) 2008 Arne Pagel + Copyright (C) 2021 Arne Pagel - 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. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: - 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 . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. */ -#include -#include -#include -#include -#include -#include -#include -#include +/* + pbc -> PhonieBox Command line interface -//#include + depenmds on libczmq: + apt-get install libczmq-dev -#include + how to compile: + gcc pbc.c -o pbc -lzmq -Wall -//apt-get install libczmq-dev +*/ -// build this gcc pbc.c -o pbc -lzmq +#include +#include #define MAX_STRLEN 256 -#define MAX_REQEST_STRLEN (MAX_STRLEN * 3) + 256 - +#define MAX_REQEST_STRLEN (MAX_STRLEN * 16) +#define MAX_PARAMS 16 int g_verbose = 0; - typedef struct { char object [MAX_STRLEN]; char method [MAX_STRLEN]; - char params [MAX_STRLEN]; + char params [MAX_PARAMS][MAX_STRLEN]; + int num_params; } t_request; + +int send_zmq_request_and_wait_response(char * request, int request_len, char * response, int max_response_len) +{ + int zmq_ret,ret = -1; + void *context = zmq_ctx_new (); + void *requester = zmq_socket (context, ZMQ_REQ); + int linger = 200; + zmq_setsockopt(requester,ZMQ_LINGER,&linger,sizeof(linger)); + zmq_setsockopt(requester,ZMQ_RCVTIMEO,&linger,sizeof(linger)); + zmq_connect (requester, "tcp://localhost:5555"); + + zmq_ret = zmq_send (requester, request, request_len, 0); + + if (zmq_ret > 0) + { + zmq_ret = zmq_recv (requester, response, max_response_len, 0); + + if (zmq_ret > 0) + { + printf ("Received %s (%d Bytes)\n", response,zmq_ret); + ret = 0; + } + else + { + printf ("zmq_recv rturned %d \n", zmq_ret); + } + } + else + { + if (g_verbose) printf ("zmq_send returned %d\n", zmq_ret); + } + + zmq_close (requester); + zmq_ctx_destroy (context); + return (ret); +} + + void * connect_and_send_request(t_request * tr) { char json_request[MAX_REQEST_STRLEN]; + char json_response[MAX_REQEST_STRLEN]; + char params[MAX_STRLEN * 8]; size_t json_len; - printf ("Connecting to hello world server…\n"); - - - - /*printf ("object %s'\n", tr->object); - printf ("object %s'\n", tr->method); - printf ("object %s'\n", tr->params);*/ + int n; - snprintf(json_request,MAX_REQEST_STRLEN,"{\"OBJECT\": %s, \"METHOD\": %s, \"PARAMS\": {%s},\"id\":%d}",tr->object,tr->method,tr->params,123); - json_len = strlen(json_request); + if (tr->num_params > 0) + { + sprintf(params, "\"params\":{"); + + for (n = 0;n < tr->num_params;) + { + strcat(params,tr->params[n]); + n++; + if (n < tr->num_params) strcat(params,","); + } - - if (g_verbose) printf("Sending Request (%ld Bytes):\n%s\n",json_len,json_request); + strcat(params,"},"); - void *context = zmq_ctx_new (); - void *requester = zmq_socket (context, ZMQ_REQ); + } + else params[0] = 0; + snprintf(json_request,MAX_REQEST_STRLEN,"{\"object\": \"%s\", \"method\": \"%s\", %s\"id\":%d}",tr->object,tr->method,params,123); + json_len = strlen(json_request); - zmq_connect (requester, "tcp://localhost:5555"); + if (g_verbose) printf("Sending Request (%ld Bytes):\n%s\n",json_len,json_request); - zmq_send (requester, json_request, json_len, 0); - //zmq_recv (requester, buffer, 10, 0); - //printf ("Received World %d\n", request_nbr); + send_zmq_request_and_wait_response(json_request,json_len,json_response,MAX_REQEST_STRLEN); - zmq_close (requester); - zmq_ctx_destroy (context); return 0; } - +int check_and_map_parameters_to_json(char * arg, t_request * tr) +{ + char * name; + char * value; + char * fmt; + int ret = 0; + if (strchr(arg, ':') != NULL) + { + name = strtok(arg, ":"); + value = strtok(NULL, ":"); + fmt = (isdigit(*value)) ? "\"%s\":%s" : "\"%s\":\"%s\""; + snprintf (tr->params[tr->num_params++],MAX_STRLEN, fmt,name,value); + ret = 1; + } + return (ret); +} void usage(void) { - fprintf(stderr,"\nusage: rfg -i inputfile -o outputfile -t outpufiletype\n\n"); - //fprintf(stderr," -b startsegment for firmwarefile\n"); - //fprintf(stderr," -c generate crc checksum struct within the firmware at 0x0C10200 \n"); - fprintf(stderr," -d swtich debugmesages on\n"); + fprintf(stderr,"\npbc -> PhonieBox Command line interface\nusage: pbc -o object -m method param_name:value\n\n"); fprintf(stderr," -h this screen\n"); - fprintf(stderr," -i input firmware-file\n"); - //fprintf(stderr," -l fpga-file\n"); - - //fprintf(stderr," -f outputformat\n"); - //fprintf(stderr," -g gui-override, displays a dialog wich is asking you what to do\n"); - - //fprintf(stderr," -n create name.txt, a file with version and date\n"); - //fprintf(stderr," -s create sw_info struct (only with -c)\n"); - fprintf(stderr," -t Outfiletype: e = elf, h = ihex, \n"); - + fprintf(stderr," -o, --object object\n"); + fprintf(stderr," -m, --method method\n"); fprintf(stderr," -v verbose\n"); - //fprintf(stderr," -x generate intel hex format (128 byte per row)\n"); - //fprintf(stderr," -z compress-firmware (default without compression)\n"); - fprintf(stderr,"\nrfg, written by Arne Pagel 01.Feb.2007\n"); fprintf(stderr,"last change %s\n\n",__DATE__); exit (1); } @@ -119,9 +160,8 @@ void usage(void) */ int HandleOptions(int argc,char *argv[], t_request * tr) { - int i,c,firstnonoption=0; - int oft_cnt = 0,of_cnt = 0; - + int c; + const struct option long_options[] = { /* These options set a flag. */ @@ -132,7 +172,6 @@ int HandleOptions(int argc,char *argv[], t_request * tr) {"help", no_argument, 0, 'h'}, {"object", required_argument, 0, 'o'}, {"method", required_argument, 0, 'm'}, - {"params", optional_argument, 0, 'p'}, {0, 0, 0, 0} }; @@ -163,10 +202,6 @@ int HandleOptions(int argc,char *argv[], t_request * tr) strncpy (tr->method,optarg,MAX_STRLEN); break; - case 'p': - strncpy (tr->params,optarg,MAX_STRLEN); - break; - case 'v': g_verbose = '1'; break; @@ -177,96 +212,26 @@ int HandleOptions(int argc,char *argv[], t_request * tr) } } - /* Print any remaining command line arguments (not options). */ + /* treat remaining command line arguments (not options). */ if (optind < argc) { - printf ("non-option ARGV-elements: "); - while (optind < argc) printf ("%s ", argv[optind++]); - putchar ('\n'); + while (optind < argc) + { + check_and_map_parameters_to_json(argv[optind++], tr); + } } - return firstnonoption; + return (1); } - - -#ifdef WIN32 - #include "windows.h" -#endif - -#define CC_BLACK 0 -#define CC_BLUE 1 -#define CC_RED 2 - -void set_color(int color) -{ - #ifdef WIN32 - int w_col; - HANDLE hConsole; - static CONSOLE_SCREEN_BUFFER_INFO ConsoleInfo; - hConsole = GetStdHandle(STD_OUTPUT_HANDLE); // Get handle to standard output - static int init; - - if (init == 0) - { - init = 1; - GetConsoleScreenBufferInfo(hConsole, &ConsoleInfo); - } - - switch (color) - { - case CC_BLACK: - w_col = ConsoleInfo.wAttributes; - break; - case CC_BLUE: - w_col = FOREGROUND_BLUE | FOREGROUND_INTENSITY; - break; - case CC_RED: - w_col = FOREGROUND_RED | FOREGROUND_INTENSITY; - break; - } - SetConsoleTextAttribute(hConsole,w_col); // set the text attribute of the previous handle - #else - switch (color) - { - case CC_BLACK: - printf ("\033[0;00m"); - break; - case CC_BLUE: - printf ("\033[1;34m"); - break; - case CC_RED: - printf ("\033[1;33m"); - break; - } - #endif -} - - - int main(int argc,char *argv[]) { - time_t timestamp; - struct tm * tmtime; t_request tr; bzero(&tr, sizeof(t_request)); - - - -//t_data_sec_descriptor sd; - - //Zeitstempel holen - time(×tamp); - - //Zeitstempel konvertieren - tmtime = localtime(×tamp); - - //Programmoptionen interpretieren HandleOptions(argc,argv,&tr); - connect_and_send_request(&tr); - + return 0; } diff --git a/scripts/python-phoniebox/PhonieboxDaemon.py b/scripts/python-phoniebox/PhonieboxDaemon.py index 8963cf060..eb730394d 100755 --- a/scripts/python-phoniebox/PhonieboxDaemon.py +++ b/scripts/python-phoniebox/PhonieboxDaemon.py @@ -4,7 +4,6 @@ import threading import sys, os.path import signal -from Phoniebox import Phoniebox from time import sleep, time #import gpio_control @@ -17,69 +16,6 @@ dir_path = os.path.dirname(os.path.realpath(__file__)) defaultconfigFilePath = os.path.join(dir_path, 'phoniebox.conf') -# watchdog blocks the script, so it cannot be used in the same file as the PhonieboxDaemon -# from watchdog.observers import Observer -# from watchdog.events import FileSystemEventHandler -# from os.path import dirname - -# class FileModifiedHandler(FileSystemEventHandler): - -# """ watch the given file for changes and execute callback function on modification """ -# def __init__(self, file_path, callback): -# self.file_path = file_path -# self.callback = callback - -# # set observer to watch for changes in the directory -# self.observer = Observer() -# self.observer.schedule(self, dirname(file_path), recursive=False) -# self.observer.start() -# try: -# while True: -# sleep(1) -# except KeyboardInterrupt: -# self.observer.stop() -# self.observer.join() -# -# def on_modified(self, event): -# # only act on the change that we're looking for -# if not event.is_directory and event.src_path.endswith(self.file_path): -# daemon.log("cardAssignmentsFile modified!",3) -# self.callback() # call callback - - -class PhonieboxDaemon(Phoniebox): - """ This subclass of Phoniebox is to be called directly, running as RFID reader daemon """ - - def __init__(self, configFilePath=defaultconfigFilePath): - Phoniebox.__init__(self, configFilePath) - self.lastplayedID = 0 - - def run(self): - # do things if killed - signal.signal(signal.SIGINT, self.signal_handler) - signal.signal(signal.SIGTERM, self.signal_handler) - - # establish mpd connection - self.mpd_init_connection() - self.mpd_init_settings() - state = self.client.status()["state"] - - daemon.play_alsa(daemon.get_setting("phoniebox", 'startup_sound')) - if state == "play": - self.client.play() - - # launch watcher for config files, blocks the script - # TODO: it would be better to watch the changes with a second process that - # tells the PhonieboxDaemon to reload the config whenever needed. - - # card_assignments_file = daemon.get_setting("phoniebox","card_assignments_file") - # cardAssignmentsWatchdog = FileModifiedH check for process existandler(card_assignments_file, self.update_cardAssignments) - # ConfigWatchdog = FileModifiedHandler(configFilePath, self.read_config) - -# # start_reader runs an endless loop, nothing will be executed afterwards - daemon.start_reader() - - def signal_handler(signal, frame): """ catches signal and triggers the graceful exit """ print("Caught signal {}, exiting...".format(signal)) @@ -94,26 +30,11 @@ def exit_gracefully(esignal, frame): #play stop (maybe) #shutdown () - - #""" stop mpd and write cardAssignments to disk if daemon is stopped """ - #self.mpd_connect_timeout() - #self.client.stop() - #self.client.disconnect() - # write config to update playstate - #self.write_new_cardAssignments() - # exit script - print ("Exiting") - sys.exit(0) - #trigger halt of system? - -def startsound(): - print ("Play Start Sound") - ##play start sound - ##use dub to play sound? -> benchmark could be used to play 440 hz or other music things +def playstartsound(): # import required libraries from pydub import AudioSegment from pydub.playback import play @@ -142,14 +63,13 @@ def startsound(): #gpio_config.read(config_path) # Play Startup Sound - startsound_thread = threading.Thread(target=startsound) + startsound_thread = threading.Thread(target=playstartsound) startsound_thread.start() PhonieboxVolume.list_cards() PhonieboxVolume.list_mixers({ 'cardindex': 0 }) - ##run zeromq_server as thread #initialize Phonibox objcts objects = {'volume':PhonieboxVolume.volume_control_alsa(), 'player':PhonieboxPlayer.player_control()} diff --git a/scripts/python-phoniebox/PhonieboxNvManager.py b/scripts/python-phoniebox/PhonieboxNvManager.py index 817b8097d..5f7a0d9fb 100644 --- a/scripts/python-phoniebox/PhonieboxNvManager.py +++ b/scripts/python-phoniebox/PhonieboxNvManager.py @@ -4,14 +4,26 @@ #Non Volatile data storage # to avoid frequent file system writes to prevent sd card wereout this module should take care of non volatile data and their storage -#preferably as homan readable -# the idea is that submodules create an nv object, for data they like to store, these are then interacting with a singletonm. +# preferably as human readable, probaly json +# the idea is that submodules create an nv object, for data they like to store +#the Idea is just to handle dictionaries, do we need data type awareness here? +#this could offer the option, that instead of a central configuration, each subclass can handle its configuration on its own, with setter / getter methods +class nv_object(): + __instance = None + @staticmethod + def getInstance(): + """ Static access method. """ + if nv_object.__instance == None: + nv_object() + return nv_object.__instance + def __init__(self): + """ Virtually private constructor. """ + if nv_object.__instance != None: + raise Exception("This class is a singleton!") + else: + nv_object.__instance = self -#do we need data type awareness? -class nv_object(): - __init__(instance_name,object_name) - check ifexist nv_manager register new object def get() @@ -22,9 +34,10 @@ def get() class nv_manager __init__(file_name) - register new object: + def register_new_nv_object: + - set storage frequncex: + def set_storage_frequncey: read # Read data from file: diff --git a/scripts/python-phoniebox/PhonieboxPlayer.py b/scripts/python-phoniebox/PhonieboxPlayer.py index 8a036f191..a2e11c9f5 100644 --- a/scripts/python-phoniebox/PhonieboxPlayer.py +++ b/scripts/python-phoniebox/PhonieboxPlayer.py @@ -3,13 +3,11 @@ from mpd import MPDClient - class player_control: def __init__(self): self.mpd_client = MPDClient() # create client object self.mpd_client.timeout = 0.5 # network timeout in seconds (floats allowed), default: None self.mpd_client.idletimeout = 0.5 # timeout for fetching the result of the idle command is handled seperately, default: None - #self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 self.connect() print("Connected to MPD Version: "+self.mpd_client.mpd_version) @@ -18,19 +16,17 @@ def connect(self): def _mpd_retry(self,mpd_cmd,params=None): try: - mpd_cmd(params) + ret = mpd_cmd(params) except ConnectionError: print ("MPD Connection Error, retry") self.conncet() - mpd_cmd(params) + ret = mpd_cmd(params) except Exception as e: print(e) + return ret - def get_player_type_and_version(self, param): - return ({'tpye':'mpd','version':self.mpd_client.mpd_version}) - - + return ({'result':'mpd','version':self.mpd_client.mpd_version}) def play(self, param): try: @@ -45,9 +41,7 @@ def play(self, param): return ({'song':song}) def get_current_song(self, param): - song = self.mpd_client.currentsong() - #resp = {'resp': self.mpd_client.currentsong()} - return song + return {'resp': self.mpd_client.currentsong()} def playlistaddplay(self, param): diff --git a/scripts/python-phoniebox/PhonieboxRpcServer.py b/scripts/python-phoniebox/PhonieboxRpcServer.py index ba8782277..2bd685b2f 100644 --- a/scripts/python-phoniebox/PhonieboxRpcServer.py +++ b/scripts/python-phoniebox/PhonieboxRpcServer.py @@ -54,20 +54,18 @@ def server(self): print (client_request) - #lets make it jsonrpc https://www.jsonrpc.org/specification + #make it jsonrpc https://www.jsonrpc.org/specification ?? #{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3} #{"jsonrpc": "2.0", "result": 19, "id": 3} + #liked the object separation on that level: + #{'object':'','method':'','params':{},id:''} - #hm, overhead, + strucure, we should takeover id - - #{'object':'','method':'','params':{}} - - client_object = client_request.get('obj') #lets call it object + client_object = client_request.get('object') if (client_object != None): - client_command = client_request.get('cmd') #lets call it method + client_command = client_request.get('method') client_id = client_request.get('id') if (client_command != None): - client_param = client_request.get('param') #lets call it params + client_param = client_request.get('params') client_response['resp'] = self.execute(client_object,client_command,client_param) client_response['id'] = client_id diff --git a/scripts/python-phoniebox/PhonieboxVolume.py b/scripts/python-phoniebox/PhonieboxVolume.py index 33856ffec..1ea563415 100644 --- a/scripts/python-phoniebox/PhonieboxVolume.py +++ b/scripts/python-phoniebox/PhonieboxVolume.py @@ -5,8 +5,7 @@ class volume_control_alsa: def __init__(self): self.mixer = alsaaudio.Mixer('Master', 0) - self.volume = 0 - #self.mixer.getvolume() + self.volume = self.mixer.getvolume()[0] def get(self, param): return ({'volume':self.volume}) From 562e61f10642162ed9ea60825fa3a88ab3fee49a Mon Sep 17 00:00:00 2001 From: arne123 Date: Sun, 7 Mar 2021 22:29:29 +0100 Subject: [PATCH 008/606] switched to alsa for wave file instead of pydub --- scripts/python-phoniebox/PhonieboxDaemon.py | 18 +++---------- scripts/python-phoniebox/PhonieboxVolume.py | 28 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/scripts/python-phoniebox/PhonieboxDaemon.py b/scripts/python-phoniebox/PhonieboxDaemon.py index eb730394d..a8b15126d 100755 --- a/scripts/python-phoniebox/PhonieboxDaemon.py +++ b/scripts/python-phoniebox/PhonieboxDaemon.py @@ -33,17 +33,6 @@ def exit_gracefully(esignal, frame): print ("Exiting") sys.exit(0) - -def playstartsound(): - # import required libraries - from pydub import AudioSegment - from pydub.playback import play - - # Import an audio file - wav_file = AudioSegment.from_file(file = "../../shared/startupsound.wav", format = "wav") - play(wav_file) - - if __name__ == "__main__": # if called directly, launch Phoniebox.py as rfid-reader daemon @@ -62,16 +51,17 @@ def playstartsound(): #gpio_config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') #gpio_config.read(config_path) + volume_control_alsa = PhonieboxVolume.volume_control_alsa() + # Play Startup Sound - startsound_thread = threading.Thread(target=playstartsound) + startsound_thread = threading.Thread(target=volume_control_alsa.play_wave_file, args=["../../shared/startupsound.wav"]) startsound_thread.start() - PhonieboxVolume.list_cards() PhonieboxVolume.list_mixers({ 'cardindex': 0 }) #initialize Phonibox objcts - objects = {'volume':PhonieboxVolume.volume_control_alsa(), + objects = {'volume':volume_control_alsa, 'player':PhonieboxPlayer.player_control()} print ("Init Phonibox ZMQ Server ") diff --git a/scripts/python-phoniebox/PhonieboxVolume.py b/scripts/python-phoniebox/PhonieboxVolume.py index 1ea563415..46582a062 100644 --- a/scripts/python-phoniebox/PhonieboxVolume.py +++ b/scripts/python-phoniebox/PhonieboxVolume.py @@ -35,6 +35,34 @@ def dec(self, param): self.mixer.setvolume(self.volume) return ({'volume':self.volume}) + def play_wave_file(self,file_name): + import wave + with wave.open(file_name, 'rb') as f: + format = None + + # 8bit is unsigned in wav files + if f.getsampwidth() == 1: + format = alsaaudio.PCM_FORMAT_U8 + # Otherwise we assume signed data, little endian + elif f.getsampwidth() == 2: + format = alsaaudio.PCM_FORMAT_S16_LE + elif f.getsampwidth() == 3: + format = alsaaudio.PCM_FORMAT_S24_3LE + elif f.getsampwidth() == 4: + format = alsaaudio.PCM_FORMAT_S32_LE + else: + raise ValueError('Unsupported format') + + periodsize = f.getframerate() // 8 + + print('%d channels, %d sampling rate, format %d, periodsize %d\n' % (f.getnchannels(),f.getframerate(), format, periodsize)) + + device = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=periodsize, device="default") + + data = f.readframes(periodsize) + while data: + device.write(data) + data = f.readframes(periodsize) def list_cards(): From 5e3c61a6ae68e2aa7d4948a272324a5e2009ca1d Mon Sep 17 00:00:00 2001 From: arne123 Date: Wed, 24 Mar 2021 23:01:21 +0100 Subject: [PATCH 009/606] plyer can play songs from rfid, added FakeRfidReader --- components/rfid-reader/FakeRfidReader.py | 51 ++++++ components/rfid-reader/PhonieboxRfidReader.py | 57 ++++--- htdocs/api/zmq.php | 0 htdocs/inc.setPlayerBehaviourRFID.php | 0 htdocs/inc.setShutdownVolumeReduction.php | 0 htdocs/phpinfo.php | 0 scripts/python-phoniebox/PhonieboxDaemon.py | 71 +++++---- .../python-phoniebox/PhonieboxNvManager.py | 128 +++++++++++---- scripts/python-phoniebox/PhonieboxPlayer.py | 147 ++++++++++++++++-- settings/phoniebox_cardid_database.json | 53 +++++++ 10 files changed, 403 insertions(+), 104 deletions(-) create mode 100644 components/rfid-reader/FakeRfidReader.py mode change 100644 => 100755 htdocs/api/zmq.php mode change 100644 => 100755 htdocs/inc.setPlayerBehaviourRFID.php mode change 100644 => 100755 htdocs/inc.setShutdownVolumeReduction.php mode change 100644 => 100755 htdocs/phpinfo.php create mode 100644 settings/phoniebox_cardid_database.json diff --git a/components/rfid-reader/FakeRfidReader.py b/components/rfid-reader/FakeRfidReader.py new file mode 100644 index 000000000..e2c24dd72 --- /dev/null +++ b/components/rfid-reader/FakeRfidReader.py @@ -0,0 +1,51 @@ +from tkinter import * +import time + +class FakeReader: + def __init__(self): + self._keep_running = True + self.root = None + self.card_id = None + self.card_id_prev = None + + def create_ui(self): + self.root = Tk() + self.root.title("Fake RFID Reader") + + # Add a grid + self.mainframe = Frame(self.root) + self.mainframe.grid(column=0,row=0, sticky=(N,W,E,S) ) + self.mainframe.columnconfigure(0, weight = 1) + self.mainframe.rowconfigure(0, weight = 1) + self.mainframe.pack(pady = 100, padx = 100) + + # Create a Tkinter variable + self.tkvar = StringVar(self.root) + + popupMenu = OptionMenu(self.mainframe, self.tkvar, *self.card_ids) + Label(self.mainframe, text="Choose a RFID Card Number").grid(row = 1, column = 1) + popupMenu.grid(row = 2, column =1) + + # link function to change dropdown + self.tkvar.trace('w', self.change_dropdown) + + # on change dropdown value + def change_dropdown(self,*args): + self.card_id = self.tkvar.get() + + def set_card_ids(self, card_ids): + self.card_ids = card_ids + + def readCard(self): + if self.root is None: + self.create_ui() + + while self.card_id == self.card_id_prev: + if self.root is not None: + self.root.update() + time.sleep(0.1) + + self.card_id_prev = self.card_id + + return self.card_id + diff --git a/components/rfid-reader/PhonieboxRfidReader.py b/components/rfid-reader/PhonieboxRfidReader.py index 04fcad442..c05d57a2f 100644 --- a/components/rfid-reader/PhonieboxRfidReader.py +++ b/components/rfid-reader/PhonieboxRfidReader.py @@ -8,10 +8,13 @@ import os.path import sys -import RPi.GPIO as GPIO +#import RPi.GPIO as GPIO import logging -from evdev import InputDevice, categorize, ecodes, list_devices +sys.path.insert(0,'../') +from phonie_access_objects import phoniebox_object_access_queue + +#from evdev import InputDevice, categorize, ecodes, list_devices logger = logging.getLogger(__name__) @@ -60,6 +63,9 @@ def __init__(self,device_name,param=None): elif device_name == 'PN532': import RfidReader_PN532 self.reader = Pn532Reader() + elif device_name == 'Fake': + from FakeRfidReader import FakeReader + self.reader = FakeReader() else: try: device = [device for device in get_devices() if device.name == device_name][0] @@ -70,32 +76,33 @@ def __init__(self,device_name,param=None): self.queue = phoniebox_object_access_queue() self.queue.connect() self._keep_running = True - - + self.cardnotification = None + self.valid_cardnotification = None + self.invalid_cardnotification = None def set_cardid_db(self,cardid_db): ##potentially dangerous for runtime updates, needs look? if cardid_db != None: self.cardid_db = cardid_db - def get_card_assignment(self,cardid) + def get_card_assignment(self,cardid): ##potentially dangerous for runtime updates, needs look? - card_assignment = self.cardid_db.get(cardid) + return self.cardid_db.get(cardid) def get_last_card_id(self): return self.last_card_id - def set_cardnotification(self, callback) - if is callable(callback): + def set_cardnotification(self, callback): + if callable(callback): self.cardnotification = callback - def set_valid_cardnotification(self, callback) - if is callable(callback): + def set_valid_cardnotification(self, callback): + if callable(callback): self.valid_cardnotification = callback - def set_invalid_cardnotification(self, callback) - if is callable(callback): + def set_invalid_cardnotification(self, callback): + if callable(callback): self.invalid_cardnotification = callback @@ -106,17 +113,17 @@ def run(self): self._keep_running = True ##card_detection_sound = self.get_setting("phoniebox", "card_detection_sound") <-- this module should not deciede about sound - debounce_time = self.get_setting("phoniebox", "debounce_time") - if debounce_time == -1: - debounce_time = 0.5 - second_swipe_delay = self.get_setting("phoniebox", "second_swipe_delay") - if second_swipe_delay == -1: - second_swipe_delay = 0 - store_card_assignments = self.get_setting("phoniebox", "store_card_assignments") - if store_card_assignments == -1: - store_card_assignments = 30 - last_swipe = 0 - last_write_card_assignments = 0 + #debounce_time = self.get_setting("phoniebox", "debounce_time") + #if debounce_time == -1: + # debounce_time = 0.5 + #second_swipe_delay = self.get_setting("phoniebox", "second_swipe_delay") + #if second_swipe_delay == -1: + # second_swipe_delay = 0 + #store_card_assignments = self.get_setting("phoniebox", "store_card_assignments") + #if store_card_assignments == -1: + # store_card_assignments = 30 + #last_swipe = 0 + #last_write_card_assignments = 0 ## who does know about card ids? ##the reader? @@ -127,7 +134,7 @@ def run(self): ## -> Reader knows about IDs eecuitng a command, everything else is treted -> maybe while self._keep_running: #since readCard is a blocking call, this will not work - cardid = reader.reader.readCard() + cardid = self.reader.readCard() self.last_card_id = cardid if self.cardnotification is not None: @@ -137,7 +144,7 @@ def run(self): if card_assignment is not None: - #probably deal with 2nd wipe here + #probably deal with 2nd swipe here if self.valid_cardnotification is not None: self.valid_cardnotification() diff --git a/htdocs/api/zmq.php b/htdocs/api/zmq.php old mode 100644 new mode 100755 diff --git a/htdocs/inc.setPlayerBehaviourRFID.php b/htdocs/inc.setPlayerBehaviourRFID.php old mode 100644 new mode 100755 diff --git a/htdocs/inc.setShutdownVolumeReduction.php b/htdocs/inc.setShutdownVolumeReduction.php old mode 100644 new mode 100755 diff --git a/htdocs/phpinfo.php b/htdocs/phpinfo.php old mode 100644 new mode 100755 diff --git a/scripts/python-phoniebox/PhonieboxDaemon.py b/scripts/python-phoniebox/PhonieboxDaemon.py index a8b15126d..0e654a575 100755 --- a/scripts/python-phoniebox/PhonieboxDaemon.py +++ b/scripts/python-phoniebox/PhonieboxDaemon.py @@ -6,15 +6,18 @@ import signal from time import sleep, time -#import gpio_control - import PhonieboxVolume import PhonieboxPlayer from PhonieboxRpcServer import phoniebox_rpc_server +from PhonieboxNvManager import nv_manager + +sys.path.insert(0,'../../components/gpio_control') +sys.path.insert(0,'../../components/rfid-reader') +#print(sys.path) +from PhonieboxRfidReader import RFID_Reader +#import gpio_control -# get absolute path of this script -dir_path = os.path.dirname(os.path.realpath(__file__)) -defaultconfigFilePath = os.path.join(dir_path, 'phoniebox.conf') +g_nvm = None def signal_handler(signal, frame): """ catches signal and triggers the graceful exit """ @@ -27,6 +30,7 @@ def exit_gracefully(esignal, frame): #stop all threads #save all nv + g_nvm.save_all() #play stop (maybe) #shutdown () @@ -35,6 +39,9 @@ def exit_gracefully(esignal, frame): if __name__ == "__main__": + # get absolute path of this script + dir_path = os.path.dirname(os.path.realpath(__file__)) + defaultconfigFilePath = os.path.join(dir_path, 'phoniebox.conf') # if called directly, launch Phoniebox.py as rfid-reader daemon # treat the first argument as defaultconfigFilePath if given if len(sys.argv) <= 1: @@ -42,52 +49,50 @@ def exit_gracefully(esignal, frame): else: configFilePath = sys.argv[1] - #sys.path.append(parentdir+"/scripts") - sys.path.insert(0,'../../gpio_control') - print(sys.path) - #parse config #gpio_config = configparser.ConfigParser(inline_comment_prefixes=";") #gpio_config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') #gpio_config.read(config_path) - volume_control_alsa = PhonieboxVolume.volume_control_alsa() - + #read config to dictionary? + phoniebox_config = {} + phoniebox_config['audiofolders_path'] = "../../shared/" + # Play Startup Sound + volume_control_alsa = PhonieboxVolume.volume_control_alsa() startsound_thread = threading.Thread(target=volume_control_alsa.play_wave_file, args=["../../shared/startupsound.wav"]) startsound_thread.start() - PhonieboxVolume.list_cards() - PhonieboxVolume.list_mixers({ 'cardindex': 0 }) + #Debug: List ALSA cards and mixers + #PhonieboxVolume.list_cards() + #PhonieboxVolume.list_mixers({ 'cardindex': 0 }) + + g_nvm = nv_manager() + + + #phoniebox music player status + music_player_status = g_nvm.load("../../shared/music_player_status.json") + + #card id database + cardid_database = g_nvm.load("../../settings/phoniebox_cardid_database.json") #initialize Phonibox objcts objects = {'volume':volume_control_alsa, - 'player':PhonieboxPlayer.player_control()} + 'player':PhonieboxPlayer.player_control(music_player_status)} - print ("Init Phonibox ZMQ Server ") + print ("Init Phonibox RPC Server ") rpcs = phoniebox_rpc_server(objects) if rpcs != None: rpcs.connect() - #pc_t = threading.Thread(target=pc.process_queue) - #pc_t.start() - - - ##rfid - #card id will be linked directly with object call which are feeded into the mq - cardid_db = {'104,49914':{'object':'','method':'','params':{}}, - '103,12632':{'object':'','method':'','params':{}}, - '104,29698':{'object':'','method':'','params':{}}, - '108,07437':{'object':'','method':'','params':{}}, - '107,60360':{'object':'','method':'','params':{}}, - '106,64513':{'object':'','method':'','params':{}}, - '104,14891':{'object':'','method':'','params':{}}, - '103,24033':{'object':'','method':'','params':{}}, - '104,32860':{'object':'','method':'','params':{}} } #rfid_reader = RFID_Reader("RDM6300",{'numberformat':'card_id_float'}) - rfid_reader = None + rfid_reader = RFID_Reader("Fake") + #rfid_reader = None if rfid_reader is not None: - rfid_reader.set_cardid_db() + print ("set db") + rfid_reader.set_cardid_db(cardid_database) + rfid_reader.reader.set_card_ids(list(cardid_database)) #just for Fake Reader + print ("set thread") rfid_thread = threading.Thread(target=rfid_reader.run) #rfid_t.start() else: @@ -122,5 +127,5 @@ def exit_gracefully(esignal, frame): if rfid_thread is not None: print ("Starting RFID Thread") rfid_thread.start() - print ("Starting ZMQ Server") + print ("Starting RPC Server") rpcs.server() \ No newline at end of file diff --git a/scripts/python-phoniebox/PhonieboxNvManager.py b/scripts/python-phoniebox/PhonieboxNvManager.py index 5f7a0d9fb..efe4b8adc 100644 --- a/scripts/python-phoniebox/PhonieboxNvManager.py +++ b/scripts/python-phoniebox/PhonieboxNvManager.py @@ -1,48 +1,112 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import json +import os -#Non Volatile data storage -# to avoid frequent file system writes to prevent sd card wereout this module should take care of non volatile data and their storage -# preferably as human readable, probaly json -# the idea is that submodules create an nv object, for data they like to store -#the Idea is just to handle dictionaries, do we need data type awareness here? -#this could offer the option, that instead of a central configuration, each subclass can handle its configuration on its own, with setter / getter methods -class nv_object(): - __instance = None - @staticmethod - def getInstance(): - """ Static access method. """ - if nv_object.__instance == None: - nv_object() - return nv_object.__instance +class nv_manager(): def __init__(self): - """ Virtually private constructor. """ - if nv_object.__instance != None: - raise Exception("This class is a singleton!") - else: - nv_object.__instance = self + self.all_nv_objects = [] + def load(self,default_filename,type=None): + new_nv_dict = nv_dict(default_filename) + self.all_nv_objects.append(new_nv_dict) + return new_nv_dict + + def save_all(self): + for nv_obj in self.all_nv_objects: + #print ("Saving {} ".format(nv_obj)) + nv_obj.save_to_json() - nv_manager register new object - def get() + - sef set(val) +# Todo: add hashing support +# this is inherting from dict +# you should not do this! +# In this case it is ok since we are just adding methods, and are not overwriting any parent methods +class nv_dict(dict): + def __init__(self,default_filename=None): + super().__init__() + + self.default_filename=default_filename -class nv_manager - __init__(file_name) + if self.default_filename is not None: + if (os.path.isfile(self.default_filename) and os.access(self.default_filename, os.R_OK)): + self.init_from_json(self.default_filename) + self.default_filename_is_writeable = os.access(self.default_filename, os.W_OK) + else: + print ("File {} does not exist, creating it".format(self.default_filename)) + #self['nv_dict_version'] = 1.0 + self.default_filename_is_writeable = True + self.save_to_json() + self.dirty = False + + def __setitem__(self, item, value): + self.dirty = True #not working, see comment above, intention is to just write dicts which content has changed + super(nv_dict,self).__setitem__(item, value) + + def init_from_json(self,path=None,merge=False): + if merge is False: + self.clear() + self.update(json.load( open( path ) )) - def register_new_nv_object: + def save_to_json(self,filename=None): + #if self.dirty is True: + print ("Saving {} ".format(self)) + save_to_filename = None + if filename is not None: + if os.access(filename, os.W_OK) : + save_to_filename = filename + else : + if (self.default_filename_is_writeable): + save_to_filename = self.default_filename + + if save_to_filename is not None: + json.dump( self, open( save_to_filename, 'w' ), indent=2) + self.dirty = False + #do we need to close ? + +if __name__ == "__main__": + + nvd = nv_dict() + + nvd['a'] = 1 + nvd['b'] = 2 + + nvd['c'] = {'abc':123} + + nvd.save_to_json("test.json") + nvd['d'] = {'abc':987} #will not be stored in test + + nvd.save_to_json("test2.json") + + print (nvd) + + nvd.init_from_json("test.json") + + print (nvd) + + nvd2 = nv_dict(default_filename="test2.json") + nvd2['b'] = {"xyz":"a string"} + nvd2.save_to_json() + + nvd3 = nv_dict(default_filename="test3.json") + + + + nvm = nv_manager() + + nvd4 =nvm.load("test4.json") + nvd4['a'] = 'A' + + print (nvm.all_nv_objects) - def set_storage_frequncey: + nvd5 =nvm.load("test5.json") + nvd5['B'] = 'B' - read - # Read data from file: - data = json.load( open( "file_name.json" ) ) + print (nvm.all_nv_objects) - write - # Serialize data into file: - json.dump( data, open( "file_name.json", 'w' ) ) \ No newline at end of file + nvm.save_all() + \ No newline at end of file diff --git a/scripts/python-phoniebox/PhonieboxPlayer.py b/scripts/python-phoniebox/PhonieboxPlayer.py index a2e11c9f5..1ea978c37 100644 --- a/scripts/python-phoniebox/PhonieboxPlayer.py +++ b/scripts/python-phoniebox/PhonieboxPlayer.py @@ -4,7 +4,13 @@ from mpd import MPDClient class player_control: - def __init__(self): + def __init__(self,music_player_status): + self.music_player_status = music_player_status + if not self.music_player_status: + self.music_player_status['player_status'] = {} + self.music_player_status['audio_folder_status'] = {} + self.music_player_status.save_to_json() + self.mpd_client = MPDClient() # create client object self.mpd_client.timeout = 0.5 # network timeout in seconds (floats allowed), default: None self.mpd_client.idletimeout = 0.5 # timeout for fetching the result of the idle command is handled seperately, default: None @@ -43,6 +49,11 @@ def play(self, param): def get_current_song(self, param): return {'resp': self.mpd_client.currentsong()} + def map_filename_to_playlist_pos(self,filename): + print ("map_filename_to_playlist_pos not yet implemented") + #self.mpd_client.playlistfind() + return 0 + def playlistaddplay(self, param): # add to playlist (and play) @@ -54,19 +65,66 @@ def playlistaddplay(self, param): # Read the current config file (include will execute == read) #. "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" - # load playlist - #mpc clear - #mpc load "${VALUE//\//SLASH}" - #if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "mpc load "${VALUE//\//SLASH} >> ${PATHDATA}/../logs/debug.log; fi + folder = param.get("folder") + + if folder is not None: + # load playlist + #mpc clear + self.mpd_client.clear() + #mpc load "${VALUE//\//SLASH}" + #why dealing with playlists? at least partially redundant with folder.config, so why not combine if needed + #alternative solution, just add folders recursively to quene + self.mpd_client.add(folder) + self.music_player_status['player_status']['last_played_folder'] = folder + + current_status = self.music_player_status['audio_folder_status'].get(folder) + if current_status is None: + current_status = self.music_player_status['audio_folder_status'][folder] = {} + + self.mpd_client.play() + + + if 0: # Change some settings according to current folder IF the folder.conf exists #. ${PATHDATA}/inc.settingsFolderSpecific.sh # check if we switch to single file playout - #${PATHDATA}/single_play.sh -c=single_check -d="${FOLDER}" + ##${PATHDATA}/single_play.sh -c=single_check -d="${FOLDER}" + #single_check) + #Check if SINGLE is switched on. As this is called for each playlist change, it will overwrite temporary shuffle mode + #if [ $SINGLE == "ON" ] + #then + # mpc single on + #else + # mpc single off + #fi + + if currecnt_status["SINGLE"] == "OFF": + self.mpd_client.single(0) + else: + self.mpd_client.single(1) + # check if we shuffle the playlist #${PATHDATA}/shuffle_play.sh -c=shuffle_check -d="${FOLDER}" + #shuffle_check) + ##Check if SHUFFLE is switched on. As this is called for each playlist change, it will overwrite temporary shuffle mode + #if [ $SHUFFLE == "ON" ]; + #then + # mpc shuffle + #else + # mpc random off + #fi + #;; + + if currecnt_status["SHUFFLE"] == "OFF": + self.mpd_client.random(0) + else: + self.mpd_client.shuffle() + + ## oh, player controls volume + #need a setter for mute/unmute from volume class # Unmute if muted #if [ -f $VOLFILE ]; then @@ -79,17 +137,78 @@ def playlistaddplay(self, param): # rm -f $VOLFILE #fi + # Now load and play - #if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "mpc load "${VALUE//\//SLASH}" && ${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}"" >> ${PATHDATA}/../logs/debug.log; fi - #${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}" + #${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}" + + ## Check if RESUME is switched on + #if [ $RESUME == "ON" ] || [ $SINGLE == "ON" ]; + #then + # + # # Check if we got a "savepos" command after the last "resume". Otherwise we assume that the playlist was played until the end. + # # In this case, start the playlist from beginning + # if [ $PLAYSTATUS == "Stopped" ] + # then + # # Get the playlist position of the file from mpd + # # Alternative approach: "mpc searchplay xx && mpc seek yy" + # PLAYLISTPOS=$(echo -e playlistfind filename \"$CURRENTFILENAME\"\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=Pos: ).*') + # + # # If the file is found, it is played from ELAPSED, otherwise start playlist from beginning. If we got a playlist position + # # play from that position, not the saved one. + # if [ ! -z $PLAYLISTPOS ] && [ -z $VALUE ] ; + # then + # # doesnt work correctly + # # echo -e seek $PLAYLISTPOS $ELAPSED \\nclose | nc -w 1 localhost 6600 + # # workaround, see https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/878#issuecomment-672283454 + # echo -e "play $PLAYLISTPOS" | nc -w 1 localhost 6600 + # echo -e seekcur $ELAPSED \\nclose | nc -w 1 localhost 6600 + # else + # echo -e "play $VALUE" | nc -w 1 localhost 6600 + # fi + # # If the playlist ends without any stop/shutdown/new swipe (you've listened to all of the tracks), + # # there's no savepos event and we would resume at the last position anywhere in the playlist. + # # To catch these, we signal it to the next "resume" call via writing it to folder.conf that + # # we still assume that the audio is playing. + # # be anything here, as we won't use the information if "Playing" is found by "resume". + # + # # set the vars we need to change + # PLAYSTATUS="Playing" + # + # else + # # We assume that the playlist ran to the end the last time and start from the beginning. + # # Or: playlist is playing and we've got a play from playlist position command. + # echo -e "play $VALUE" | nc -w 1 localhost 6600 + # fi + #else + # # if no last played data exists (resume play disabled), we play the playlist from the beginning or the given playlist position + # echo -e "play $VALUE" | nc -w 1 localhost 6600 + #fi + + if currecnt_status["RESUME"] is not "OFF" or currecnt_status["SINGLE"] is not "OFF": + if currecnt_status["PLAYSTATUS"] is "Stopped": + + self.map_filename_to_playlist_pos() + + #PLAYLISTPOS=$(echo -e playlistfind filename \"$CURRENTFILENAME\"\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=Pos: ).*') + + currecnt_status["PLAYSTATUS"] = "Playing" + else: + self.mpd_client.play() # what is in value here? songpos + else: + #Begins playing the playlist at song number SONGPOS. + self.mpd_client.play() # what is in value here? songpos # write latest folder played to settings file #sudo echo ${FOLDER} > ${PATHDATA}/../settings/Latest_Folder_Played - #sudo chown pi:www-data ${PATHDATA}/../settings/Latest_Folder_Played - #sudo chmod 777 ${PATHDATA}/../settings/Latest_Folder_Played - - self.mpd_client.add(uri) - self.mpd_client.load(name[123, start:end]) - + song = self.mpd_client.currentsong() + + current_status["CURRENTFILENAME"] = song.get('file') + current_status["ELAPSED"] = 0 + current_status["PLAYSTATUS"] = "Stopped" + current_status["RESUME"] = "OFF" + current_status["SHUFFLE"] = "OFF" + current_status["LOOP"] = "OFF" + current_status["SINGLE"] = "OFF" + return ({'song':song}) \ No newline at end of file diff --git a/settings/phoniebox_cardid_database.json b/settings/phoniebox_cardid_database.json new file mode 100644 index 000000000..b350f0fdb --- /dev/null +++ b/settings/phoniebox_cardid_database.json @@ -0,0 +1,53 @@ +{ + "104,49914": { + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "a/acdc" + } + }, + "103,12632": { + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "r/roxette" + } + }, + "104,29698": { + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "d/digger barnes/every story true" + } + }, + "108,07437": { + "object": "", + "method": "", + "params": {} + }, + "107,60360": { + "object": "", + "method": "", + "params": {} + }, + "106,64513": { + "object": "", + "method": "", + "params": {} + }, + "104,14891": { + "object": "", + "method": "", + "params": {} + }, + "103,24033": { + "object": "", + "method": "", + "params": {} + }, + "104,32860": { + "object": "", + "method": "", + "params": {} + } +} \ No newline at end of file From 43fbb9e4bd5d5acaab6aa3ca985443c313bd7fed Mon Sep 17 00:00:00 2001 From: arne123 Date: Wed, 7 Apr 2021 23:15:07 +0200 Subject: [PATCH 010/606] reorganized python code in a package living in components (for now) --- .../PhonieboxDaemon.py | 22 ++++------ .../PhonieboxNvManager.py | 41 +++++++++++-------- .../PhonieboxVolume.py | 0 .../player/PhonieboxPlayerMPD.py | 22 +++++++++- .../{rfid-reader => player}/__init__.py | 0 .../FakeRfidReader.py | 0 .../PN532/README.md | 0 .../PN532/requirements.txt | 0 .../PN532/reset_pn532.sh | 0 .../PN532/setup_pn532.sh | 0 .../PhonieboxRfidReader.py | 11 +++-- .../RC522/README.md | 0 .../RC522/requirements.txt | 0 .../RC522/setup_rc522.sh | 0 .../RfidReader_PN532.py | 0 .../RfidReader_RC522.py | 0 .../RfidReader_RDM6300.py | 0 components/rfid_reader/__init__.py | 0 .../rpc/PhonieboxRpcClient.py | 0 .../rpc}/PhonieboxRpcServer.py | 8 ++-- components/rpc/__init__.py | 0 settings/phoniebox_cardid_database.json | 6 +-- 22 files changed, 64 insertions(+), 46 deletions(-) rename {scripts/python-phoniebox => components}/PhonieboxDaemon.py (85%) rename {scripts/python-phoniebox => components}/PhonieboxNvManager.py (74%) rename {scripts/python-phoniebox => components}/PhonieboxVolume.py (100%) rename scripts/python-phoniebox/PhonieboxPlayer.py => components/player/PhonieboxPlayerMPD.py (93%) rename components/{rfid-reader => player}/__init__.py (100%) rename components/{rfid-reader => rfid_reader}/FakeRfidReader.py (100%) rename components/{rfid-reader => rfid_reader}/PN532/README.md (100%) rename components/{rfid-reader => rfid_reader}/PN532/requirements.txt (100%) rename components/{rfid-reader => rfid_reader}/PN532/reset_pn532.sh (100%) rename components/{rfid-reader => rfid_reader}/PN532/setup_pn532.sh (100%) rename components/{rfid-reader => rfid_reader}/PhonieboxRfidReader.py (96%) rename components/{rfid-reader => rfid_reader}/RC522/README.md (100%) rename components/{rfid-reader => rfid_reader}/RC522/requirements.txt (100%) rename components/{rfid-reader => rfid_reader}/RC522/setup_rc522.sh (100%) rename components/{rfid-reader => rfid_reader}/RfidReader_PN532.py (100%) rename components/{rfid-reader => rfid_reader}/RfidReader_RC522.py (100%) rename components/{rfid-reader => rfid_reader}/RfidReader_RDM6300.py (100%) create mode 100644 components/rfid_reader/__init__.py rename scripts/phonie_access_objects.py => components/rpc/PhonieboxRpcClient.py (100%) rename {scripts/python-phoniebox => components/rpc}/PhonieboxRpcServer.py (94%) create mode 100644 components/rpc/__init__.py diff --git a/scripts/python-phoniebox/PhonieboxDaemon.py b/components/PhonieboxDaemon.py similarity index 85% rename from scripts/python-phoniebox/PhonieboxDaemon.py rename to components/PhonieboxDaemon.py index 0e654a575..723dd9e83 100755 --- a/scripts/python-phoniebox/PhonieboxDaemon.py +++ b/components/PhonieboxDaemon.py @@ -7,15 +7,11 @@ from time import sleep, time import PhonieboxVolume -import PhonieboxPlayer -from PhonieboxRpcServer import phoniebox_rpc_server +from player import PhonieboxPlayerMPD +from rpc.PhonieboxRpcServer import phoniebox_rpc_server from PhonieboxNvManager import nv_manager - -sys.path.insert(0,'../../components/gpio_control') -sys.path.insert(0,'../../components/rfid-reader') -#print(sys.path) -from PhonieboxRfidReader import RFID_Reader -#import gpio_control +from rfid_reader.PhonieboxRfidReader import RFID_Reader +#from gpio_control import gpio_control g_nvm = None @@ -56,11 +52,11 @@ def exit_gracefully(esignal, frame): #read config to dictionary? phoniebox_config = {} - phoniebox_config['audiofolders_path'] = "../../shared/" + phoniebox_config['audiofolders_path'] = "../shared/" # Play Startup Sound volume_control_alsa = PhonieboxVolume.volume_control_alsa() - startsound_thread = threading.Thread(target=volume_control_alsa.play_wave_file, args=["../../shared/startupsound.wav"]) + startsound_thread = threading.Thread(target=volume_control_alsa.play_wave_file, args=["../shared/startupsound.wav"]) startsound_thread.start() #Debug: List ALSA cards and mixers @@ -71,14 +67,14 @@ def exit_gracefully(esignal, frame): #phoniebox music player status - music_player_status = g_nvm.load("../../shared/music_player_status.json") + music_player_status = g_nvm.load("../shared/music_player_status.json") #card id database - cardid_database = g_nvm.load("../../settings/phoniebox_cardid_database.json") + cardid_database = g_nvm.load("../settings/phoniebox_cardid_database.json") #initialize Phonibox objcts objects = {'volume':volume_control_alsa, - 'player':PhonieboxPlayer.player_control(music_player_status)} + 'player':PhonieboxPlayerMPD.player_control(music_player_status)} print ("Init Phonibox RPC Server ") rpcs = phoniebox_rpc_server(objects) diff --git a/scripts/python-phoniebox/PhonieboxNvManager.py b/components/PhonieboxNvManager.py similarity index 74% rename from scripts/python-phoniebox/PhonieboxNvManager.py rename to components/PhonieboxNvManager.py index efe4b8adc..868a56991 100644 --- a/scripts/python-phoniebox/PhonieboxNvManager.py +++ b/components/PhonieboxNvManager.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import json import os - +import hashlib class nv_manager(): def __init__(self): self.all_nv_objects = [] @@ -40,7 +40,7 @@ def __init__(self,default_filename=None): #self['nv_dict_version'] = 1.0 self.default_filename_is_writeable = True self.save_to_json() - self.dirty = False + self.initial_hash = self.hash() def __setitem__(self, item, value): self.dirty = True #not working, see comment above, intention is to just write dicts which content has changed @@ -51,22 +51,29 @@ def init_from_json(self,path=None,merge=False): self.clear() self.update(json.load( open( path ) )) + def hash(self): + return hashlib.md5(json.dumps(self).encode('UTF8')).digest() + def save_to_json(self,filename=None): - #if self.dirty is True: - print ("Saving {} ".format(self)) - - save_to_filename = None - if filename is not None: - if os.access(filename, os.W_OK) : - save_to_filename = filename - else : - if (self.default_filename_is_writeable): - save_to_filename = self.default_filename - - if save_to_filename is not None: - json.dump( self, open( save_to_filename, 'w' ), indent=2) - self.dirty = False - #do we need to close ? + actual_hash = self.hash() + if self.initial_hash != actual_hash: + self.initial_hash = actual_hash + print ("Saving {} ".format(self)) + + save_to_filename = None + if filename is not None: + if os.access(filename, os.W_OK) : + save_to_filename = filename + else : + if (self.default_filename_is_writeable): + save_to_filename = self.default_filename + + if save_to_filename is not None: + with open(save_to_filename, 'w') as outfile: + json.dump( self, outfile, indent=2) + else: + print ("dont need to save") + if __name__ == "__main__": diff --git a/scripts/python-phoniebox/PhonieboxVolume.py b/components/PhonieboxVolume.py similarity index 100% rename from scripts/python-phoniebox/PhonieboxVolume.py rename to components/PhonieboxVolume.py diff --git a/scripts/python-phoniebox/PhonieboxPlayer.py b/components/player/PhonieboxPlayerMPD.py similarity index 93% rename from scripts/python-phoniebox/PhonieboxPlayer.py rename to components/player/PhonieboxPlayerMPD.py index 1ea978c37..11ca3f17b 100644 --- a/scripts/python-phoniebox/PhonieboxPlayer.py +++ b/components/player/PhonieboxPlayerMPD.py @@ -20,7 +20,7 @@ def __init__(self,music_player_status): def connect(self): self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 - def _mpd_retry(self,mpd_cmd,params=None): + def mpd_retry(self,mpd_cmd,params=None): try: ret = mpd_cmd(params) except ConnectionError: @@ -67,6 +67,8 @@ def playlistaddplay(self, param): folder = param.get("folder") + print("playing folder: {}".format(folder)) + if folder is not None: # load playlist #mpc clear @@ -211,4 +213,20 @@ def playlistaddplay(self, param): current_status["LOOP"] = "OFF" current_status["SINGLE"] = "OFF" - return ({'song':song}) \ No newline at end of file + return ({'song':song}) + + + def playerstop(self,param): + status = self.mpd_client.status() + #current_status["ELAPSED"] = status.get('elapsed') + + # stop the player + self.mpd_client.stop() + #if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi + return ({}) + + def playerstatus(self,param): + status = self.mpd_client.status() + for k in status: + print ("{} : {}".format(k,status.get(k))) + return (status) \ No newline at end of file diff --git a/components/rfid-reader/__init__.py b/components/player/__init__.py similarity index 100% rename from components/rfid-reader/__init__.py rename to components/player/__init__.py diff --git a/components/rfid-reader/FakeRfidReader.py b/components/rfid_reader/FakeRfidReader.py similarity index 100% rename from components/rfid-reader/FakeRfidReader.py rename to components/rfid_reader/FakeRfidReader.py diff --git a/components/rfid-reader/PN532/README.md b/components/rfid_reader/PN532/README.md similarity index 100% rename from components/rfid-reader/PN532/README.md rename to components/rfid_reader/PN532/README.md diff --git a/components/rfid-reader/PN532/requirements.txt b/components/rfid_reader/PN532/requirements.txt similarity index 100% rename from components/rfid-reader/PN532/requirements.txt rename to components/rfid_reader/PN532/requirements.txt diff --git a/components/rfid-reader/PN532/reset_pn532.sh b/components/rfid_reader/PN532/reset_pn532.sh similarity index 100% rename from components/rfid-reader/PN532/reset_pn532.sh rename to components/rfid_reader/PN532/reset_pn532.sh diff --git a/components/rfid-reader/PN532/setup_pn532.sh b/components/rfid_reader/PN532/setup_pn532.sh similarity index 100% rename from components/rfid-reader/PN532/setup_pn532.sh rename to components/rfid_reader/PN532/setup_pn532.sh diff --git a/components/rfid-reader/PhonieboxRfidReader.py b/components/rfid_reader/PhonieboxRfidReader.py similarity index 96% rename from components/rfid-reader/PhonieboxRfidReader.py rename to components/rfid_reader/PhonieboxRfidReader.py index c05d57a2f..799f5408c 100644 --- a/components/rfid-reader/PhonieboxRfidReader.py +++ b/components/rfid_reader/PhonieboxRfidReader.py @@ -11,8 +11,7 @@ #import RPi.GPIO as GPIO import logging -sys.path.insert(0,'../') -from phonie_access_objects import phoniebox_object_access_queue +from rpc.PhonieboxRpcClient import phoniebox_object_access_queue #from evdev import InputDevice, categorize, ecodes, list_devices @@ -55,16 +54,16 @@ class RFID_Reader(object): def __init__(self,device_name,param=None): if device_name == 'MFRC522': - import RfidReader_RC522 + from . import RfidReader_RC522 self.reader = Mfrc522Reader() elif device_name == 'RDM6300': - import RfidReader_RDM6300 + from . import RfidReader_RDM6300 self.reader = Rdm6300Reader(param) elif device_name == 'PN532': - import RfidReader_PN532 + from . import RfidReader_PN532 self.reader = Pn532Reader() elif device_name == 'Fake': - from FakeRfidReader import FakeReader + from .FakeRfidReader import FakeReader self.reader = FakeReader() else: try: diff --git a/components/rfid-reader/RC522/README.md b/components/rfid_reader/RC522/README.md similarity index 100% rename from components/rfid-reader/RC522/README.md rename to components/rfid_reader/RC522/README.md diff --git a/components/rfid-reader/RC522/requirements.txt b/components/rfid_reader/RC522/requirements.txt similarity index 100% rename from components/rfid-reader/RC522/requirements.txt rename to components/rfid_reader/RC522/requirements.txt diff --git a/components/rfid-reader/RC522/setup_rc522.sh b/components/rfid_reader/RC522/setup_rc522.sh similarity index 100% rename from components/rfid-reader/RC522/setup_rc522.sh rename to components/rfid_reader/RC522/setup_rc522.sh diff --git a/components/rfid-reader/RfidReader_PN532.py b/components/rfid_reader/RfidReader_PN532.py similarity index 100% rename from components/rfid-reader/RfidReader_PN532.py rename to components/rfid_reader/RfidReader_PN532.py diff --git a/components/rfid-reader/RfidReader_RC522.py b/components/rfid_reader/RfidReader_RC522.py similarity index 100% rename from components/rfid-reader/RfidReader_RC522.py rename to components/rfid_reader/RfidReader_RC522.py diff --git a/components/rfid-reader/RfidReader_RDM6300.py b/components/rfid_reader/RfidReader_RDM6300.py similarity index 100% rename from components/rfid-reader/RfidReader_RDM6300.py rename to components/rfid_reader/RfidReader_RDM6300.py diff --git a/components/rfid_reader/__init__.py b/components/rfid_reader/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/phonie_access_objects.py b/components/rpc/PhonieboxRpcClient.py similarity index 100% rename from scripts/phonie_access_objects.py rename to components/rpc/PhonieboxRpcClient.py diff --git a/scripts/python-phoniebox/PhonieboxRpcServer.py b/components/rpc/PhonieboxRpcServer.py similarity index 94% rename from scripts/python-phoniebox/PhonieboxRpcServer.py rename to components/rpc/PhonieboxRpcServer.py index 2bd685b2f..a1a194d4a 100644 --- a/scripts/python-phoniebox/PhonieboxRpcServer.py +++ b/components/rpc/PhonieboxRpcServer.py @@ -28,13 +28,11 @@ def execute(self, obj, cmd, param): call_function = getattr(call_obj,cmd,None) if (call_function is not None): # is callable() ?? response = call_function(param) - print (response) else: response = {'resp': "no valid commad"} - print (response) else: response = {'resp': "no valid obj"} - print (response) + #print (response) return response def terminate(self): @@ -52,7 +50,7 @@ def server(self): client_request=json.loads(message) client_response = {} - print (client_request) + #print (client_request) #make it jsonrpc https://www.jsonrpc.org/specification ?? #{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3} @@ -74,7 +72,7 @@ def server(self): client_response['total_processing_time'] = (nt - int(client_request['tsp'])) / 1000000 print ("processing time: {:2.3f} ms".format(client_response['total_processing_time'])) - print(client_response) + #print(client_response) # Send reply back to client self.socket.send_string(json.dumps(client_response)) diff --git a/components/rpc/__init__.py b/components/rpc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/settings/phoniebox_cardid_database.json b/settings/phoniebox_cardid_database.json index b350f0fdb..d3bedefae 100644 --- a/settings/phoniebox_cardid_database.json +++ b/settings/phoniebox_cardid_database.json @@ -3,21 +3,21 @@ "object": "player", "method": "playlistaddplay", "params": { - "folder": "a/acdc" + "folder": "acdc" } }, "103,12632": { "object": "player", "method": "playlistaddplay", "params": { - "folder": "r/roxette" + "folder": "roxette" } }, "104,29698": { "object": "player", "method": "playlistaddplay", "params": { - "folder": "d/digger barnes/every story true" + "folder": "digger barnes/every story true" } }, "108,07437": { From fdf04f8cdd9a91b13abdfa0dba139ac5d6742fe0 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sat, 10 Apr 2021 00:23:56 +0200 Subject: [PATCH 011/606] implemented most of web player vio zmq --- components/PhonieboxDaemon.py | 2 +- components/PhonieboxVolume.py | 22 ++++- components/player/PhonieboxPlayerMPD.py | 48 ++++++---- components/rpc/PhonieboxRpcServer.py | 2 +- htdocs/api/player.php | 114 ++++++++++-------------- 5 files changed, 98 insertions(+), 90 deletions(-) diff --git a/components/PhonieboxDaemon.py b/components/PhonieboxDaemon.py index 723dd9e83..76ab50f94 100755 --- a/components/PhonieboxDaemon.py +++ b/components/PhonieboxDaemon.py @@ -74,7 +74,7 @@ def exit_gracefully(esignal, frame): #initialize Phonibox objcts objects = {'volume':volume_control_alsa, - 'player':PhonieboxPlayerMPD.player_control(music_player_status)} + 'player':PhonieboxPlayerMPD.player_control(music_player_status,volume_control_alsa)} print ("Init Phonibox RPC Server ") rpcs = phoniebox_rpc_server(objects) diff --git a/components/PhonieboxVolume.py b/components/PhonieboxVolume.py index 46582a062..f33cd7f85 100644 --- a/components/PhonieboxVolume.py +++ b/components/PhonieboxVolume.py @@ -21,20 +21,34 @@ def set(self, param): volume = -1 return ({'volume':volume}) - def inc(self, param): - volume = self.volume +3 + def inc(self, param=None): + step = param.get('step') + if step is None: + step = 3 + volume = self.volume +step if (volume > 100): volume = 100 self.volume = volume self.mixer.setvolume(self.volume) return ({'volume':self.volume}) - def dec(self, param): - volume = self.volume -3 + def dec(self, param=None): + step = param.get('step') + if step is None: + step = 3 + volume = self.volume -step if (volume < 0): volume = 0 self.volume = volume self.mixer.setvolume(self.volume) return ({'volume':self.volume}) + def mute(self,param=None): + self.mixer.setmute(1) + return ({}) + + def unmute(self,param=None): + self.mixer.setmute(0) + return ({}) + def play_wave_file(self,file_name): import wave with wave.open(file_name, 'rb') as f: diff --git a/components/player/PhonieboxPlayerMPD.py b/components/player/PhonieboxPlayerMPD.py index 11ca3f17b..69040a024 100644 --- a/components/player/PhonieboxPlayerMPD.py +++ b/components/player/PhonieboxPlayerMPD.py @@ -4,7 +4,8 @@ from mpd import MPDClient class player_control: - def __init__(self,music_player_status): + def __init__(self,music_player_status,volume_control=None): + self.volume_control = volume_control self.music_player_status = music_player_status if not self.music_player_status: self.music_player_status['player_status'] = {} @@ -44,8 +45,30 @@ def play(self, param): except Exception as e: print(e) song = self.mpd_client.currentsong() - return ({'song':song}) + return ({}) + def stop(self,param): + self.mpd_client.stop() + return ({}) + + def pause(self, param): + self.mpd_client.pause(1) + return ({}) + + def prev(self, param): + self.mpd_client.previous() + return ({}) + + def next(self, param): + self.mpd_client.next() + return ({}) + + def seek(self, param): + val = param.get('time') + if val is not None: + self.mpd_client.seekcur(val) + return ({}) + def get_current_song(self, param): return {'resp': self.mpd_client.currentsong()} @@ -118,7 +141,7 @@ def playlistaddplay(self, param): #else # mpc random off #fi - #;; + #;; def playerstop(self,param): if currecnt_status["SHUFFLE"] == "OFF": self.mpd_client.random(0) @@ -215,18 +238,11 @@ def playlistaddplay(self, param): return ({'song':song}) - - def playerstop(self,param): - status = self.mpd_client.status() - #current_status["ELAPSED"] = status.get('elapsed') - - # stop the player - self.mpd_client.stop() - #if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - return ({}) - def playerstatus(self,param): - status = self.mpd_client.status() - for k in status: - print ("{} : {}".format(k,status.get(k))) + status = self.mpd_client.currentsong() + status.update(self.mpd_client.status()) + status['volume'] = self.volume_control.volume + + #for k in status: + # print ("{} : {}".format(k,status.get(k))) return (status) \ No newline at end of file diff --git a/components/rpc/PhonieboxRpcServer.py b/components/rpc/PhonieboxRpcServer.py index a1a194d4a..b65fe8e92 100644 --- a/components/rpc/PhonieboxRpcServer.py +++ b/components/rpc/PhonieboxRpcServer.py @@ -50,7 +50,7 @@ def server(self): client_request=json.loads(message) client_response = {} - #print (client_request) + print (client_request) #make it jsonrpc https://www.jsonrpc.org/specification ?? #{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3} diff --git a/htdocs/api/player.php b/htdocs/api/player.php index f0672b5d2..b9a47c0b2 100755 --- a/htdocs/api/player.php +++ b/htdocs/api/player.php @@ -9,6 +9,25 @@ */ include 'common.php'; +$command_map = array( + 'play'=>['player','play',''], + 'next'=>['player','next',''], + 'prev'=>['player','prev',''], + 'replay'=>'-c=playerreplay -v=playlist', + 'pause'=> ['player','pause',''], + 'repeat'=>'-c=playerrepeat -v=playlist', + 'single'=> 'playerrepeat -v=single', + 'repeatoff'=>'playerrepeat -v=off', + 'seekBack'=> ['player','seek',['time' => '-15']], + 'seekAhead'=> ['player','seek',['time' => '+15']], + 'seekPosition' => 'playerseek', + 'stop'=>['player','stop',''], + 'mute'=> ['volume','mute',''], + 'volumeup'=> ['volume','inc',['step' => 5]], + 'volumedown'=> ['volume','dec',['step' => 5]], +); + + /* * debug? Conf file line: * DEBUG_WebApp_API="TRUE" @@ -30,6 +49,7 @@ function handlePut() { global $debugLoggingConf; + global $command_map; if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { file_put_contents("../../logs/debug.log", "\n # function handlePut() ", FILE_APPEND | LOCK_EX); } @@ -41,18 +61,20 @@ function handlePut() { } $inputCommand = $json['command']; $inputValue = $json['value'] ?? ""; + if ($inputCommand != null) { - if ($inputCommand == 'play') + + if (array_key_exists($inputCommand,$command_map)==True) { - $response = phonie_enquene(array('obj'=>'player','cmd'=>$inputCommand,'param'=>'')); + $cmd = $command_map[$inputCommand]; + $response = phonie_enquene(array('object'=>$cmd[0],'method'=>$cmd[1],'params'=>$cmd[2])); } else { - $controlsCommand = determineCommand($inputCommand); - $controlsValue = $inputValue !== "" ? " -v=" . ((float)$inputValue) : ""; - $execCommand = "playout_controls.sh {$controlsCommand}{$controlsValue}"; - execScript($execCommand); + echo "Unknown command {$inputCommand}"; + http_response_code(400); } + } else { echo "Body is missing command"; http_response_code(400); @@ -62,33 +84,23 @@ function handlePut() { function handleGet() { global $debugLoggingConf; global $globalConf; - $statusCommand = "status\ncurrentsong\nclose"; - $commandResponseList = execMPDCommand($statusCommand); - $responseList = array(); - forEach ($commandResponseList as $commandResponse) { - preg_match("/(?P.+?): (?P.*)/", $commandResponse, $match); - if ($match) { - $responseList[strtolower($match['key'])] = $match['value']; - } - } - - // get volume separately from mpd, because we might use amixer to control volume - if ($globalConf['VOLUMEMANAGER'] != "mpd"){ - $command = "playout_controls.sh -c=getvolume"; - $output = execScript($command); - $responseList['volume'] = implode('\n', $output); - } + + $json_response = phonie_enquene(array('object'=>'player','method'=>'playerstatus','param'=>'')); + $responseList = json_decode ( $json_response,true)['resp']; + + //so solltes es aussehen: + //$responseList: {"volume":"3","repeat":"0","random":"0","single":"0","consume":"0","partition":"default","playlist":"4","playlistlength":"14","mixrampdb":"0.000000","state":"play","song":"1","songid":"2","time":"282","elapsed":"1.329","bitrate":"128","duration":"282.064","audio":"44100:16:2","nextsong":"2","nextsongid":"3","file":"Billy Idol\/Billy Idol - Cradle Of Love.mp3","last-modified":"2021-01-02T21:04:29Z","pos":"1","id":"2","chapters":[]}GET // get chapter info if file extension indicates supports - $fileExtension = pathinfo ( $responseList['file'], PATHINFO_EXTENSION); + /*$fileExtension = pathinfo ( $responseList['file'], PATHINFO_EXTENSION); if (in_array($fileExtension, explode(',', $globalConf['CHAPTEREXTENSIONS']))) { $command = "playout_controls.sh -c=getchapters"; - $output = execScript($command); + #$output = execScript($command); $jsonChapters = trim(implode("\n", $output)); $chapters = @json_decode($jsonChapters, true); - } + }*/ - + /* $currentChapterIndex = null; $mappedChapters = array_filter(array_map(function($chapter) use($responseList, &$currentChapterIndex) { static $i = 1; @@ -106,57 +118,23 @@ function handleGet() { }, $chapters["chapters"] ?? [])); $responseList['chapters'] = $mappedChapters; + */ if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { file_put_contents("../../logs/debug.log", "\n # function handleGet() ", FILE_APPEND | LOCK_EX); file_put_contents("../../logs/debug.log", "\n\$responseList: " . json_encode($responseList) . $_SERVER['REQUEST_METHOD'], FILE_APPEND | LOCK_EX); } + file_put_contents("../../logs/debug.log", "\n # json response ".print_R($responseList,true), FILE_APPEND | LOCK_EX); + header('Content-Type: application/json'); echo json_encode($responseList); } -function determineCommand($body) { - switch ($body) { - case 'play': - return '-c=playerplay'; - case 'next': - return '-c=playernext'; - case 'prev': - return '-c=playerprev'; - case 'replay': - return '-c=playerreplay -v=playlist'; - case 'pause': - return '-c=playerpause -v=single'; - case 'repeat': - //return '-c=playerprev -c=playerrepeat -v=playlist'; - return '-c=playerrepeat -v=playlist'; - case 'single': - //return '-c=playerprev -c=playerrepeat -v=single'; - return '-c=playerrepeat -v=single'; - case 'repeatoff': - //return '-c=playerprev -c=playerrepeat -v=off'; - return '-c=playerrepeat -v=off'; - case 'seekBack': - //return '-c=playerprev -c=playerseek -v=-15'; - return '-c=playerseek -v=-15'; - case 'seekAhead': - //return '-c=playerprev -c=playerseek -v=+15'; - return '-c=playerseek -v=+15'; - case 'seekPosition': - return '-c=playerseek'; - case 'stop': - return '-c=playerstop'; - case 'mute': - return '-c=mute'; - case 'volumeup': - return '-c=volumeup'; - case 'volumedown': - return '-c=volumedown'; - } - echo "Unknown command {$body}"; - http_response_code(400); - exit; -} + + + + + ?> From 2ef1d3ac627aa640d011cdb79bfbd9d92c687461 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sat, 10 Apr 2021 23:26:06 +0200 Subject: [PATCH 012/606] added actual playlist display in webui --- components/player/PhonieboxPlayerMPD.py | 6 +++- htdocs/api/player.php | 2 -- htdocs/api/playlist.php | 41 ++++++++----------------- htdocs/api/volume.php | 11 ++----- 4 files changed, 21 insertions(+), 39 deletions(-) diff --git a/components/player/PhonieboxPlayerMPD.py b/components/player/PhonieboxPlayerMPD.py index 69040a024..24beefe7d 100644 --- a/components/player/PhonieboxPlayerMPD.py +++ b/components/player/PhonieboxPlayerMPD.py @@ -245,4 +245,8 @@ def playerstatus(self,param): #for k in status: # print ("{} : {}".format(k,status.get(k))) - return (status) \ No newline at end of file + return (status) + + def playlistinfo(self,param): + playlistinfo = (self.mpd_client.playlistinfo()) + return (playlistinfo) diff --git a/htdocs/api/player.php b/htdocs/api/player.php index b9a47c0b2..5c1fadb00 100755 --- a/htdocs/api/player.php +++ b/htdocs/api/player.php @@ -125,8 +125,6 @@ function handleGet() { file_put_contents("../../logs/debug.log", "\n\$responseList: " . json_encode($responseList) . $_SERVER['REQUEST_METHOD'], FILE_APPEND | LOCK_EX); } - file_put_contents("../../logs/debug.log", "\n # json response ".print_R($responseList,true), FILE_APPEND | LOCK_EX); - header('Content-Type: application/json'); echo json_encode($responseList); } diff --git a/htdocs/api/playlist.php b/htdocs/api/playlist.php index 98844a7c3..cb242ae6e 100755 --- a/htdocs/api/playlist.php +++ b/htdocs/api/playlist.php @@ -1,6 +1,8 @@ $record ) { - preg_match("/(?P.+?): (?P.*)/", $record, $match); - if ($match) { - $key = strtolower($match['key']); - $value = $match['value']; - if ("file" == $key) { - if ($track && $track['file'] != $value) { - $playList['tracks'][] = $track; - } - $track = array(); - $track[$key] = $value; - - } else { - $track[$key] = $value; - $albumLength += ("time" == $key) ? $value : 0; - } - } - if ($index == array_key_last($playListInfoResponse) && !empty($track)) { - $playList['tracks'][] = $track; - $playList['albumLength'] = $albumLength; - } - } - + $playlist_json = phonie_enquene(array('object'=>'player','method'=>'playlistinfo','param'=>'')); + $playList = array("tracks" => json_decode ( $playlist_json,true)['resp']); /* sample array, uncomment for checking frontend * $playList = array( @@ -77,6 +53,9 @@ function handleGet() { ), ); /**/ + + // file_put_contents("../../logs/debug.log", "\n # \$playlistINFO:" . print_R($playList,true) , FILE_APPEND | LOCK_EX); + header('Content-Type: application/json'); echo json_encode($playList); } @@ -88,6 +67,11 @@ function handlePut() { } $body = file_get_contents('php://input'); $json = json_decode(trim($body), TRUE); + + http_response_code(400); + echo "Not yet implemented"; + + /* if (validateRequest($json)) { $playlist = $json['playlist']; if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { @@ -99,6 +83,7 @@ function handlePut() { execScript("rfid_trigger_play.sh -d='{$playlist}'"); } } + */ } function validateRequest($json) { diff --git a/htdocs/api/volume.php b/htdocs/api/volume.php index f2355cd07..3a41da7b5 100755 --- a/htdocs/api/volume.php +++ b/htdocs/api/volume.php @@ -19,18 +19,13 @@ } if ($_SERVER['REQUEST_METHOD'] === 'GET') { - $command = "playout_controls.sh -c=getvolume"; - execAndEcho($command); + $json_response = phonie_enquene(array('object'=>'volume','method'=>'get')); + echo json_decode ( $json_response,true)['resp']['volume']; } else if ($_SERVER['REQUEST_METHOD'] === 'PUT') { $body = file_get_contents('php://input'); if (is_numeric($body)) { - #$command = "playout_controls.sh -c=setvolume -v=$body"; - #execScript($command); echo $body; - $response = phonie_enquene(array('obj'=>'volume','cmd'=>'set','param'=>array('volume'=>(int)$body))); - - #if ($response == NULL) {$response = "No Response receivd";}; - #echo($response); + phonie_enquene(array('object'=>'volume','method'=>'set','params'=>array('volume'=>(int)$body))); } else { http_response_code(400); } From af2c0e5b308618a56f955184b0148b2bb5ad0ad1 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sun, 11 Apr 2021 12:18:14 +0200 Subject: [PATCH 013/606] Fake Reade will start in Background and on top right --- components/rfid_reader/FakeRfidReader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/rfid_reader/FakeRfidReader.py b/components/rfid_reader/FakeRfidReader.py index e2c24dd72..4aa900245 100644 --- a/components/rfid_reader/FakeRfidReader.py +++ b/components/rfid_reader/FakeRfidReader.py @@ -11,13 +11,15 @@ def __init__(self): def create_ui(self): self.root = Tk() self.root.title("Fake RFID Reader") + self.root.geometry("200x100") + self.root.geometry("-120+50") # Add a grid self.mainframe = Frame(self.root) self.mainframe.grid(column=0,row=0, sticky=(N,W,E,S) ) self.mainframe.columnconfigure(0, weight = 1) self.mainframe.rowconfigure(0, weight = 1) - self.mainframe.pack(pady = 100, padx = 100) + self.mainframe.pack(pady = 20, padx = 1) # Create a Tkinter variable self.tkvar = StringVar(self.root) @@ -29,6 +31,8 @@ def create_ui(self): # link function to change dropdown self.tkvar.trace('w', self.change_dropdown) + self.root.lower() + # on change dropdown value def change_dropdown(self,*args): self.card_id = self.tkvar.get() From 66c3dfdcd54f3e676eea3ccc6b4d1dffc872afa3 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sun, 11 Apr 2021 22:59:06 +0200 Subject: [PATCH 014/606] added module (object) for system commands --- components/PhonieboxDaemon.py | 22 +++++++--------------- components/PhonieboxSystem.py | 23 +++++++++++++++++++++++ components/PhonieboxVolume.py | 30 +++++++++++++++++------------- htdocs/api/common.php | 23 ----------------------- htdocs/inc.header.php | 31 ++++++++++++++----------------- 5 files changed, 61 insertions(+), 68 deletions(-) create mode 100644 components/PhonieboxSystem.py diff --git a/components/PhonieboxDaemon.py b/components/PhonieboxDaemon.py index 76ab50f94..78299a4dc 100755 --- a/components/PhonieboxDaemon.py +++ b/components/PhonieboxDaemon.py @@ -7,6 +7,7 @@ from time import sleep, time import PhonieboxVolume +import PhonieboxSystem from player import PhonieboxPlayerMPD from rpc.PhonieboxRpcServer import phoniebox_rpc_server from PhonieboxNvManager import nv_manager @@ -55,16 +56,11 @@ def exit_gracefully(esignal, frame): phoniebox_config['audiofolders_path'] = "../shared/" # Play Startup Sound - volume_control_alsa = PhonieboxVolume.volume_control_alsa() - startsound_thread = threading.Thread(target=volume_control_alsa.play_wave_file, args=["../shared/startupsound.wav"]) + volume_control = PhonieboxVolume.volume_control_alsa(listcards=False) + startsound_thread = threading.Thread(target=volume_control.play_wave_file, args=["../shared/startupsound.wav"]) startsound_thread.start() - #Debug: List ALSA cards and mixers - #PhonieboxVolume.list_cards() - #PhonieboxVolume.list_mixers({ 'cardindex': 0 }) - g_nvm = nv_manager() - #phoniebox music player status music_player_status = g_nvm.load("../shared/music_player_status.json") @@ -73,8 +69,9 @@ def exit_gracefully(esignal, frame): cardid_database = g_nvm.load("../settings/phoniebox_cardid_database.json") #initialize Phonibox objcts - objects = {'volume':volume_control_alsa, - 'player':PhonieboxPlayerMPD.player_control(music_player_status,volume_control_alsa)} + objects = {'volume':volume_control, + 'player':PhonieboxPlayerMPD.player_control(music_player_status,volume_control), + 'system':PhonieboxSystem.system_control} print ("Init Phonibox RPC Server ") rpcs = phoniebox_rpc_server(objects) @@ -83,14 +80,10 @@ def exit_gracefully(esignal, frame): #rfid_reader = RFID_Reader("RDM6300",{'numberformat':'card_id_float'}) rfid_reader = RFID_Reader("Fake") - #rfid_reader = None if rfid_reader is not None: - print ("set db") rfid_reader.set_cardid_db(cardid_database) - rfid_reader.reader.set_card_ids(list(cardid_database)) #just for Fake Reader - print ("set thread") + rfid_reader.reader.set_card_ids(list(cardid_database)) #just for Fake Reader to be aware of card numbers rfid_thread = threading.Thread(target=rfid_reader.run) - #rfid_t.start() else: rfid_thread = None @@ -110,7 +103,6 @@ def exit_gracefully(esignal, frame): else: gpio_thread = None - # signal.raise_signal(signum) # setup the signal listeners signal.signal(signal.SIGINT, exit_gracefully) signal.signal(signal.SIGTERM, exit_gracefully) diff --git a/components/PhonieboxSystem.py b/components/PhonieboxSystem.py new file mode 100644 index 000000000..e88c9474d --- /dev/null +++ b/components/PhonieboxSystem.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +class system_control: + def __init__(self): + self.init = 1 + + def shutdown(self, param=None): + print ("shutdown") + return ({}) + + def reboot(self, param=None): + print ("reboot") + return ({}) + + def settings_get(self, param): + return ({}) + + def settings_set(self, param): + return ({}) + + def settings_getall(self, param): + return ({}) \ No newline at end of file diff --git a/components/PhonieboxVolume.py b/components/PhonieboxVolume.py index f33cd7f85..cf8033957 100644 --- a/components/PhonieboxVolume.py +++ b/components/PhonieboxVolume.py @@ -3,10 +3,23 @@ import alsaaudio class volume_control_alsa: - def __init__(self): - self.mixer = alsaaudio.Mixer('Master', 0) - self.volume = self.mixer.getvolume()[0] + def __init__(self, listcards=False): + if listcards is True: + self.list_cards_and_mixers() + self.mixer = alsaaudio.Mixer('Master', 0) + self.volume = self.mixer.getvolume()[0] + + def list_cards_and_mixers(self): + print("Available sound cards:") + for i in alsaaudio.card_indexes(): + (name, longname) = alsaaudio.card_name(i) + print(" %d: %s (%s)" % (i, name, longname)) + + print("\nAvailable mixer controls:") + for m in alsaaudio.mixers(**{ 'cardindex': 0 }): + print(" '%s'" % m) + def get(self, param): return ({'volume':self.volume}) @@ -69,7 +82,7 @@ def play_wave_file(self,file_name): periodsize = f.getframerate() // 8 - print('%d channels, %d sampling rate, format %d, periodsize %d\n' % (f.getnchannels(),f.getframerate(), format, periodsize)) + #print('%d channels, %d sampling rate, format %d, periodsize %d\n' % (f.getnchannels(),f.getframerate(), format, periodsize)) device = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=periodsize, device="default") @@ -79,14 +92,5 @@ def play_wave_file(self,file_name): data = f.readframes(periodsize) -def list_cards(): - print("Available sound cards:") - for i in alsaaudio.card_indexes(): - (name, longname) = alsaaudio.card_name(i) - print(" %d: %s (%s)" % (i, name, longname)) -def list_mixers(kwargs): - print("Available mixer controls:") - for m in alsaaudio.mixers(**kwargs): - print(" '%s'" % m) diff --git a/htdocs/api/common.php b/htdocs/api/common.php index f6855561c..8533909b0 100755 --- a/htdocs/api/common.php +++ b/htdocs/api/common.php @@ -1,15 +1,6 @@ diff --git a/htdocs/inc.header.php b/htdocs/inc.header.php index cdf1972e7..51cafcea6 100755 --- a/htdocs/inc.header.php +++ b/htdocs/inc.header.php @@ -1,6 +1,8 @@ "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setvolume -v=%s", // change volume 'maxvolume' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setmaxvolume -v=%s", // change max volume 'startupvolume' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setstartupvolume -v=%s", // change startup volume @@ -310,16 +313,11 @@ function fileGetContentOrDefault($filename, $defaultValue) 'DebugLogClear' => "sudo rm ../logs/debug.log; sudo touch ../logs/debug.log; sudo chmod 777 ../logs/debug.log", 'scan' => array( 'true' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=scan > /dev/null 2>&1 &" // scan the library - ), - 'stop' => array( - 'true' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=playerstop" // stop playing - ), - 'reboot' => array( - 'true' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=reboot > /dev/null 2>&1 &" // reboot the jukebox - ), - 'shutdown' => array( - 'true' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=shutdown > /dev/null 2>&1 &"// shutdown the jukebox - ), + ),*/ + 'stop' => ['object'=>'player','method'=>'stop','param'=>''], // stop playing + 'reboot' => ['object'=>'system','method'=>'reboot','param'=>''], // reboot the jukebox + 'shutdown' => ['object'=>'system','method'=>'shutdown','param'=>''], // shutdown the jukebox + /* 'rfidstatus' => array( 'turnon' => "/usr/bin/sudo /bin/systemctl start phoniebox-rfid-reader.service", // start the rfid service 'turnoff' => "/usr/bin/sudo /bin/systemctl stop phoniebox-rfid-reader.service" // stop the rfid service @@ -340,15 +338,14 @@ function fileGetContentOrDefault($filename, $defaultValue) "repeatoff" => "/usr/bin/sudo " . $conf['scripts_abs'] . "/playout_controls.sh -c=playerrepeat -v=off", "seekBack" => "/usr/bin/sudo " . $conf['scripts_abs'] . "/playout_controls.sh -c=playerseek -v=-15", "seekAhead" => "/usr/bin/sudo " . $conf['scripts_abs'] . "/playout_controls.sh -c=playerseek -v=+15", - ), + ),*/ ); + foreach ($urlparams as $paramKey => $paramValue) { - if(isset($commandToAction[$paramKey]) && !is_array($commandToAction[$paramKey])) { - $exec = sprintf($commandToAction[$paramKey], $paramValue); - execAndRedirect($exec); - } elseif (isset($commandToAction[$paramKey]) && isset($commandToAction[$paramKey][$paramValue])) { - $exec = sprintf($commandToAction[$paramKey][$paramValue], $paramValue); - execAndRedirect($exec); + if(isset($commandToAction[$paramKey]) ) { + $json_response = phonie_enquene($commandToAction[$paramKey]); + } else { + error_log("calling script in inc_header via commandToAction: \"".$paramKey."\" Val:\"".$paramValue."\"", 0); } } From f2af289999273687679d63abead28796b36b9bfc Mon Sep 17 00:00:00 2001 From: arne123 Date: Mon, 12 Apr 2021 23:57:50 +0200 Subject: [PATCH 015/606] Added frame for player playlist functions --- components/player/PhonieboxPlayerMPD.py | 16 ++++++++++++++++ htdocs/api/common.php | 9 --------- htdocs/api/playlist/appendFileToPlaylist.php | 8 +++++--- htdocs/api/playlist/moveDownSongInPlaylist.php | 7 ++++--- htdocs/api/playlist/moveUpSongInPlaylist.php | 4 ++-- htdocs/api/playlist/playsinglefile.php | 6 +++--- htdocs/api/playlist/removeSongFromPlaylist.php | 4 ++-- htdocs/api/playlist/song.php | 4 ++-- htdocs/inc.processCheckCardEditRegister.php | 8 +++++--- 9 files changed, 39 insertions(+), 27 deletions(-) diff --git a/components/player/PhonieboxPlayerMPD.py b/components/player/PhonieboxPlayerMPD.py index 24beefe7d..e18da2c8e 100644 --- a/components/player/PhonieboxPlayerMPD.py +++ b/components/player/PhonieboxPlayerMPD.py @@ -77,6 +77,22 @@ def map_filename_to_playlist_pos(self,filename): #self.mpd_client.playlistfind() return 0 + def remove(self, param): + print ("remove not yet implemented") + return ({}) + + def moveup(self, param): + print ("moveup not yet implemented") + return ({}) + + def movedown(self, param): + print ("movedown not yet implemented") + return ({}) + + def playsingle(self, param): + print ("playsingle not yet implemented") + return ({}) + def playlistaddplay(self, param): # add to playlist (and play) diff --git a/htdocs/api/common.php b/htdocs/api/common.php index 8533909b0..9a586ce7b 100755 --- a/htdocs/api/common.php +++ b/htdocs/api/common.php @@ -6,15 +6,6 @@ function execScript($command) { return execSuccessfully($absoluteCommand); } -function execScriptWithoutCheck($command) { - global $debugLoggingConf; - if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n # function execScriptWithoutCheck: " . $command , FILE_APPEND | LOCK_EX); - } - $absoluteCommand = realpath(dirname(__FILE__) .'/../../scripts') ."/{$command}"; - exec("sudo ".$absoluteCommand); -} - function execSuccessfully($command) { global $debugLoggingConf; if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { diff --git a/htdocs/api/playlist/appendFileToPlaylist.php b/htdocs/api/playlist/appendFileToPlaylist.php index 3d2a43c0b..ebd8ee43a 100755 --- a/htdocs/api/playlist/appendFileToPlaylist.php +++ b/htdocs/api/playlist/appendFileToPlaylist.php @@ -4,7 +4,7 @@ /** * Appends a given file to the current playlist (and starts playing) */ -include('../common.php'); +require_once("../zmq.php"); /* * debug? Conf file line: @@ -21,12 +21,14 @@ if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { file_put_contents("../../../logs/debug.log", "\n # \$body: " . $body , FILE_APPEND | LOCK_EX); } - execScriptWithoutCheck("playout_controls.sh -c=playlistappend -v='{$body}'"); + phonie_enquene(['object'=>'player','method'=>'playlistappend','param'=>['songid'=>$body ]]); } else { $file = $_GET["file"]; if ($file !== "") { print "Playing file " . $file; - execScriptWithoutCheck("playout_controls.sh -c=playlistappend -v='$file'"); + phonie_enquene(['object'=>'player','method'=>'playlistappend','param'=>['songid'=>$file ]]); + + require_once("../zmq.php"); }else{ http_response_code(405); } diff --git a/htdocs/api/playlist/moveDownSongInPlaylist.php b/htdocs/api/playlist/moveDownSongInPlaylist.php index e78036fe4..bc58c0744 100755 --- a/htdocs/api/playlist/moveDownSongInPlaylist.php +++ b/htdocs/api/playlist/moveDownSongInPlaylist.php @@ -1,10 +1,12 @@ 'player','method'=>'movedown','param'=>['songid'=>$body ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/moveUpSongInPlaylist.php b/htdocs/api/playlist/moveUpSongInPlaylist.php index 9d317c1bd..28bfcd2d0 100755 --- a/htdocs/api/playlist/moveUpSongInPlaylist.php +++ b/htdocs/api/playlist/moveUpSongInPlaylist.php @@ -4,7 +4,7 @@ /** * Moves a song within the current playlist up. */ -include('../common.php'); +require_once("../zmq.php"); /* * debug? Conf file line: @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - execScriptWithoutCheck("playout_controls.sh -c=playermoveup -v='{$body}'"); + phonie_enquene(['object'=>'player','method'=>'moveup','param'=>['songid'=>$body ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/playsinglefile.php b/htdocs/api/playlist/playsinglefile.php index 496bc88b2..911bbb24f 100755 --- a/htdocs/api/playlist/playsinglefile.php +++ b/htdocs/api/playlist/playsinglefile.php @@ -4,7 +4,7 @@ /** * Starts to play a single song in a new playlist. */ -include('../common.php'); +require_once("../zmq.php"); /* * debug? Conf file line: @@ -22,12 +22,12 @@ file_put_contents("../../../logs/debug.log", "\n # \$body: " . $body , FILE_APPEND | LOCK_EX); } // This script always returns with returncode 1, so we cannot check that the returncode is 0 - execScriptWithoutCheck("playout_controls.sh -c=playsinglefile -v='{$body}'"); + phonie_enquene(['object'=>'player','method'=>'playsingle','param'=>['songid'=>$body ]]); } else { $file = $_GET["file"]; if ($file !== "") { print "Playing file " . $file; - execScriptWithoutCheck("playout_controls.sh -c=playsinglefile -v='$file'"); + phonie_enquene(['object'=>'player','method'=>'playsingle','param'=>['songid'=>$file ]]); }else{ http_response_code(405); } diff --git a/htdocs/api/playlist/removeSongFromPlaylist.php b/htdocs/api/playlist/removeSongFromPlaylist.php index cbe893dde..9cfbf3588 100755 --- a/htdocs/api/playlist/removeSongFromPlaylist.php +++ b/htdocs/api/playlist/removeSongFromPlaylist.php @@ -4,7 +4,7 @@ /** * Removes a song from the current playlist. */ -include('../common.php'); +require_once("../zmq.php"); /* * debug? Conf file line: @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - execScriptWithoutCheck("playout_controls.sh -c=playerremove -v='{$body}'"); + phonie_enquene(['object'=>'player','method'=>'remove','param'=>['songid'=>$body ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/song.php b/htdocs/api/playlist/song.php index 7bc2d2993..c065660bf 100755 --- a/htdocs/api/playlist/song.php +++ b/htdocs/api/playlist/song.php @@ -4,7 +4,7 @@ /** * Starts to play a song in the current playlist. */ -include('../common.php'); +require_once("../zmq.php"); /* * debug? Conf file line: @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - execScriptWithoutCheck("playout_controls.sh -c=playerplay -v='{$body}'"); + phonie_enquene(['object'=>'player','method'=>'play','param'=>['songid'=>$body ]]); } else { http_response_code(400); } diff --git a/htdocs/inc.processCheckCardEditRegister.php b/htdocs/inc.processCheckCardEditRegister.php index 5ae713aee..df35a1613 100755 --- a/htdocs/inc.processCheckCardEditRegister.php +++ b/htdocs/inc.processCheckCardEditRegister.php @@ -13,6 +13,7 @@ function rfidAvailArr() { $rfidAvailRaw = ""; + /* $conf['conf_abs'] = realpath(getcwd().'/../settings/rfid_trigger_play.conf.sample'); $fn = fopen($conf['conf_abs'],"r"); while(! feof($fn)) { @@ -23,7 +24,7 @@ function rfidAvailArr() { } } fclose($fn); - $rfidAvailArr = parse_ini_string($rfidAvailRaw); //print "
"; print_r($rfidAvailArr); print "
"; + $rfidAvailArr = parse_ini_string($rfidAvailRaw); //print "
"; print_r($rfidAvailArr); print "
";*/ return $rfidAvailArr; } /******************************************/ @@ -35,7 +36,8 @@ function rfidAvailArr() { function rfidUsedArr() { $rfidUsedRaw = ""; - $fn = fopen("../settings/rfid_trigger_play.conf","r"); + + /*$fn = fopen("../settings/rfid_trigger_play.conf","r"); while(! feof($fn)) { $result = fgets($fn); // ignore commented and empty lines @@ -44,7 +46,7 @@ function rfidUsedArr() { } } fclose($fn); - $rfidUsedArr = parse_ini_string($rfidUsedRaw); //print "
"; print_r($rfidUsedArr); print "
"; + $rfidUsedArr = parse_ini_string($rfidUsedRaw); //print "
"; print_r($rfidUsedArr); print "
";*/ return $rfidUsedArr; } /******************************************/ From 82404b6353eee9141da4ef5c1f2b0dd1fc62da5b Mon Sep 17 00:00:00 2001 From: arne123 Date: Tue, 13 Apr 2021 21:06:02 +0200 Subject: [PATCH 016/606] added player play songpos --- components/player/PhonieboxPlayerMPD.py | 23 +++++++++++++------ .../api/playlist/moveDownSongInPlaylist.php | 2 +- htdocs/api/playlist/moveUpSongInPlaylist.php | 2 +- htdocs/api/playlist/song.php | 2 +- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/components/player/PhonieboxPlayerMPD.py b/components/player/PhonieboxPlayerMPD.py index e18da2c8e..9fb510ed4 100644 --- a/components/player/PhonieboxPlayerMPD.py +++ b/components/player/PhonieboxPlayerMPD.py @@ -36,12 +36,20 @@ def get_player_type_and_version(self, param): return ({'result':'mpd','version':self.mpd_client.mpd_version}) def play(self, param): + + if param is not None and isinstance(param, dict): + songid = param.get("songid") + if songid is None: + songid = 1 + else: + songid = 1 + try: - self.mpd_client.play() + self.mpd_client.play(songid) except ConnectionError: print ("MPD Connection Error, retry") self.conncet() - self.mpd_client.play() + self.mpd_client.play(songid) except Exception as e: print(e) song = self.mpd_client.currentsong() @@ -81,12 +89,13 @@ def remove(self, param): print ("remove not yet implemented") return ({}) - def moveup(self, param): - print ("moveup not yet implemented") - return ({}) + def move(self, param): + song_id = param.get("song_id") + step = param.get("step") + #MPDClient.playlistmove(name, from, to) + #MPDClient.swapid(song1, song2) - def movedown(self, param): - print ("movedown not yet implemented") + print ("move not yet implemented") return ({}) def playsingle(self, param): diff --git a/htdocs/api/playlist/moveDownSongInPlaylist.php b/htdocs/api/playlist/moveDownSongInPlaylist.php index bc58c0744..bfbc13faf 100755 --- a/htdocs/api/playlist/moveDownSongInPlaylist.php +++ b/htdocs/api/playlist/moveDownSongInPlaylist.php @@ -24,7 +24,7 @@ file_put_contents("../../../logs/debug.log", "\n # \$body: " . $body , FILE_APPEND | LOCK_EX); } if (is_numeric($body)) { - phonie_enquene(['object'=>'player','method'=>'movedown','param'=>['songid'=>$body ]]); + phonie_enquene(['object'=>'player','method'=>'move','params'=>['songid'=>$body,'step' => -1 ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/moveUpSongInPlaylist.php b/htdocs/api/playlist/moveUpSongInPlaylist.php index 28bfcd2d0..d0d8c8ccf 100755 --- a/htdocs/api/playlist/moveUpSongInPlaylist.php +++ b/htdocs/api/playlist/moveUpSongInPlaylist.php @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - phonie_enquene(['object'=>'player','method'=>'moveup','param'=>['songid'=>$body ]]); + phonie_enquene(['object'=>'player','method'=>'move','params'=>['songid'=>$body,'step' => 1 ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/song.php b/htdocs/api/playlist/song.php index c065660bf..c77d0b223 100755 --- a/htdocs/api/playlist/song.php +++ b/htdocs/api/playlist/song.php @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - phonie_enquene(['object'=>'player','method'=>'play','param'=>['songid'=>$body ]]); + phonie_enquene(['object'=>'player','method'=>'play','params'=>['songid'=>$body ]]); } else { http_response_code(400); } From dacbe47976b1967cf6212c3e4b1265d5eb4c5b93 Mon Sep 17 00:00:00 2001 From: arne123 Date: Tue, 20 Apr 2021 00:17:42 +0200 Subject: [PATCH 017/606] Harmonized Names for RPC related Code -removed currently not used stuff I am aware of --- components/PhonieboxDaemon.py | 4 +- components/player/PhonieboxPlayerMPD.py | 69 +- components/rfid_reader/PhonieboxRfidReader.py | 10 +- components/rpc/PhonieboxRpcClient.py | 12 +- components/rpc/PhonieboxRpcServer.py | 2 +- .../api/{zmq.php => PhonieboxRpcClient.php} | 2 +- htdocs/api/player.php | 16 +- htdocs/api/playlist.php | 4 +- htdocs/api/playlist/appendFileToPlaylist.php | 8 +- .../api/playlist/moveDownSongInPlaylist.php | 4 +- htdocs/api/playlist/moveUpSongInPlaylist.php | 4 +- htdocs/api/playlist/playsinglefile.php | 6 +- .../api/playlist/removeSongFromPlaylist.php | 4 +- htdocs/api/playlist/song.php | 4 +- htdocs/api/volume.php | 6 +- htdocs/inc.header.php | 4 +- scripts/Reader.py | 200 --- scripts/Reader.py.Multi | 73 - scripts/Reader.py.experimental.Multi | 211 --- scripts/Reader.py.kkmoonRFIDreader | 60 - scripts/Reader.py.original | 70 - scripts/RegisterDevice.py | 18 - scripts/RegisterDevice.py.Multi | 103 -- scripts/__init__.py | 0 scripts/activate_amplifier.py | 46 - scripts/daemon_rfid_reader.py | 87 - .../Analytics_AfterInstallScript.sh | 84 - scripts/helperscripts/AssignIDs4Shortcuts.php | 109 -- .../helperscripts/CreateCsvFromShortcuts.php | 42 - .../CreatePodcastsKidsDeutsch.sh | 36 - .../CreateSampleAudiofoldersStreams.sh | 95 -- scripts/helperscripts/DeleteAllConfig.sh | 49 - .../DeleteSampleAudiofoldersStreams.sh | 16 - scripts/helperscripts/autohotspot | 169 -- scripts/helperscripts/cli_ReadWifiIp.php | 21 - scripts/idle-watchdog-countdown.sh | 51 - scripts/idle-watchdog.sh | 43 - scripts/inc.readArgsFromCommandLine.sh | 47 - scripts/inc.settingsFolderSpecific.sh | 51 - scripts/inc.writeFolderConfig.sh | 160 -- scripts/inc.writeGlobalConfig.sh | 380 ----- ...buster-install-default-with-autohotspot.sh | 1427 ----------------- .../installscripts/buster-install-default.sh | 1299 --------------- .../stretch-install-default-HotspotAddOn.sh | 124 -- .../installscripts/stretch-install-default.sh | 894 ----------- .../tests/ShellCheck/PhonieboxInstall.conf | 16 - .../tests/run_installation_tests.sh | 32 - .../tests/run_installation_tests2.sh | 34 - .../tests/run_installation_tests3.sh | 32 - .../installscripts/tests/test_installation.sh | 349 ---- scripts/playlist_recursive_by_folder.php | 225 --- scripts/playout_controls.sh | 1035 ------------ .../python-phoniebox/ConfigParserExtended.py | 31 - scripts/python-phoniebox/LICENSE | 21 - scripts/python-phoniebox/Phoniebox.py | 412 ----- .../PhonieboxConfigChanger.py | 139 -- scripts/python-phoniebox/README.md | 6 - .../RawConfigParserExtended.py | 31 - scripts/python-phoniebox/Reader.py | 140 -- scripts/python-phoniebox/__init__.py | 0 scripts/python-phoniebox/deviceName.txt | 1 - .../helpers_unused_atm/__init__.py | 0 .../helpers_unused_atm/helpers.py | 23 - scripts/python-phoniebox/phoniebox.conf | 62 - scripts/resume_play.sh | 180 --- scripts/rfid_trigger_play.sh | 499 ------ scripts/shuffle_play.sh | 104 -- scripts/single_play.sh | 101 -- scripts/startup-scripts.sh | 65 - scripts/startup_sound.sh | 11 - scripts/test/mockedGPIO.py | 17 - scripts/test/test_TwoButtonControl.py | 157 -- scripts/test_zmq.py | 160 -- scripts/userscripts/addhotspot.sh | 10 - settings/phoniebox_cardid_database.json | 8 +- 75 files changed, 101 insertions(+), 9924 deletions(-) rename htdocs/api/{zmq.php => PhonieboxRpcClient.php} (95%) delete mode 100755 scripts/Reader.py delete mode 100644 scripts/Reader.py.Multi delete mode 100644 scripts/Reader.py.experimental.Multi delete mode 100755 scripts/Reader.py.kkmoonRFIDreader delete mode 100755 scripts/Reader.py.original delete mode 100755 scripts/RegisterDevice.py delete mode 100644 scripts/RegisterDevice.py.Multi delete mode 100755 scripts/__init__.py delete mode 100755 scripts/activate_amplifier.py delete mode 100755 scripts/daemon_rfid_reader.py delete mode 100755 scripts/helperscripts/Analytics_AfterInstallScript.sh delete mode 100755 scripts/helperscripts/AssignIDs4Shortcuts.php delete mode 100755 scripts/helperscripts/CreateCsvFromShortcuts.php delete mode 100755 scripts/helperscripts/CreatePodcastsKidsDeutsch.sh delete mode 100755 scripts/helperscripts/CreateSampleAudiofoldersStreams.sh delete mode 100755 scripts/helperscripts/DeleteAllConfig.sh delete mode 100755 scripts/helperscripts/DeleteSampleAudiofoldersStreams.sh delete mode 100755 scripts/helperscripts/autohotspot delete mode 100755 scripts/helperscripts/cli_ReadWifiIp.php delete mode 100755 scripts/idle-watchdog-countdown.sh delete mode 100755 scripts/idle-watchdog.sh delete mode 100755 scripts/inc.readArgsFromCommandLine.sh delete mode 100755 scripts/inc.settingsFolderSpecific.sh delete mode 100755 scripts/inc.writeFolderConfig.sh delete mode 100755 scripts/inc.writeGlobalConfig.sh delete mode 100755 scripts/installscripts/buster-install-default-with-autohotspot.sh delete mode 100755 scripts/installscripts/buster-install-default.sh delete mode 100644 scripts/installscripts/stretch-install-default-HotspotAddOn.sh delete mode 100755 scripts/installscripts/stretch-install-default.sh delete mode 100644 scripts/installscripts/tests/ShellCheck/PhonieboxInstall.conf delete mode 100644 scripts/installscripts/tests/run_installation_tests.sh delete mode 100644 scripts/installscripts/tests/run_installation_tests2.sh delete mode 100644 scripts/installscripts/tests/run_installation_tests3.sh delete mode 100755 scripts/installscripts/tests/test_installation.sh delete mode 100755 scripts/playlist_recursive_by_folder.php delete mode 100755 scripts/playout_controls.sh delete mode 100644 scripts/python-phoniebox/ConfigParserExtended.py delete mode 100644 scripts/python-phoniebox/LICENSE delete mode 100755 scripts/python-phoniebox/Phoniebox.py delete mode 100755 scripts/python-phoniebox/PhonieboxConfigChanger.py delete mode 100644 scripts/python-phoniebox/README.md delete mode 100755 scripts/python-phoniebox/RawConfigParserExtended.py delete mode 100755 scripts/python-phoniebox/Reader.py delete mode 100755 scripts/python-phoniebox/__init__.py delete mode 100755 scripts/python-phoniebox/deviceName.txt delete mode 100755 scripts/python-phoniebox/helpers_unused_atm/__init__.py delete mode 100755 scripts/python-phoniebox/helpers_unused_atm/helpers.py delete mode 100755 scripts/python-phoniebox/phoniebox.conf delete mode 100755 scripts/resume_play.sh delete mode 100755 scripts/rfid_trigger_play.sh delete mode 100755 scripts/shuffle_play.sh delete mode 100755 scripts/single_play.sh delete mode 100755 scripts/startup-scripts.sh delete mode 100755 scripts/startup_sound.sh delete mode 100644 scripts/test/mockedGPIO.py delete mode 100644 scripts/test/test_TwoButtonControl.py delete mode 100644 scripts/test_zmq.py delete mode 100644 scripts/userscripts/addhotspot.sh diff --git a/components/PhonieboxDaemon.py b/components/PhonieboxDaemon.py index 78299a4dc..3b857563d 100755 --- a/components/PhonieboxDaemon.py +++ b/components/PhonieboxDaemon.py @@ -9,7 +9,7 @@ import PhonieboxVolume import PhonieboxSystem from player import PhonieboxPlayerMPD -from rpc.PhonieboxRpcServer import phoniebox_rpc_server +from rpc.PhonieboxRpcServer import PhonieboxRpcServer from PhonieboxNvManager import nv_manager from rfid_reader.PhonieboxRfidReader import RFID_Reader #from gpio_control import gpio_control @@ -74,7 +74,7 @@ def exit_gracefully(esignal, frame): 'system':PhonieboxSystem.system_control} print ("Init Phonibox RPC Server ") - rpcs = phoniebox_rpc_server(objects) + rpcs = PhonieboxRpcServer(objects) if rpcs != None: rpcs.connect() diff --git a/components/player/PhonieboxPlayerMPD.py b/components/player/PhonieboxPlayerMPD.py index 9fb510ed4..7e5d56d28 100644 --- a/components/player/PhonieboxPlayerMPD.py +++ b/components/player/PhonieboxPlayerMPD.py @@ -7,17 +7,26 @@ class player_control: def __init__(self,music_player_status,volume_control=None): self.volume_control = volume_control self.music_player_status = music_player_status - if not self.music_player_status: - self.music_player_status['player_status'] = {} - self.music_player_status['audio_folder_status'] = {} - self.music_player_status.save_to_json() - + self.mpd_client = MPDClient() # create client object self.mpd_client.timeout = 0.5 # network timeout in seconds (floats allowed), default: None self.mpd_client.idletimeout = 0.5 # timeout for fetching the result of the idle command is handled seperately, default: None self.connect() - print("Connected to MPD Version: "+self.mpd_client.mpd_version) - + print("Connected to MPD Version: "+self.mpd_client.mpd_version) + + if not self.music_player_status: + self.music_player_status['player_status'] = {} + self.music_player_status['audio_folder_status'] = {} + self.music_player_status.save_to_json() + self.current_folder_status = {} + else: + last_played_folder = self.music_player_status['player_status']['last_played_folder'] + if last_played_folder is not None: + self.current_folder_status = self.music_player_status['audio_folder_status'][last_played_folder] + self.mpd_client.clear() + self.mpd_client.add(last_played_folder) + print ("Last Played Folder: "+last_played_folder) + def connect(self): self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 @@ -77,6 +86,24 @@ def seek(self, param): self.mpd_client.seekcur(val) return ({}) + def replay(self, param): + return ({}) + + def repeatmode(self, param): + if param is not None and isinstance(param, dict): + mode = param.get("mode") + + if mode == 'repeat' : + MPDClient.repeat(1) + MPDClient.single(0) + elif mode == 'single' : + MPDClient.repeat(1) + MPDClient.single(1) + else: + MPDClient.repeat(0) + MPDClient.single(0) + return ({}) + def get_current_song(self, param): return {'resp': self.mpd_client.currentsong()} @@ -128,9 +155,9 @@ def playlistaddplay(self, param): self.music_player_status['player_status']['last_played_folder'] = folder - current_status = self.music_player_status['audio_folder_status'].get(folder) - if current_status is None: - current_status = self.music_player_status['audio_folder_status'][folder] = {} + self.current_folder_status = self.music_player_status['audio_folder_status'].get(folder) + if self.current_folder_status is None: + self.current_folder_status = self.music_player_status['audio_folder_status'][folder] = {} self.mpd_client.play() @@ -253,13 +280,13 @@ def playlistaddplay(self, param): song = self.mpd_client.currentsong() - current_status["CURRENTFILENAME"] = song.get('file') - current_status["ELAPSED"] = 0 - current_status["PLAYSTATUS"] = "Stopped" - current_status["RESUME"] = "OFF" - current_status["SHUFFLE"] = "OFF" - current_status["LOOP"] = "OFF" - current_status["SINGLE"] = "OFF" + self.current_folder_status["CURRENTFILENAME"] = song.get('file') + self.current_folder_status["ELAPSED"] = 0 + self.current_folder_status["PLAYSTATUS"] = "Stopped" + self.current_folder_status["RESUME"] = "OFF" + self.current_folder_status["SHUFFLE"] = "OFF" + self.current_folder_status["LOOP"] = "OFF" + self.current_folder_status["SINGLE"] = "OFF" return ({'song':song}) @@ -268,6 +295,14 @@ def playerstatus(self,param): status.update(self.mpd_client.status()) status['volume'] = self.volume_control.volume + #for now use this to update the actual folder status (require web ui to run) + #finnaly a switch to asynio implementation makes sense, to handle the plloing independant + elapsed = status.get('elapsed') + if elapsed is not None: + self.current_folder_status["ELAPSED"] = elapsed + self.music_player_status['player_status']["CURRENTSONGPOS"] = status['song'] + self.music_player_status['player_status']["CURRENTFILENAME"] = status['file'] + #for k in status: # print ("{} : {}".format(k,status.get(k))) return (status) diff --git a/components/rfid_reader/PhonieboxRfidReader.py b/components/rfid_reader/PhonieboxRfidReader.py index 799f5408c..01059d0f2 100644 --- a/components/rfid_reader/PhonieboxRfidReader.py +++ b/components/rfid_reader/PhonieboxRfidReader.py @@ -8,10 +8,9 @@ import os.path import sys -#import RPi.GPIO as GPIO import logging -from rpc.PhonieboxRpcClient import phoniebox_object_access_queue +from rpc.PhonieboxRpcClient import PhonieboxRpcClient #from evdev import InputDevice, categorize, ecodes, list_devices @@ -72,8 +71,8 @@ def __init__(self,device_name,param=None): except IndexError: sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) - self.queue = phoniebox_object_access_queue() - self.queue.connect() + self.PhonieboxRpc = PhonieboxRpcClient() + self.PhonieboxRpc.connect() self._keep_running = True self.cardnotification = None self.valid_cardnotification = None @@ -149,7 +148,8 @@ def run(self): self.valid_cardnotification() #queue = phoniebox_object_access_queue() #queue.connect() - resp = self.queue.phonie_enqueue(card_assignment) + resp = self.PhonieboxRpc.enqueue(card_assignment) + #hm, what to do with response here? diff --git a/components/rpc/PhonieboxRpcClient.py b/components/rpc/PhonieboxRpcClient.py index c3c34e056..f64c08336 100644 --- a/components/rpc/PhonieboxRpcClient.py +++ b/components/rpc/PhonieboxRpcClient.py @@ -1,7 +1,7 @@ import zmq import json -class phoniebox_object_access_queue: +class PhonieboxRpcClient: def __init__(self): #self.objects = objects @@ -16,7 +16,7 @@ def connect(self,addr= None): self.queue.setsockopt(zmq.LINGER, 200) self.queue.connect(addr) - def phonie_enqueue(self, request): + def enqueue(self, request): #todo check reqest print (request) self.queue.send_string(json.dumps(request)) @@ -34,10 +34,10 @@ def phonie_enqueue(self, request): if __name__ == "__main__": import time - test_objects = [{'obj':'volume','cmd':'get','param':None}, - {'obj':'volume','cmd':'set','param':{'volume':30}}, - {'obj':'volume','cmd':'set','param':{'volume':33}}, - {'obj':'volume','cmd':'set','param':{'volume':36}}] + test_objects = [{'object':'volume','method':'get','params':None}, + {'object':'volume','method':'set','params':{'volume':30}}, + {'object':'volume','method':'set','params':{'volume':33}}, + {'object':'volume','method':'set','params':{'volume':36}}] print ("Test Phonibox Object Acces Client") queue = phoniebox_object_access_queue() diff --git a/components/rpc/PhonieboxRpcServer.py b/components/rpc/PhonieboxRpcServer.py index b65fe8e92..627a520c4 100644 --- a/components/rpc/PhonieboxRpcServer.py +++ b/components/rpc/PhonieboxRpcServer.py @@ -6,7 +6,7 @@ import json import time -class phoniebox_rpc_server: +class PhonieboxRpcServer: def __init__(self,objects): self.objects = objects diff --git a/htdocs/api/zmq.php b/htdocs/api/PhonieboxRpcClient.php similarity index 95% rename from htdocs/api/zmq.php rename to htdocs/api/PhonieboxRpcClient.php index d15c38161..df114bded 100755 --- a/htdocs/api/zmq.php +++ b/htdocs/api/PhonieboxRpcClient.php @@ -1,6 +1,6 @@ ['player','play',''], 'next'=>['player','next',''], 'prev'=>['player','prev',''], - 'replay'=>'-c=playerreplay -v=playlist', + 'replay'=>['player','replay',[]], 'pause'=> ['player','pause',''], - 'repeat'=>'-c=playerrepeat -v=playlist', - 'single'=> 'playerrepeat -v=single', - 'repeatoff'=>'playerrepeat -v=off', + 'repeat'=> ['player','repeatmode',['mode'=>'repeat']], + 'single'=> ['player','repeatmode',['mode'=>'single']], + 'repeatoff'=>['player','repeatmode',['mode'=>'off']], 'seekBack'=> ['player','seek',['time' => '-15']], 'seekAhead'=> ['player','seek',['time' => '+15']], - 'seekPosition' => 'playerseek', + 'seekPosition' => ['player','seek',['time' => '0']], 'stop'=>['player','stop',''], 'mute'=> ['volume','mute',''], 'volumeup'=> ['volume','inc',['step' => 5]], @@ -67,7 +67,7 @@ function handlePut() { if (array_key_exists($inputCommand,$command_map)==True) { $cmd = $command_map[$inputCommand]; - $response = phonie_enquene(array('object'=>$cmd[0],'method'=>$cmd[1],'params'=>$cmd[2])); + $response = PhonieboxRpcEnquene(array('object'=>$cmd[0],'method'=>$cmd[1],'params'=>$cmd[2])); } else { @@ -85,7 +85,7 @@ function handleGet() { global $debugLoggingConf; global $globalConf; - $json_response = phonie_enquene(array('object'=>'player','method'=>'playerstatus','param'=>'')); + $json_response = PhonieboxRpcEnquene(array('object'=>'player','method'=>'playerstatus','param'=>'')); $responseList = json_decode ( $json_response,true)['resp']; //so solltes es aussehen: diff --git a/htdocs/api/playlist.php b/htdocs/api/playlist.php index cb242ae6e..e43d63c59 100755 --- a/htdocs/api/playlist.php +++ b/htdocs/api/playlist.php @@ -1,7 +1,7 @@ 'player','method'=>'playlistinfo','param'=>'')); + $playlist_json = PhonieboxRpcEnquene(array('object'=>'player','method'=>'playlistinfo','param'=>'')); $playList = array("tracks" => json_decode ( $playlist_json,true)['resp']); /* sample array, uncomment for checking frontend * diff --git a/htdocs/api/playlist/appendFileToPlaylist.php b/htdocs/api/playlist/appendFileToPlaylist.php index ebd8ee43a..5f38de980 100755 --- a/htdocs/api/playlist/appendFileToPlaylist.php +++ b/htdocs/api/playlist/appendFileToPlaylist.php @@ -4,7 +4,7 @@ /** * Appends a given file to the current playlist (and starts playing) */ -require_once("../zmq.php"); +require_once("../PhonieboxRpcClient.php"); /* * debug? Conf file line: @@ -21,14 +21,12 @@ if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { file_put_contents("../../../logs/debug.log", "\n # \$body: " . $body , FILE_APPEND | LOCK_EX); } - phonie_enquene(['object'=>'player','method'=>'playlistappend','param'=>['songid'=>$body ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'playlistappend','param'=>['songid'=>$body ]]); } else { $file = $_GET["file"]; if ($file !== "") { print "Playing file " . $file; - phonie_enquene(['object'=>'player','method'=>'playlistappend','param'=>['songid'=>$file ]]); - - require_once("../zmq.php"); + PhonieboxRpcEnquene(['object'=>'player','method'=>'playlistappend','param'=>['songid'=>$file ]]); }else{ http_response_code(405); } diff --git a/htdocs/api/playlist/moveDownSongInPlaylist.php b/htdocs/api/playlist/moveDownSongInPlaylist.php index bfbc13faf..1e2c9d6d7 100755 --- a/htdocs/api/playlist/moveDownSongInPlaylist.php +++ b/htdocs/api/playlist/moveDownSongInPlaylist.php @@ -6,7 +6,7 @@ /** * Moves a song within the current playlist down. */ -require_once("../zmq.php"); +require_once("../PhonieboxRpcClient.php"); /* * debug? Conf file line: @@ -24,7 +24,7 @@ file_put_contents("../../../logs/debug.log", "\n # \$body: " . $body , FILE_APPEND | LOCK_EX); } if (is_numeric($body)) { - phonie_enquene(['object'=>'player','method'=>'move','params'=>['songid'=>$body,'step' => -1 ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'move','params'=>['songid'=>$body,'step' => -1 ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/moveUpSongInPlaylist.php b/htdocs/api/playlist/moveUpSongInPlaylist.php index d0d8c8ccf..74367a503 100755 --- a/htdocs/api/playlist/moveUpSongInPlaylist.php +++ b/htdocs/api/playlist/moveUpSongInPlaylist.php @@ -4,7 +4,7 @@ /** * Moves a song within the current playlist up. */ -require_once("../zmq.php"); +require_once("../PhonieboxRpcClient.php"); /* * debug? Conf file line: @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - phonie_enquene(['object'=>'player','method'=>'move','params'=>['songid'=>$body,'step' => 1 ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'move','params'=>['songid'=>$body,'step' => 1 ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/playsinglefile.php b/htdocs/api/playlist/playsinglefile.php index 911bbb24f..e0c289798 100755 --- a/htdocs/api/playlist/playsinglefile.php +++ b/htdocs/api/playlist/playsinglefile.php @@ -4,7 +4,7 @@ /** * Starts to play a single song in a new playlist. */ -require_once("../zmq.php"); +require_once("../PhonieboxRpcClient.php"); /* * debug? Conf file line: @@ -22,12 +22,12 @@ file_put_contents("../../../logs/debug.log", "\n # \$body: " . $body , FILE_APPEND | LOCK_EX); } // This script always returns with returncode 1, so we cannot check that the returncode is 0 - phonie_enquene(['object'=>'player','method'=>'playsingle','param'=>['songid'=>$body ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'playsingle','param'=>['songid'=>$body ]]); } else { $file = $_GET["file"]; if ($file !== "") { print "Playing file " . $file; - phonie_enquene(['object'=>'player','method'=>'playsingle','param'=>['songid'=>$file ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'playsingle','param'=>['songid'=>$file ]]); }else{ http_response_code(405); } diff --git a/htdocs/api/playlist/removeSongFromPlaylist.php b/htdocs/api/playlist/removeSongFromPlaylist.php index 9cfbf3588..818d66af1 100755 --- a/htdocs/api/playlist/removeSongFromPlaylist.php +++ b/htdocs/api/playlist/removeSongFromPlaylist.php @@ -4,7 +4,7 @@ /** * Removes a song from the current playlist. */ -require_once("../zmq.php"); +require_once("../PhonieboxRpcClient.php"); /* * debug? Conf file line: @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - phonie_enquene(['object'=>'player','method'=>'remove','param'=>['songid'=>$body ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'remove','param'=>['songid'=>$body ]]); } else { http_response_code(400); } diff --git a/htdocs/api/playlist/song.php b/htdocs/api/playlist/song.php index c77d0b223..11201c637 100755 --- a/htdocs/api/playlist/song.php +++ b/htdocs/api/playlist/song.php @@ -4,7 +4,7 @@ /** * Starts to play a song in the current playlist. */ -require_once("../zmq.php"); +require_once("../PhonieboxRpcClient.php"); /* * debug? Conf file line: @@ -23,7 +23,7 @@ } if (is_numeric($body)) { // This script always returns with returncode 1, so we cannot check that the returncode is 0 - phonie_enquene(['object'=>'player','method'=>'play','params'=>['songid'=>$body ]]); + PhonieboxRpcEnquene(['object'=>'player','method'=>'play','params'=>['songid'=>$body ]]); } else { http_response_code(400); } diff --git a/htdocs/api/volume.php b/htdocs/api/volume.php index 3a41da7b5..809afe60e 100755 --- a/htdocs/api/volume.php +++ b/htdocs/api/volume.php @@ -1,7 +1,7 @@ 'volume','method'=>'get')); + $json_response = PhonieboxRpcEnquene(array('object'=>'volume','method'=>'get')); echo json_decode ( $json_response,true)['resp']['volume']; } else if ($_SERVER['REQUEST_METHOD'] === 'PUT') { $body = file_get_contents('php://input'); if (is_numeric($body)) { echo $body; - phonie_enquene(array('object'=>'volume','method'=>'set','params'=>array('volume'=>(int)$body))); + PhonieboxRpcEnquene(array('object'=>'volume','method'=>'set','params'=>array('volume'=>(int)$body))); } else { http_response_code(400); } diff --git a/htdocs/inc.header.php b/htdocs/inc.header.php index 51cafcea6..feb542f90 100755 --- a/htdocs/inc.header.php +++ b/htdocs/inc.header.php @@ -1,7 +1,7 @@ $paramValue) { if(isset($commandToAction[$paramKey]) ) { - $json_response = phonie_enquene($commandToAction[$paramKey]); + $json_response = PhonieboxRpcEnquene($commandToAction[$paramKey]); } else { error_log("calling script in inc_header via commandToAction: \"".$paramKey."\" Val:\"".$paramValue."\"", 0); } diff --git a/scripts/Reader.py b/scripts/Reader.py deleted file mode 100755 index 50dceb057..000000000 --- a/scripts/Reader.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522, RDM6300 or PN532. -# Please use the github issue threads to share bugs and improvements -# or create pull requests. - -import os.path -import sys - -import RPi.GPIO as GPIO -import logging - -from evdev import InputDevice, categorize, ecodes, list_devices - -logger = logging.getLogger(__name__) - -def get_devices(): - devices = [InputDevice(fn) for fn in list_devices()] - devices.append(NonUsbDevice('MFRC522')) - devices.append(NonUsbDevice('RDM6300')) - devices.append(NonUsbDevice('PN532')) - return devices - - -class NonUsbDevice(object): - name = None - - def __init__(self, name): - self.name = name - - -class UsbReader(object): - def __init__(self, device): - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - self.dev = device - - def readCard(self): - from select import select - stri = '' - key = '' - while key != 'KEY_ENTER': - select([self.dev], [], []) - for event in self.dev.read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - key = ecodes.KEY[event.code] - return stri[:-1] - - -class Mfrc522Reader(object): - def __init__(self): - import pirc522 - self.device = pirc522.RFID() - - def readCard(self): - # Scan for cards - self.device.wait_for_tag() - (error, tag_type) = self.device.request() - - if not error: - logger.info("Card detected.") - # Perform anti-collision detection to find card uid - (error, uid) = self.device.anticoll() - if not error: - card_id = ''.join((str(x) for x in uid)) - logger.info(card_id) - return card_id - logger.debug("No Device ID found.") - return None - - @staticmethod - def cleanup(): - GPIO.cleanup() - -class Rdm6300Reader: - def __init__(self,param = None): - import serial - device = '/dev/ttyS0' - baudrate = 9600 - ser_timeout = 0.1 - self.last_card_id = '' - try: - self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) - self.serial_SerialException = serial.SerialException - except serial.SerialException as e: - logger.error(e) - exit(1) - - self.number_format = '' - if param is not None: - nf = param.get("numberformat") - if nf is not None: - self.number_format = nf - - def convert_to_weigand26_when_checksum_ok(self,raw_card_id): - weigand26 = [] - xor = 0 - for i in range(0, len(raw_card_id)>>1): - val = int(raw_card_id[i*2:i*2+2],16) - if (i < 5): - xor = xor ^ val - weigand26.append(val) - else: - chk = val - if (chk == val): - return weigand26 - else: - return None - - def readCard(self): - byte_card_id = bytearray() - - try: - while True: - try: - wait_for_start_byte = True - while True: - read_byte = self.rfid_serial.read() - - if (wait_for_start_byte): - if read_byte == b'\x02': - wait_for_start_byte = False - else: - if read_byte != b'\x03': #could stuck here, check len? check timeout by len == 0?? - byte_card_id.extend(read_byte) - else: - break - - raw_card_id = byte_card_id.decode('ascii') - byte_card_id.clear() - self.rfid_serial.reset_input_buffer() - - if len(raw_card_id) == 12 : - w26 = self.convert_to_weigand26_when_checksum_ok(raw_card_id) - if (w26 is not None): - #print ("factory code is ignored" ,w26[0]) - - if self.number_format == 'card_id_dec': - #this will return a 10 Digit card ID e.g. 0006762840 - card_id = '{0:010d}'.format( (w26[1] << 24) + (w26[2] << 16) + (w26[3] << 8) + w26[4]) - elif self.number_format == 'card_id_float': - #this will return a fractional card ID e.g. 103,12632 - card_id='{0:d},{1:05d}'.format( ((w26[1] << 8) + w26[2]) , ((w26[3] << 8) + w26[4])) - else: - #this will return the raw (original) card ID e.g. 070067315809 - card_id = raw_card_id - - if card_id != self.last_card_id: - self.last_card_id = card_id - return self.last_card_id - - except ValueError as ve: - logger.error(ve) - - except self.serial_SerialException as se: - logger.error(se) - - def cleanup(self): - self.rfid_serial.close() - - -class Pn532Reader: - def __init__(self): - from py532lib.i2c import Pn532_i2c - from py532lib.mifare import Mifare - pn532 = Pn532_i2c() - self.device = Mifare() - self.device.SAMconfigure() - self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY) - - def readCard(self): - return str(+int('0x' + self.device.scan_field().hex(), 0)) - - def cleanup(self): - # Not sure if something needs to be done here. - logger.debug("PN532Reader clean up.") - -class Reader(object): - def __init__(self): - path = os.path.dirname(os.path.realpath(__file__)) - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - device_name = f.read() - - if device_name == 'MFRC522': - self.reader = Mfrc522Reader() - elif device_name == 'RDM6300': - self.reader = Rdm6300Reader({'numberformat':'card_id_float'}) - #self.reader = Rdm6300Reader() - elif device_name == 'PN532': - self.reader = Pn532Reader() - else: - try: - device = [device for device in get_devices() if device.name == device_name][0] - self.reader = UsbReader(device) - except IndexError: - sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) diff --git a/scripts/Reader.py.Multi b/scripts/Reader.py.Multi deleted file mode 100644 index e727d7b5d..000000000 --- a/scripts/Reader.py.Multi +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# There are a variety of RFID readers out there, USB and non-USB variants. -# This might create problems in recognizing the reader you are using. -# We haven't found the silver bullet yet. If you can contribute to this -# quest, please comment in the issue thread or create pull requests. -# ALTERNATIVE SCRIPTS: -# If you encounter problems with this script Reader.py -# consider and test one of the alternatives in the same scripts folder. -# Replace the Reader.py file with one of the following files: -# * Reader.py.experimental -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522, RDM6300 or PN532. -# * Reader.py.kkmoonRFIDreader -# KKMOON RFID Reader which appears twice in the devices list as HID 413d:2107 -# and this required to check "if" the device is a keyboard. - -# import string -# import csv -import os.path -import sys - -from evdev import InputDevice, ecodes, list_devices -from select import select - - -def get_devices(): - return [InputDevice(fn) for fn in list_devices()] - - -class Reader: - reader = None - - def __init__(self): - self.reader = self - devs = list() - path = os.path.dirname(os.path.realpath(__file__)) - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - device_keys = f.readlines() - devices = get_devices() - for device in devices: - for dev_key in device_keys: - dev_name, dev_phys = dev_key.rstrip().split(';', 1) - if device.name == dev_name and device.phys == dev_phys: - devs.append(device) - break - for dev in devs: - try: - dev - except: - sys.exit('Could not find the device %s\n. Make sure is connected' % dev.name) - - str_devs = ','.join([str(x) for x in devs]) - # print("Devs: " + str_devs) - self.devices = map(InputDevice, str_devs) - self.devices = {dev.fd: dev for dev in devs} - - def readCard(self): - stri = '' - key = '' - while key != 'KEY_ENTER': - r, w, x = select(self.devices, [], []) - for fd in r: - for event in self.devices[fd].read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - # print( keys[ event.code ] ) - key = ecodes.KEY[event.code] - return stri[:-1] diff --git a/scripts/Reader.py.experimental.Multi b/scripts/Reader.py.experimental.Multi deleted file mode 100644 index 326a5bd86..000000000 --- a/scripts/Reader.py.experimental.Multi +++ /dev/null @@ -1,211 +0,0 @@ -#!/usr/bin/env python3 -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522, RDM6300 or PN532. -# Please use the github issue threads to share bugs and improvements -# or create pull requests. -import multiprocessing -try: - from multiprocessing import SimpleQueue -except ImportError: - from multiprocessing.queues import SimpleQueue -import os.path -import sys -import serial -import string -import RPi.GPIO as GPIO -import logging -from enum import Enum -from evdev import InputDevice, ecodes, list_devices -# Workaround: when using RC522 reader with pirc522 pkg the py532lib pkg may not be installed and vice-versa -try: - import pirc522 - from py532lib.i2c import * - from py532lib.mifare import * -except ImportError: - pass - -logger = logging.getLogger(__name__) - -class EDevices(Enum): - MFRC522 = 0 - RDM6300 = 1 - PN532 = 2 - - -def get_devices(): - devices = [InputDevice(fn) for fn in list_devices()] - devices.append(NonUsbDevice(EDevices.MFRC522.name)) - devices.append(NonUsbDevice(EDevices.RDM6300.name)) - devices.append(NonUsbDevice(EDevices.PN532.name)) - return devices - - -class NonUsbDevice(object): - name = None - - def __init__(self, name, phys=''): - self.name = name - self.phys = phys - - -class UsbReader(object): - def __init__(self, device): - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - self.dev = device - - def readCard(self): - from select import select - stri = '' - key = '' - while key != 'KEY_ENTER': - select([self.dev], [], []) - for event in self.dev.read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - key = ecodes.KEY[event.code] - return stri[:-1] - - -class Mfrc522Reader(object): - def __init__(self): - self.device = pirc522.RFID() - - def readCard(self): - # Scan for cards - self.device.wait_for_tag() - (error, tag_type) = self.device.request() - - if not error: - logger.info("Card detected.") - # Perform anti-collision detection to find card uid - (error, uid) = self.device.anticoll() - if not error: - card_id = ''.join((str(x) for x in uid)) - logger.info(card_id) - return card_id - logger.debug("No Device ID found.") - return None - - @staticmethod - def cleanup(): - GPIO.cleanup() - - -class Rdm6300Reader: - def __init__(self): - device = '/dev/ttyS0' - baudrate = 9600 - ser_timeout = 0.1 - self.last_card_id = '' - try: - self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) - except serial.SerialException as e: - logger.error(e) - exit(1) - - def readCard(self): - byte_card_id = b'' - - try: - while True: - try: - read_byte = self.rfid_serial.read() - - if read_byte == b'\x02': # start byte - while read_byte != b'\x03': # end bye - read_byte = self.rfid_serial.read() - byte_card_id += read_byte - - card_id = byte_card_id.decode('utf-8') - byte_card_id = '' - card_id = ''.join(x for x in card_id if x in string.printable) - - # Only return UUIDs with correct length - if len(card_id) == 12 and card_id != self.last_card_id: - self.last_card_id = card_id - self.rfid_serial.reset_input_buffer() - return self.last_card_id - - else: # wrong UUID length or already send that UUID last time - self.rfid_serial.reset_input_buffer() - - except ValueError as ve: - logger.errror(ve) - - except serial.SerialException as se: - logger.error(se) - - def cleanup(self): - self.rfid_serial.close() - - -class Pn532Reader: - def __init__(self): - pn532 = Pn532_i2c() - self.device = Mifare() - self.device.SAMconfigure() - self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY) - - def readCard(self): - return str(+int('0x' + self.device.scan_field().hex(), 0)) - - def cleanup(self): - # Not sure if something needs to be done here. - logger.debug("PN532Reader clean up.") - - -class Reader(object): - def __init__(self): - self.reader = self - self.devs = list() - path = os.path.dirname(os.path.realpath(__file__)) - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - device_keys = f.readlines() - devices = get_devices() - for device in devices: - for dev_key in device_keys: - dev_name_phys = dev_key.rstrip().split(';', 1) - dev_name = dev_name_phys[0] - dev_phys = '' - if len(dev_name_phys) > 1: - dev_phys = dev_name_phys[1] - if device.name == dev_name and device.phys == dev_phys: - if dev_name == 'MFRC522': - self.devs.append(Mfrc522Reader()) - elif dev_name == 'RDM6300': - self.devs.append(Rdm6300Reader()) - elif dev_name == 'PN532': - self.devs.append(Pn532Reader()) - else: - try: - usb_reader = UsbReader(device) - self.devs.append(usb_reader) - except IndexError: - sys.exit('Could not find the device %s.\n Make sure it is connected' % dev_name) - break - - def readCard(self): - que = SimpleQueue() - threads_list = list() - - for dev in self.devs: - t = multiprocessing.Process(target=lambda q: q.put(dev.readCard()), args=(que,)) - t.start() - threads_list.append(t) - - found_result = False - while not found_result: - for process in threads_list: - process.join(0.001) - if not process.is_alive(): - found_result = True - break - - for process in threads_list: - process.terminate() - - return que.get() diff --git a/scripts/Reader.py.kkmoonRFIDreader b/scripts/Reader.py.kkmoonRFIDreader deleted file mode 100755 index 10853b41b..000000000 --- a/scripts/Reader.py.kkmoonRFIDreader +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# This alternative Reader.py script was meant to fixes issues with -# KKMOON RFID Reader which appears twice in the devices list as HID 413d:2107 -# and this required to check "if" the device is a keyboard. -# Please use the github issue threads to share bugs and improvements -# or create pull requests. - -import os.path -import sys - - -from evdev import InputDevice, categorize, ecodes, list_devices -from select import select - - -def get_devices(): - return [InputDevice(fn) for fn in list_devices()] - - -class Reader: - - def is_Keyboard(self, device): - device_key_list = device.capabilities()[ecodes.EV_KEY] - - if self.mandatory_keys.issubset(device_key_list) and self.reserved_key.isdisjoint(device_key_list): - return True - else: - return False - - def __init__(self): - path = os.path.dirname(os.path.realpath(__file__)) - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - self.mandatory_keys = {i for i in range(ecodes.KEY_ESC, ecodes.KEY_D)} - self.reserved_key = {0} - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - deviceName = f.read() - devices = [InputDevice(fn) for fn in list_devices()] - for device in devices: - if device.name == deviceName and self.is_Keyboard(device): - self.dev = device - break - try: - self.dev - except: - sys.exit('Could not find the device %s\n. Make sure is connected' % deviceName) - - def readCard(self): - stri = '' - key = '' - while key != 'KEY_ENTER': - r, w, x = select([self.dev], [], []) - for event in self.dev.read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - # print( keys[ event.code ] ) - key = ecodes.KEY[event.code] - return stri[:-1] diff --git a/scripts/Reader.py.original b/scripts/Reader.py.original deleted file mode 100755 index 49f1957bc..000000000 --- a/scripts/Reader.py.original +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# There are a variety of RFID readers out there, USB and non-USB variants. -# This might create problems in recognizing the reader you are using. -# We haven't found the silver bullet yet. If you can contribute to this -# quest, please comment in the issue thread or create pull requests. -# ALTERNATIVE SCRIPTS: -# If you encounter problems with this script Reader.py -# consider and test one of the alternatives in the same scripts folder. -# Replace the Reader.py file with one of the following files: -# * Reader.py.experimental -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522, RDM6300 or PN532 -# * Reader.py.kkmoonRFIDreader -# KKMOON RFID Reader which appears twice in the devices list as HID 413d:2107 -# and this required to check "if" the device is a keyboard. - - -import os.path -import sys - -from evdev import InputDevice, categorize, ecodes, list_devices -from select import select -import logging -logger = logging.getLogger(__name__) - - -def get_devices(): - return [InputDevice(fn) for fn in list_devices()] - - -class Reader: - reader = None - - def __init__(self): - logger.debug('Initialize Reader') - self.reader = self - path = os.path.dirname(os.path.realpath(__file__)) - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - deviceNameFile = os.path.join(path, 'deviceName.txt') - if not os.path.isfile(deviceNameFile): - logger.error('deviceName not set in {deviceNameFile}'.format(deviceNameFile=deviceNameFile)) - sys.exit('Please run RegisterDevice.py first') - else: - with open(deviceNameFile, 'r') as f: - deviceName = f.read() - logging.debug('DeviceName {deviceName}'.format(deviceName=deviceName)) - devices = get_devices() - for device in devices: - if device.name == deviceName: - self.dev = device - logger.debug('Found device') - break - try: - self.dev - except: - sys.exit('Could not find the device %s\n. Make sure is connected' % deviceName) - - def readCard(self): - logger.debug('readCard') - stri = '' - key = '' - while key != 'KEY_ENTER': - r, w, x = select([self.dev], [], []) - for event in self.dev.read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - # print( keys[ event.code ] ) - key = ecodes.KEY[event.code] - return stri[:-1] diff --git a/scripts/RegisterDevice.py b/scripts/RegisterDevice.py deleted file mode 100755 index 026a4ecb3..000000000 --- a/scripts/RegisterDevice.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 - -import os.path -from Reader import get_devices - -devices = get_devices() -path = os.path.dirname(os.path.realpath(__file__)) -i = 0 -print("Choose the reader from list") -for dev in devices: - print(i, dev.name) - i += 1 - -dev_id = int(input('Device Number: ')) - -with open(path + '/deviceName.txt', 'w') as f: - f.write(devices[dev_id].name) - f.close() diff --git a/scripts/RegisterDevice.py.Multi b/scripts/RegisterDevice.py.Multi deleted file mode 100644 index 66dc16898..000000000 --- a/scripts/RegisterDevice.py.Multi +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 - -import os.path -import subprocess - -JUKEBOX_HOME_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) - - -def runCmd(cmd, wait=True): - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) - (output, err) = p.communicate() - if wait: - p.wait() - return output - - -def setupPN532(): - answer = input('Please make sure that the PN532 reader is wired up correctly ' - 'to the GPIO ports before continuing...\n Continue?: [Y/n]') - if not answer or answer[0] != 'Y': - return False - print("Activating I2C interface...\n") - runCmd("sudo raspi-config nonint do_i2c 0") - print("Installing i2c-tools...\n") - runCmd("sudo apt-get -qq -y install i2c-tools") - print("Checking if PN532 RFID reader is found through I2C...\n") - output = runCmd("sudo i2cdetect -y 1") - if "24" in str(output): - print(" PN532 was found.\n") - else: - print(" ERROR: PN532 was not found.\n") - print(str(output)) - return False - print("Installing Python requirements for PN532...\n") - runCmd("sudo python3 -m pip install --upgrade --force-reinstall " - "-q -r {}/components/rfid-reader/PN532/requirements.txt".format(JUKEBOX_HOME_DIR)) - print("Done") - return True - - -def setupMFRC522(): - answer = input('Please make sure that the RC522 reader is wired up correctly ' - 'to the GPIO ports before continuing...\n Continue?: [Y/n]') - if not answer or answer[0] != 'Y': - return False - print("Installing Python requirements for RC522...\n") - runCmd("sudo python3 -m pip install --upgrade --force-reinstall " - "-q -r {}/components/rfid-reader/RC522/requirements.txt".format(JUKEBOX_HOME_DIR)) - print("Done") - return True - - -runCmd("cp {0}/scripts/Reader.py.experimental.Multi {1}/scripts/Reader.py".format(JUKEBOX_HOME_DIR, JUKEBOX_HOME_DIR)) -from Reader import get_devices, EDevices -list_dev_ids = list() -devices = get_devices() - - -def addDevice(): - i = 0 - print("Choose the reader from list") - for dev in devices: - if i not in list_dev_ids: - print(i, dev.name + str(dev.phys)) - i += 1 - dev_id = int(input('Device Number: ')) - if dev_id not in list_dev_ids: - if devices[dev_id].name == EDevices.PN532.name: - if not setupPN532(): - return - if devices[dev_id].name == EDevices.MFRC522.name: - if not setupMFRC522(): - return - list_dev_ids.append(dev_id) - - -def configureDevices(): - addDevice() - while True: - answer = input('Do you want to add another device: [Y/n]') - if not answer or answer[0] != 'Y': - break - addDevice() - - -print("Stopping phoniebox-rfid-reader service...\n") -runCmd("sudo systemctl stop phoniebox-rfid-reader.service") - -configureDevices() - -path = os.path.dirname(os.path.realpath(__file__)) -with open(path + '/deviceName.txt', 'w') as f: - for sel_dev_id in list_dev_ids: - f.write(devices[sel_dev_id].name + ";" + devices[sel_dev_id].phys + '\n') - f.close() - -print("Restarting phoniebox-rfid-reader service...\n") -runCmd("sudo systemctl start phoniebox-rfid-reader.service") - -runCmd("sudo chown pi:www-data {}/scripts/deviceName.txt".format(JUKEBOX_HOME_DIR)) -runCmd("sudo chmod 644 {}/scripts/deviceName.txt".format(JUKEBOX_HOME_DIR)) - -print("Register Device(s) Done!") diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100755 index e69de29bb..000000000 diff --git a/scripts/activate_amplifier.py b/scripts/activate_amplifier.py deleted file mode 100755 index c51ecf59b..000000000 --- a/scripts/activate_amplifier.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 - -import sys -from signal import pause -import RPi.GPIO as GPIO - -# script to activate and deactivate an amplifier, power led, etc. using a GPIO -# pin on power up / down - -# see for an example implementation with a PAM8403 digital amplifier -# (PAM pin 12 connected to GPIO 26) -# https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Hardware-Hack-PAM8403-Poweroff - -# change this value based on which GPIO port the amplifier or other devices are connected to -# Flexible Pinout -AMP_GPIO = 26 -# Classic Pinout -# AMP_GPIO = 23 - -# setup RPi lib to control output pin -# we do not cleanup the GPIO because we want the pin low = off after program exit -# the resulting warning can be ignored -GPIO.setwarnings(False) -GPIO.setmode(GPIO.BCM) -GPIO.setup(AMP_GPIO, GPIO.OUT) - - -def set_amplifier(status): - if status: - print("Setting amplifier: ON") - GPIO.output(AMP_GPIO, GPIO.HIGH) - else: - print("Setting amplifier: OFF") - GPIO.output(AMP_GPIO, GPIO.LOW) - - -if __name__ == "__main__": - try: - set_amplifier(True) - pause() - except KeyboardInterrupt: - # turn the relay off - set_amplifier(False) - print("\nExiting amplifier control\n") - # exit the application - sys.exit(0) diff --git a/scripts/daemon_rfid_reader.py b/scripts/daemon_rfid_reader.py deleted file mode 100755 index 5bf13d43a..000000000 --- a/scripts/daemon_rfid_reader.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import os -import subprocess -import time -import re - -from Reader import Reader - -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) -ch = logging.StreamHandler() -ch.setLevel(logging.INFO) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -ch.setFormatter(formatter) -logger.addHandler(ch) - -reader = Reader() - -# get absolute path of this script -dir_path = os.path.dirname(os.path.realpath(__file__)) -logger.info('Dir_PATH: {dir_path}'.format(dir_path=dir_path)) - -# get control card ids -file_path = os.path.dirname(__file__) -if file_path != "": - os.chdir(file_path) - -# vars for ensuring delay between same-card-swipes -ssp = open('../settings/Second_Swipe_Pause', 'r') -same_id_delay = ssp.read().strip() -sspc = open('../settings/Second_Swipe_Pause_Controls', 'r') -sspc_nodelay = sspc.readline().strip() -previous_id = "" -previous_time = time.time() - -# create array for control card ids -cards = [] - -# open file and read the content in a list -with open('../settings/global.conf', 'r') as filehandle: - filecontents = filehandle.readlines() - - for line in filecontents: - cids = line[:-1] - cards.append(cids) - - -extract = [s for s in cards if s.startswith('CMD')] -string = ''.join(extract) - -# if controlcards delay is deactivated, let the cards pass, otherwise, they have to wait... -if sspc_nodelay == "ON": - ids = re.findall("(\d+)", string) -else: - ids = "" - -while True: - # reading the card id - # NOTE: it's been reported that KKMOON Reader might need the following line altered. - # Instead of: - # cardid = reader.reader.readCard() - # change the line to: - # cardid = reader.readCard() - # See here for (German ;) details: - # https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/551 - cardid = reader.reader.readCard() - try: - # start the player script and pass on the cardid (but only if new card or otherwise - # "same_id_delay" seconds have passed) - if cardid is not None: - if cardid != previous_id or (time.time() - previous_time) >= float(same_id_delay) or cardid in str(ids): - logger.info('Trigger Play Cardid={cardid}'.format(cardid=cardid)) - subprocess.call([dir_path + '/rfid_trigger_play.sh --cardid=' + cardid], shell=True) - previous_id = cardid - - else: - logger.debug('Ignoring Card id {cardid} due to same-card-delay, delay: {same_id_delay}'.format( - cardid=cardid, - same_id_delay=same_id_delay - )) - - previous_time = time.time() - - except OSError as e: - logger.error('Execution failed: {e}'.format(e=e)) diff --git a/scripts/helperscripts/Analytics_AfterInstallScript.sh b/scripts/helperscripts/Analytics_AfterInstallScript.sh deleted file mode 100755 index 2b68e9551..000000000 --- a/scripts/helperscripts/Analytics_AfterInstallScript.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -# Check all conf files copied and modified after installation script - -echo "************************************" -echo "*** PHONIEBOX INFO" -echo "*** version:" $(cat /home/pi/RPi-Jukebox-RFID/settings/version) -echo "*** edition:" $(cat /home/pi/RPi-Jukebox-RFID/settings/edition) -echo "*** Audio_iFace_Name:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Audio_iFace_Name) -echo "*** Audio_Folders_Path:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Audio_Folders_Path) -echo "*** Audio_Volume_Change_Step:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Audio_Volume_Change_Step) -echo "*** Max_Volume_Limit:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Max_Volume_Limit) -echo "*** Idle_Time_Before_Shutdown:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Idle_Time_Before_Shutdown) -echo "*** Second_Swipe:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Second_Swipe) -echo "*** Playlists_Folders_Path:" $(cat /home/pi/RPi-Jukebox-RFID/settings/Playlists_Folders_Path) -echo "*** ShowCover:" $(cat /home/pi/RPi-Jukebox-RFID/settings/ShowCover) - -echo "************************************" -echo "*** CONF FILES DEFAULT" -echo " " - -echo "*** /etc/samba/smb.conf" -ls -lh /etc/samba/smb.conf -sudo cat /etc/samba/smb.conf | grep path= - -echo "*** /etc/lighttpd/lighttpd.conf" -ls -lh /etc/lighttpd/lighttpd.conf - -echo "*** /etc/lighttpd/conf-available/15-fastcgi-php.conf" -ls -lh /etc/lighttpd/conf-available/15-fastcgi-php.conf - -echo "*** /etc/php/7.3/fpm/php.ini" -ls -lh /etc/php/7.3/fpm/php.ini - -echo "*** /etc/sudoers" -ls -lh /etc/sudoers - -echo "*** /etc/systemd/system/phoniebox*" -ls -lh /etc/systemd/system/phoniebox-rfid-reader.service -ls -lh /etc/systemd/system/phoniebox-startup-scripts.service -ls -lh /etc/systemd/system/phoniebox-gpio-control.service -ls -lh /etc/systemd/system/phoniebox-idle-watchdog.service - -echo "*** /etc/mpd.conf" -ls -lh /etc/mpd.conf -sudo cat /etc/mpd.conf | grep music_directory -sudo cat /etc/mpd.conf | grep mixer_control - -echo "*** /etc/dhcpcd.conf" -ls -lh /etc/dhcpcd.conf -sudo cat /etc/dhcpcd.conf | grep ip_address -sudo cat /etc/dhcpcd.conf | grep routers -sudo cat /etc/dhcpcd.conf | grep domain_name_servers - -echo "*** /etc/wpa_supplicant/wpa_supplicant.conf" -ls -lh /etc/wpa_supplicant/wpa_supplicant.conf -sudo cat /etc/wpa_supplicant/wpa_supplicant.conf | grep country= -#sudo cat /etc/wpa_supplicant/wpa_supplicant.conf | grep ssid= -#sudo cat /etc/wpa_supplicant/wpa_supplicant.conf | grep psk= - -echo "************************************" -echo "*** +Spotify Edition" -echo " " - -echo "*** /etc/locale.gen" -ls -lh /etc/locale.gen - -echo "*** /etc/mopidy/mopidy.conf" -ls -lh /etc/mopidy/mopidy.conf -sudo cat /etc/mopidy/mopidy.conf | grep username -#sudo cat /etc/mopidy/mopidy.conf | grep password -#sudo cat /etc/mopidy/mopidy.conf | grep client_id -#sudo cat /etc/mopidy/mopidy.conf | grep client_secret - -echo "*** ~/.config/mopidy/mopidy.conf" -ls -lh ~/.config/mopidy/mopidy.conf -sudo cat ~/.config/mopidy/mopidy.conf | grep username -#sudo cat ~/.config/mopidy/mopidy.conf | grep password -#sudo cat ~/.config/mopidy/mopidy.conf | grep client_id -#sudo cat ~/.config/mopidy/mopidy.conf | grep client_secret - -sudo mopidyctl deps - -echo " " diff --git a/scripts/helperscripts/AssignIDs4Shortcuts.php b/scripts/helperscripts/AssignIDs4Shortcuts.php deleted file mode 100755 index 25ab712e3..000000000 --- a/scripts/helperscripts/AssignIDs4Shortcuts.php +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/php - ".$conf['path2shortcuts']."/".$pair['id']; - //print $exec."\n"; - exec($exec); - } -} - -/* -* Now replace the values in the sample script with IDs - if any -*/ -// -$bashdaemon = file_get_contents($conf['path2bashdaemonsource']); -$bashdaemon = str_replace($bashfind, $bashreplace, $bashdaemon); -file_put_contents($conf['path2bashdaemontarget'], $bashdaemon); - -function string_startsWith($haystack, $needle) { - /* - * returns true or false - */ - $length = strlen($needle); - return (substr($haystack, 0, $length) === $needle); -} - -function csv_read_file2array($file, $thead = TRUE) { - /* - * Reads a csv file into an array. - * The key for each set (row) will either be taken from the first row - * or will be 'col1', 'col2', etc. - * This function assumes that the first row is the column header (like thead). - * If this is not the case, pass on FALSE as the second value. - * Examples: - * $data = csv_read_file2array($inv['invdata']); // first csv line is column header - * $data = csv_read_file2array($inv['invdata'], FALSE); // first csv line are values, NOT column header - */ - $csv = array_map('str_getcsv', file($file)); - if($thead == FALSE) { - // first line is not thead, create one - $colcount = count($csv[0]); // number of columns - if($colcount < 1) { // this is an error, the file was corrupt - die("CSV file incorrect"); - } else { - $counter = 1; - $colhead = array(); - while($counter <= $colcount) { - $colhead[] = "col".$counter++; - } - array_unshift($csv, $colhead); // add as column header - } - } - array_walk($csv, function(&$a) use ($csv) { - $a = array_combine($csv[0], $a); - }); - array_shift($csv); // remove column header - return $csv; -} - -?> \ No newline at end of file diff --git a/scripts/helperscripts/CreateCsvFromShortcuts.php b/scripts/helperscripts/CreateCsvFromShortcuts.php deleted file mode 100755 index 5e5d23897..000000000 --- a/scripts/helperscripts/CreateCsvFromShortcuts.php +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/php - - foldername -// read files' content into array -foreach ($shortcutstemp as $shortcuttemp) { - $shortcuts[basename($shortcuttemp)] = trim(file_get_contents($shortcuttemp)); -} -//print "
"; print_r($shortcutstemp); print "
"; //??? -//print "
"; print_r($shortcuts); print "
"; //??? - -$csv = "\"id\",\"value\"\n"; - -foreach($shortcuts as $id => $value) { - $csv .= "\"".$id."\",\"".$value."\"\n"; -} -file_put_contents($conf['path2csvtarget'], $csv); -?> \ No newline at end of file diff --git a/scripts/helperscripts/CreatePodcastsKidsDeutsch.sh b/scripts/helperscripts/CreatePodcastsKidsDeutsch.sh deleted file mode 100755 index a6cef104b..000000000 --- a/scripts/helperscripts/CreatePodcastsKidsDeutsch.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Creates sample folders with files and streams -# inside the $AUDIOFOLDERSPATH directory - -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# ZZZ-Podcast-DLF-Kinderhoerspiele (dir) -# * podcast.txt (file) -# * http://www.kakadu.de/podcast-kinderhoerspiel.3420.de.podcast.xml (content) - -AUDIOFOLDERSPATH=`cat ../../settings/Audio_Folders_Path` - -mkdir $AUDIOFOLDERSPATH/PODCASTS - -mkdir $AUDIOFOLDERSPATH/PODCASTS/BR-Betthupferl -echo "https://feeds.br.de/betthupferl/feed.xml" > $AUDIOFOLDERSPATH/PODCASTS/BR-Betthupferl/podcast.txt - -mkdir $AUDIOFOLDERSPATH/PODCASTS/Kakadu -echo "http://www.kakadu.de/podcast-kakadu.2730.de.podcast.xml" > $AUDIOFOLDERSPATH/PODCASTS/Kakadu/podcast.txt - -mkdir $AUDIOFOLDERSPATH/PODCASTS/MDR-Figarino -echo "http://www.mdr.de/figarino/podcast/streiche102-podcast.xml" > $AUDIOFOLDERSPATH/PODCASTS/MDR-Figarino/podcast.txt - -mkdir $AUDIOFOLDERSPATH/PODCASTS/WDR-Baerenbude -echo "https://kinder.wdr.de/radio/kiraka/hoeren/podcast/baerenbude192.podcast" > $AUDIOFOLDERSPATH/PODCASTS/WDR-Baerenbude/podcast.txt - -mkdir $AUDIOFOLDERSPATH/PODCASTS/BR-Klaro-Nachrichten -echo "https://feeds.br.de/klaro-nachrichten-fuer-kinder/feed.xml" > $AUDIOFOLDERSPATH/PODCASTS/BR-Klaro-Nachrichten/podcast.txt - -mkdir $AUDIOFOLDERSPATH/PODCASTS/WDR-KiRaKa -echo "https://kinder.wdr.de/radio/kiraka/hoeren/podcast/kinderhoerspiel-podcast-108.podcast" > $AUDIOFOLDERSPATH/PODCASTS/WDR-KiRaKa/podcast.txt - -# chmod chown -sudo chown -R :www-data $AUDIOFOLDERSPATH/PODCASTS -sudo chmod -R 777 $AUDIOFOLDERSPATH/PODCASTS diff --git a/scripts/helperscripts/CreateSampleAudiofoldersStreams.sh b/scripts/helperscripts/CreateSampleAudiofoldersStreams.sh deleted file mode 100755 index e2f477873..000000000 --- a/scripts/helperscripts/CreateSampleAudiofoldersStreams.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -# Creates sample folders with files and streams -# inside the $AUDIOFOLDERSPATH directory - -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# move to this directory to make sure relative paths work -cd $PATHDATA - -# ZZZ-Podcast-DLF-Kinderhoerspiele (dir) -# * podcast.txt (file) -# * http://www.kakadu.de/podcast-kinderhoerspiel.3420.de.podcast.xml (content) - -AUDIOFOLDERSPATH=`cat ../../settings/Audio_Folders_Path` - -mkdir $AUDIOFOLDERSPATH/ZZZ/ - -mkdir $AUDIOFOLDERSPATH/ZZZ/Podcast-DLF-Kinderhoerspiele -echo "http://www.kakadu.de/podcast-kinderhoerspiel.3420.de.podcast.xml" > $AUDIOFOLDERSPATH/ZZZ/Podcast-DLF-Kinderhoerspiele/podcast.txt - -#mkdir $AUDIOFOLDERSPATH/ZZZ/Podcast-Kakadu -#echo "http://www.kakadu.de/podcast-kakadu.2730.de.podcast.xml" > $AUDIOFOLDERSPATH/ZZZ/Podcast-Kakadu/podcast.txt - -#mkdir $AUDIOFOLDERSPATH/ZZZ/Podcast-WDR-Hörspielspeicher -#echo "https://www1.wdr.de/mediathek/audio/hoerspiel-speicher/wdr_hoerspielspeicher150.podcast" > $AUDIOFOLDERSPATH/ZZZ/Podcast-WDR-Hörspielspeicher/podcast.txt - -mkdir $AUDIOFOLDERSPATH/ZZZ/This\ American\ Life\ Podcast -echo "http://feed.thisamericanlife.org/talpodcast" > $AUDIOFOLDERSPATH/ZZZ/This\ American\ Life\ Podcast/podcast.txt - -# ZZZ-LiveStream-Bayern2 (dir) -# * livestream.txt (file) -# * http://br-br2-nord.cast.addradio.de/br/br2/nord/mp3/56/stream.mp3 (content) - -mkdir $AUDIOFOLDERSPATH/ZZZ/LiveStream-Bayern2 -echo "http://br-br2-nord.cast.addradio.de/br/br2/nord/mp3/56/stream.mp3" > $AUDIOFOLDERSPATH/ZZZ/LiveStream-Bayern2/livestream.txt - -# ZZZ-MP3-StartUpSound (dir) -# * startupsound.mp3 (file) - -mkdir $AUDIOFOLDERSPATH/ZZZ/MP3-StartUpSound -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/MP3-StartUpSound/startupsound.mp3 - -# ZZZ MP3 Whitespace StartUpSound (dir) -# * startupsound.mp3 (file) - -mkdir $AUDIOFOLDERSPATH/ZZZ/MP3\ Whitespace\ StartUpSound -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/MP3\ Whitespace\ StartUpSound/startupsound.mp3 - -# ZZZ-AudioFormatsTest (dir) -# * startupsound.mp3 (file) - -mkdir $AUDIOFOLDERSPATH/ZZZ/Counting/ -cp $PATHDATA/../../misc/number* $AUDIOFOLDERSPATH/ZZZ/Counting/ - -mkdir $AUDIOFOLDERSPATH/ZZZ/ABC/ -cp $PATHDATA/../../misc/alphabet* $AUDIOFOLDERSPATH/ZZZ/ABC/ - -mkdir $AUDIOFOLDERSPATH/ZZZ/AudioFormatsTest -cp $PATHDATA/../../misc/audiofiletype* $AUDIOFOLDERSPATH/ZZZ/AudioFormatsTest/ - -########################################### -# Now doing the same with nested subfolders -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/SubMaster/startupsound.mp3 - -# start nested with jump back two levels -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/fff-threeSubs -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/SubMaster/fff-threeSubs/startupsound.mp3 -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/fff-threeSubs/twoSubs -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/SubMaster/fff-threeSubs/twoSubs/startupsound.mp3 -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/fff-threeSubs/twoSubs/oneSub -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/SubMaster/fff-threeSubs/twoSubs/oneSub/startupsound.mp3 - -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/1-LiveStream-Bayern2 -echo "http://br-br2-nord.cast.addradio.de/br/br2/nord/mp3/56/stream.mp3" > $AUDIOFOLDERSPATH/ZZZ/SubMaster/1-LiveStream-Bayern2/livestream.txt - -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/This\ American\ Life\ Podcast -echo "http://feed.thisamericanlife.org/talpodcast" > $AUDIOFOLDERSPATH/ZZZ/SubMaster/This\ American\ Life\ Podcast/podcast.txt - -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/100-MP3-StartUpSound -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/SubMaster/100-MP3-StartUpSound/startupsound.mp3 - -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/AAA\ MP3\ Whitespace\ StartUpSound -cp $PATHDATA/../../misc/sampleconfigs/startupsound.mp3.sample $AUDIOFOLDERSPATH/ZZZ/SubMaster/AAA\ MP3\ Whitespace\ StartUpSound/startupsound.mp3 - -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster/bbb-AudioFormatsTest -cp $PATHDATA/../../misc/audiofiletype* $AUDIOFOLDERSPATH/ZZZ/SubMaster/bbb-AudioFormatsTest/ - -mkdir $AUDIOFOLDERSPATH/ZZZ/SubMaster\ Whitespaces -cp -R $AUDIOFOLDERSPATH/ZZZ/SubMaster/* $AUDIOFOLDERSPATH/ZZZ/SubMaster\ Whitespaces/ - -# chmod chown -sudo chown -R :www-data $AUDIOFOLDERSPATH/ZZZ* -sudo chmod -R 777 $AUDIOFOLDERSPATH/ZZZ* diff --git a/scripts/helperscripts/DeleteAllConfig.sh b/scripts/helperscripts/DeleteAllConfig.sh deleted file mode 100755 index dc73e08e2..000000000 --- a/scripts/helperscripts/DeleteAllConfig.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -echo "This script will delete all config files" -echo "including mpd.conf and the like." -read -r -p "Do you want to proceed? [y/N] " response -case "$response" in - [Yy][Ee][Ss]|[Yy]) - ;; - *) - echo "Exiting script." - exit - ;; -esac -echo "Proceeding and deleting." - -# these ones we MUST leave -#sudo rm /etc/sudoers -#sudo rm /etc/samba/smb.conf - -# these ones we will leave -#sudo rm /home/pi/RPi-Jukebox-RFID/htdocs/config.php -#sudo rm /home/pi/RPi-Jukebox-RFID/settings/rfid_trigger_play.conf - -# these ones we delete -sudo rm /etc/lighttpd/lighttpd.conf -sudo rm /etc/lighttpd/conf-available/15-fastcgi-php.conf -sudo rm /etc/php/7.0/fpm/php.ini -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Audio_iFace_Name -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Audio_Folders_Path -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Audio_Volume_Change_Step -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Max_Volume_Limit -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Idle_Time_Before_Shutdown -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Second_Swipe -sudo rm /home/pi/RPi-Jukebox-RFID/settings/Playlists_Folders_Path -sudo rm /home/pi/RPi-Jukebox-RFID/settings/ShowCover -sudo rm /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py -sudo rm /etc/systemd/system/phoniebox-rfid-reader.service -sudo rm /etc/systemd/system/phoniebox-startup-sound.service -sudo rm /etc/systemd/system/phoniebox-gpio-buttons.service -sudo rm /etc/systemd/system/phoniebox-idle-watchdog.service -sudo rm /etc/systemd/system/rfid-reader.service -sudo rm /etc/systemd/system/startup-sound.service -sudo rm /etc/systemd/system/gpio-buttons.service -sudo rm /etc/systemd/system/idle-watchdog.service -sudo rm /etc/mpd.conf -sudo rm /etc/locale.gen -sudo rm /etc/default/locale -sudo rm /etc/mopidy/mopidy.conf -sudo rm ~/.config/mopidy/mopidy.conf diff --git a/scripts/helperscripts/DeleteSampleAudiofoldersStreams.sh b/scripts/helperscripts/DeleteSampleAudiofoldersStreams.sh deleted file mode 100755 index 5ae62290c..000000000 --- a/scripts/helperscripts/DeleteSampleAudiofoldersStreams.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# Deletes sample folders with files and streams -# inside the $AUDIOFOLDERSPATH directory - -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -AUDIOFOLDERSPATH=`cat ../../settings/Audio_Folders_Path` - -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ\ MP3\ Whitespace\ StartUpSound -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ\ SubMaster\ Whitespaces -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ\ This\ American\ Life\ Podcast -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ-AudioFormatsTest -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ-LiveStream-Bayern2 -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ-MP3-StartUpSound -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ-Podcast-DLF-Kinderhoerspiele -sudo rm -rf $AUDIOFOLDERSPATH/ZZZ-SubMaster diff --git a/scripts/helperscripts/autohotspot b/scripts/helperscripts/autohotspot deleted file mode 100755 index af64f1dbf..000000000 --- a/scripts/helperscripts/autohotspot +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/bash -#version 0.95-41-N/HS - -#You may share this script on the condition a reference to RaspberryConnect.com -#must be included in copies or derivatives of this script. - -#A script to switch between a wifi network and a non internet routed Hotspot -#Works at startup or with a seperate timer or manually without a reboot -#Other setup required find out more at -#http://www.raspberryconnect.com - -wifidev="wlan0" #device name to use. Default is wlan0. -#use the command: iw dev ,to see wifi interface name - -IFSdef=$IFS -cnt=0 -#These four lines capture the wifi networks the RPi is setup to use -wpassid=$(awk '/ssid="/{ print $0 }' /etc/wpa_supplicant/wpa_supplicant.conf | awk -F'ssid=' '{ print $2 }' ORS=',' | sed 's/\"/''/g' | sed 's/,$//') -wpassid=$(echo "${wpassid//[$'\r\n']}") -IFS="," -ssids=($wpassid) -IFS=$IFSdef #reset back to defaults - - -#Note:If you only want to check for certain SSIDs -#Remove the # in in front of ssids=('mySSID1'.... below and put a # infront of all four lines above -# separated by a space, eg ('mySSID1' 'mySSID2') -#ssids=('mySSID1' 'mySSID2' 'mySSID3') - -#Enter the Routers Mac Addresses for hidden SSIDs, seperated by spaces ie -#( '11:22:33:44:55:66' 'aa:bb:cc:dd:ee:ff' ) -mac=() - -ssidsmac=("${ssids[@]}" "${mac[@]}") #combines ssid and MAC for checking - -createAdHocNetwork() -{ - echo "Creating Hotspot" - ip link set dev "$wifidev" down - ip a add 10.0.0.5/24 brd + dev "$wifidev" - ip link set dev "$wifidev" up - dhcpcd -k "$wifidev" >/dev/null 2>&1 - systemctl start dnsmasq - systemctl start hostapd -} - -KillHotspot() -{ - echo "Shutting Down Hotspot" - ip link set dev "$wifidev" down - systemctl stop hostapd - systemctl stop dnsmasq - ip addr flush dev "$wifidev" - ip link set dev "$wifidev" up - dhcpcd -n "$wifidev" >/dev/null 2>&1 -} - -ChkWifiUp() -{ - echo "Checking WiFi connection ok" - sleep 20 #give time for connection to be completed to router - if ! wpa_cli -i "$wifidev" status | grep 'ip_address' >/dev/null 2>&1 - then #Failed to connect to wifi (check your wifi settings, password etc) - echo 'Wifi failed to connect, falling back to Hotspot.' - wpa_cli terminate "$wifidev" >/dev/null 2>&1 - createAdHocNetwork - fi -} - - -FindSSID() -{ -#Check to see what SSID's and MAC addresses are in range -ssidChk=('NoSSid') -i=0; j=0 -until [ $i -eq 1 ] #wait for wifi if busy, usb wifi is slower. -do - ssidreply=$((iw dev "$wifidev" scan ap-force | egrep "^BSS|SSID:") 2>&1) >/dev/null 2>&1 - echo "SSid's in range: " $ssidreply - echo "Device Available Check try " $j - if (($j >= 10)); then #if busy 10 times goto hotspot - echo "Device busy or unavailable 10 times, going to Hotspot" - ssidreply="" - i=1 - elif echo "$ssidreply" | grep "No such device (-19)" >/dev/null 2>&1; then - echo "No Device Reported, try " $j - NoDevice - elif echo "$ssidreply" | grep "Network is down (-100)" >/dev/null 2>&1 ; then - echo "Network Not available, trying again" $j - j=$((j + 1)) - sleep 2 - elif echo "$ssidreply" | grep "Read-only file system (-30)" >/dev/null 2>&1 ; then - echo "Temporary Read only file system, trying again" - j=$((j + 1)) - sleep 2 - elif echo "$ssidreply" | grep "Invalid exchange (-52)" >/dev/null 2>&1 ; then - echo "Temporary unavailable, trying again" - j=$((j + 1)) - sleep 2 - elif ! echo "$ssidreply" | grep "resource busy (-16)" >/dev/null 2>&1 ; then - echo "Device Available, checking SSid Results" - i=1 - else #see if device not busy in 2 seconds - echo "Device unavailable checking again, try " $j - j=$((j + 1)) - sleep 2 - fi -done - -for ssid in "${ssidsmac[@]}" -do - if (echo "$ssidreply" | grep "$ssid") >/dev/null 2>&1 - then - #Valid SSid found, passing to script - echo "Valid SSID Detected, assesing Wifi status" - ssidChk=$ssid - return 0 - else - #No Network found, NoSSid issued" - echo "No SSid found, assessing WiFi status" - ssidChk='NoSSid' - fi -done -} - -NoDevice() -{ - #if no wifi device,ie usb wifi removed, activate wifi so when it is - #reconnected wifi to a router will be available - echo "No wifi device connected" - wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1 - exit 1 -} - -FindSSID - -#Create Hotspot or connect to valid wifi networks -if [ "$ssidChk" != "NoSSid" ] -then - if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 - then #hotspot running and ssid in range - KillHotspot - echo "Hotspot Deactivated, Bringing Wifi Up" - wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1 - ChkWifiUp - elif { wpa_cli -i "$wifidev" status | grep 'ip_address'; } >/dev/null 2>&1 - then #Already connected - echo "Wifi already connected to a network" - else #ssid exists and no hotspot running connect to wifi network - echo "Connecting to the WiFi Network" - wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1 - ChkWifiUp - fi -else #ssid or MAC address not in range - if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 - then - echo "Hostspot already active" - elif { wpa_cli status | grep "$wifidev"; } >/dev/null 2>&1 - then - echo "Cleaning wifi files and Activating Hotspot" - wpa_cli terminate >/dev/null 2>&1 - ip addr flush "$wifidev" - ip link set dev "$wifidev" down - rm -r /var/run/wpa_supplicant >/dev/null 2>&1 - createAdHocNetwork - else #"No SSID, activating Hotspot" - createAdHocNetwork - fi -fi diff --git a/scripts/helperscripts/cli_ReadWifiIp.php b/scripts/helperscripts/cli_ReadWifiIp.php deleted file mode 100755 index 17ead8364..000000000 --- a/scripts/helperscripts/cli_ReadWifiIp.php +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/php - diff --git a/scripts/idle-watchdog-countdown.sh b/scripts/idle-watchdog-countdown.sh deleted file mode 100755 index ad3708ffd..000000000 --- a/scripts/idle-watchdog-countdown.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# This is a mpd idle watchdog to shutdown the box. -# -# Phonieboxes without reliable system time can use this idle-watchdog-script to -# shutdown the box after a pre defined number of minutes. -# This script checks every 60 seconds if mpd is paying music with volume set to -# greater than 0. If not it decreases the pre-defined number of minutes and shut -# down the box if it reaches zero. -# Be aware that playing lots of short sequences (less than 60 seconds) could be -# undetected by this script and lead to an unwanted shutdown. - -# Check for idle time settings. Assume to disable automatic shudown if not set. -if [ ! -r ./settings/Idle_Time_Before_Shutdown ]; then - logger "Idle_Time_Before_Shutdown is not set. Disable idle watchdog!" - exit 0 -else - SHUTDOWNAFTER=$(cat ./settings/Idle_Time_Before_Shutdown | head -n 1) -fi - -# Check if setting is a numeric value; else warn and disable watchdog. -[ "$SHUTDOWNAFTER" -eq "$SHUTDOWNAFTER" ] 2>/dev/null -if [ $? -ne 0 ]; then - logger "Invalid settings for Idle_Time_Before_Shutdown (not numeric). Disable idle watchdog!" - exit 0 -fi - -COUNTDOWN=$SHUTDOWNAFTER # initialize countdown value - -# start the continuous loop -while [ $SHUTDOWNAFTER -gt 0 ]; do - # check if mpd is playing and volume is not 0 - if [ $(mpc | egrep -c '^\[playing\]') -eq 1 ] && [ $(mpc | egrep -c '^volume:\s+0%') -eq 0 ]; then - if [ $COUNTDOWN -ne $SHUTDOWNAFTER ]; then - logger "mpd is playing audible again. Stop countdown to shutdown." - fi - COUNTDOWN=$SHUTDOWNAFTER # re-init countdown - else - if [ $COUNTDOWN -gt 0 ]; then - logger "mpd is NOT playing audible. Shutdown in $COUNTDOWN minutes." - else - logger "mpd was NOT playing audible for $SHUTDOWNAFTER minutes. Shutdown system!" - ./scripts/playout_controls.sh -c=shutdownsilent - exit 0 - fi - COUNTDOWN=$(expr $COUNTDOWN - 1) - fi - sleep 60 -done - -exit 0 diff --git a/scripts/idle-watchdog.sh b/scripts/idle-watchdog.sh deleted file mode 100755 index c4e721474..000000000 --- a/scripts/idle-watchdog.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -#Remove old shutdown commands from all 'at' queues after boot to prevent immediate shutdown -for i in `sudo atq | awk '{print $1}'`;do sudo atrm $i;done -#Give the RPi enough time to get the correct time via network -#Otherwise there may be an immediate shutdown because this script may set a shutdown time dated in the past -#in the first loop. If the Pi gets a correct time via network, "at" suddenly detects a overdue job, which is: -#shutting down the Pi. -sleep 60 - -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Idle time after the RPi will be shut down. 0=turn off feature. -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Idle_Time_Before_Shutdown ]; then - echo "0" > $PATHDATA/../settings/Idle_Time_Before_Shutdown -fi -# 2. then|or read value from file -IDLETIME=`cat $PATHDATA/../settings/Idle_Time_Before_Shutdown` - -#Go into infinite loop if idle time is greater 0 -while [ $IDLETIME -gt 0 ] -do - #Read volume and player status - PLAYERSTATUS=$(mpc status) - VOLPERCENT=$(echo -e "status\nclose" | nc.openbsd -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*') - sleep 1 - #Set shutdown time if box is not playing or volume is 0 and no idle shutdown time is set - if { [ "$(echo "$PLAYERSTATUS" | grep -c "\[playing\]")" == "0" ] || [ $VOLPERCENT -eq "0" ]; } && [ -z "$(sudo atq -q i)" ]; - then - # shutdown pi after idling for $IDLETIME minutes - for i in `sudo atq -q i | awk '{print $1}'`;do sudo atrm $i;done - sleep 1 - echo "$PATHDATA/playout_controls.sh -c=shutdownsilent" | at -q i now + $IDLETIME minute - fi - - # If box is playing and volume is greater 0, remove idle shutdown. Skip this if "at"-queue is already empty - if [ "$(echo "$PLAYERSTATUS" | grep -c "\[playing\]")" == "1" ] && [ $VOLPERCENT -ne "0" ] && [ -n "$(sudo atq -q i)" ]; - then - for i in `sudo atq -q i | awk '{print $1}'`;do sudo atrm $i;done - fi - - sleep 60 -done diff --git a/scripts/inc.readArgsFromCommandLine.sh b/scripts/inc.readArgsFromCommandLine.sh deleted file mode 100755 index 5ed3b09ac..000000000 --- a/scripts/inc.readArgsFromCommandLine.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# this file is called inside -# - resume_play.sh -# - rfid_trigger_play.sh -# - playout_controls.sh -# - inc.writeFolderConfig.sh -# ... and possibly more by the time you read this. -# It is meant to unify the variables which can be -# passed on to a script via the command line. - -############################################################# -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -for i in "$@" -do - case $i in - -i=*|--cardid=*) - CARDID="${i#*=}" - ;; - -c=*|--command=*) - COMMAND="${i#*=}" - ;; - -d=*|--dir=*) - FOLDER="${i#*=}" - ;; - -v=*|--value=*) - VALUE="${i#*=}" - ;; - esac -done - -if [ "${DEBUG_inc_readArgsFromCommandLine_sh}" == "TRUE" ]; then echo " ######### SCRIPT inc.readArgsFromCommandLine.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_readArgsFromCommandLine_sh}" == "TRUE" ]; then echo " # VAR CARDID: $CARDID" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_readArgsFromCommandLine_sh}" == "TRUE" ]; then echo " # VAR COMMAND: $COMMAND" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_readArgsFromCommandLine_sh}" == "TRUE" ]; then echo " # VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_readArgsFromCommandLine_sh}" == "TRUE" ]; then echo " # VAR VALUE: $VALUE" >> $PATHDATA/../logs/debug.log; fi diff --git a/scripts/inc.settingsFolderSpecific.sh b/scripts/inc.settingsFolderSpecific.sh deleted file mode 100755 index c04380054..000000000 --- a/scripts/inc.settingsFolderSpecific.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# This script is called when something needs to be played -# from script: playout_controls.sh -# It then looks into the settings of the folder and changes -# settings if need be, such as single track play or shuffle - -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " #START### SCRIPT inc.settingsFolderSpecific.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi - -# Get folder name of currently played audio -if [ "x${FOLDER}" == "x" ] -then - FOLDER=$(cat $PATHDATA/../settings/Latest_Folder_Played) - - if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " # VAR FOLDER from settings/Latest_Folder_Played: $FOLDER" >> $PATHDATA/../logs/debug.log; fi -fi - -if [ -e "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" ] -then - # Read the current config file (include will execute == read) - . "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" - - if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " # Folder exists: ${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then cat "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi - - # SINGLE TRACK PLAY (== shuffle can not be on, because single on will play one track after another) - if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " # SINGLE TRACK PLAY: $SINGLE" >> $PATHDATA/../logs/debug.log; fi - if [ $SINGLE == "ON" ] - then - if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " # # CHANGING: mpc single on" >> $PATHDATA/../logs/debug.log; fi - mpc single on - mpc random off - else - if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " # # CHANGING: mpc single off" >> $PATHDATA/../logs/debug.log; fi - mpc single off - fi - -fi - -if [ "${DEBUG_inc_settingsFolderSpecific_sh}" == "TRUE" ]; then echo " #END##### SCRIPT inc.settingsFolderSpecific.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi diff --git a/scripts/inc.writeFolderConfig.sh b/scripts/inc.writeFolderConfig.sh deleted file mode 100755 index 2077462ec..000000000 --- a/scripts/inc.writeFolderConfig.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash - -# This script is important. -# This script manages the creation and changing of folder.conf -# folder.conf sits in the audio folder and will grow over time, -# containing all infos about how to play the content -# such as loop, resume play, shuffle, elapsed time, etc. -# -# The functionality seems weird (but makes total sense:). -# This is how it works: -# 1. Since this file will be called from another bash, we can assume that -# we have same variables that need saving: check each and make a copy -# under a different name. -# 2. Read the current folder config file - now we have the vars in memory. -# 3. Create a raw config file instead of the current folder config -# which allows to replace new vars and keep the old -# 4. For each var in the config file: -# IF new var available, write this one -# ELSE write the one we read in step 2. -# -# Why so complicated? Because we don't know what other vars will be in the -# folder config in the future. Editing only the sample config file and this -# file, we are future proof, because old config files will work and update -# gracefully when new stuff arrives in the sample file. - -# We start from a sample file that contains vars like these: -# CURRENTFILENAME="%CURRENTFILENAME%" -# ELAPSED="%ELAPSED%" -# ... -# -# For complete control, the creatin of this raw config sample -# is also kept in this script and write if from here. -# So that all new vars etc. only require changing this file. - -############################################################# -# VARIABLES - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -# Get args from command line. Needed for "create default folder.conf" file -# see following file for details: -. $PATHDATA/inc.readArgsFromCommandLine.sh - -# IMPORTANT: the $FOLDER var does not need to be passed on if it was set in the master script -# that calls this one. For elegance, it might be better to pass it on. -# And for error checking this might mean: if $FOLDER is not passed on, exit, do not do anything. -# If this would be preferred by you, the users, please file a ticket to discuss it. - -# path to audio folders -AUDIOFOLDERSPATH=`cat $PATHDATA/../settings/Audio_Folders_Path` - -# some debug info -if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "########### SCRIPT inc.writeFolderConfig.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "VAR COMMAND: $COMMAND" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "VAR AUDIOFOLDERSPATH: $AUDIOFOLDERSPATH" >> $PATHDATA/../logs/debug.log; fi - - -if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "CHECK FOLDER EXISTS: ${AUDIOFOLDERSPATH}/${FOLDER}" >> $PATHDATA/../logs/debug.log; fi -# Only continue if $FOLDER exists -if [ -d "${AUDIOFOLDERSPATH}/${FOLDER}" ] -then - - # IF we got given the command to create a default folder.conf file - # set default vars, write file, exit - if [ $COMMAND == "createDefaultFolderConf" ] - then - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo " !!!setting default vars for raw create!!!" >> $PATHDATA/../logs/debug.log; fi - # set default vars - CURRENTFILENAME="filename" - ELAPSED="0" - PLAYSTATUS="Stopped" - RESUME="OFF" - SHUFFLE="OFF" - LOOP="OFF" - SINGLE="OFF" - fi - - ######################################################### - # KEEP NEW VARS IN MIND - # Go through all given vars - make copy with prefix if found - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo " KEEP NEW VARS IN MIND" >> $PATHDATA/../logs/debug.log; fi - if [ "$CURRENTFILENAME" ]; then - NEWCURRENTFILENAME="$CURRENTFILENAME"; - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "VAR NEWCURRENTFILENAME: $NEWCURRENTFILENAME" >> $PATHDATA/../logs/debug.log; fi - fi - if [ "$ELAPSED" ]; then NEWELAPSED="$ELAPSED"; fi - if [ "$PLAYSTATUS" ]; then NEWPLAYSTATUS="$PLAYSTATUS"; fi - if [ "$RESUME" ]; then NEWRESUME="$RESUME"; fi - if [ "$SHUFFLE" ]; then NEWSHUFFLE="$SHUFFLE"; fi - if [ "$LOOP" ]; then NEWLOOP="$LOOP"; fi - if [ "$SINGLE" ]; then NEWSINGLE="$SINGLE"; fi - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo " KEEP SINGLE $SINGLE IN MIND" >> $PATHDATA/../logs/debug.log; fi - - # Read the current config file (include will execute == read) - . "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo " content of ${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then cat "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" >> $PATHDATA/../logs/debug.log; fi - - ######################################################### - # RAW CONFIG FILE - # Replace current config with empty sample - # write "empty" config file with vars that will be replaced later - rm "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "CURRENTFILENAME=\"%CURRENTFILENAME%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "ELAPSED=\"%ELAPSED%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "PLAYSTATUS=\"%PLAYSTATUS%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "RESUME=\"%RESUME%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "SHUFFLE=\"%SHUFFLE%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "LOOP=\"%LOOP%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - echo "SINGLE=\"%SINGLE%\"" >> "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - - # Let the juggle begin - - ######################################################### - # REPLACE VALUES FROM THE CONFIG FILE WITH NEW ONES - # Walk through all vars and prefer new over existing to write to config - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo " REPLACE VALUES FROM THE CONFIG FILE WITH NEW ONES" >> $PATHDATA/../logs/debug.log; fi - if [ "$NEWCURRENTFILENAME" ]; then - CURRENTFILENAME="$NEWCURRENTFILENAME"; - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "VAR CURRENTFILENAME: $CURRENTFILENAME" >> $PATHDATA/../logs/debug.log; fi - fi - if [ "$NEWELAPSED" ]; then ELAPSED="$NEWELAPSED"; fi - if [ "$NEWPLAYSTATUS" ]; then PLAYSTATUS="$NEWPLAYSTATUS"; fi - if [ "$NEWRESUME" ]; then RESUME="$NEWRESUME"; fi - if [ "$NEWSHUFFLE" ]; then SHUFFLE="$NEWSHUFFLE"; fi - if [ "$NEWLOOP" ]; then LOOP="$NEWLOOP"; fi - if [ "$NEWSINGLE" ]; then SINGLE="$NEWSINGLE"; fi - - ######################################################### - # WRITE THE VALUES INTO THE NEWLY CREATED RAW CONFIG - # for $CURRENTFILENAME using | as alternate regex delimiter because of the folder path slash - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo " WRITE THE VALUES INTO THE NEWLY CREATED RAW CONFIG" >> $PATHDATA/../logs/debug.log; fi - sudo sed -i 's|%CURRENTFILENAME%|'"$CURRENTFILENAME"'|' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo sed -i 's/%ELAPSED%/'"$ELAPSED"'/' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo sed -i 's/%PLAYSTATUS%/'"$PLAYSTATUS"'/' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo sed -i 's/%RESUME%/'"$RESUME"'/' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo sed -i 's/%SHUFFLE%/'"$SHUFFLE"'/' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo sed -i 's/%LOOP%/'"$LOOP"'/' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo sed -i 's/%SINGLE%/'"$SINGLE"'/' "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo chown pi:www-data "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - sudo chmod 777 "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" - -else - if [ "${DEBUG_inc_writeFolderConfig_sh}" == "TRUE" ]; then echo "NOT FOUND: Full path to folder '${AUDIOFOLDERSPATH}/${FOLDER}'" >> $PATHDATA/../logs/debug.log; fi -fi - - - - diff --git a/scripts/inc.writeGlobalConfig.sh b/scripts/inc.writeGlobalConfig.sh deleted file mode 100755 index 85aa4c224..000000000 --- a/scripts/inc.writeGlobalConfig.sh +++ /dev/null @@ -1,380 +0,0 @@ -#!/bin/bash - -# Creates a global config file from all the individual -# files in the `settings` folder at: -# settings/global.conf -# Should be called: -# 1. on startup (list startup sound) to create a latest -# version of all settings -# 2. each settings change done in the web UI -# 3. a new feature to be implemented: manually triggered -# in the web UI -# -# Why so complicated? Because we don't know what other vars will be in the -# config in the future. Editing only this file, we are future proof, -# because old config files will work and update -# gracefully when new stuff arrives in the sample file. -# -# To make sure that the global.conf file has EVERYTHING in it -# that could be, for each feature it does the following: -# a) checks if there is a config file -# a) 1) if not, make one with the default value -# b) read the value from the config file - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder which contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -# The absolute path to the folder which contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -if [ "${DEBUG_inc_writeGlobalConfig_sh}" == "TRUE" ]; then echo "########### SCRIPT inc.writeGlobalConf.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi - -# create the configuration file from sample - if it does not exist -if [ ! -f $PATHDATA/../settings/rfid_trigger_play.conf ]; then - cp $PATHDATA/../settings/rfid_trigger_play.conf.sample $PATHDATA/../settings/rfid_trigger_play.conf - # change the read/write so that later this might also be editable through the web app - sudo chown -R pi:www-data $PATHDATA/../settings/rfid_trigger_play.conf - sudo chmod -R 775 $PATHDATA/../settings/rfid_trigger_play.conf -fi - -# Path to folder containing audio / streams -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Audio_Folders_Path ]; then - echo "/home/pi/RPi-Jukebox-RFID/shared/audiofolders" > $PATHDATA/../settings/Audio_Folders_Path - chmod 777 $PATHDATA/../settings/Audio_Folders_Path -fi -# 2. then|or read value from file -AUDIOFOLDERSPATH=`cat $PATHDATA/../settings/Audio_Folders_Path` - -# Path to folder containing playlists -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Playlists_Folders_Path ]; then - echo "/home/pi/RPi-Jukebox-RFID/playlists" > $PATHDATA/../settings/Playlists_Folders_Path - chmod 777 $PATHDATA/../settings/Playlists_Folders_Path -fi -# 2. then|or read value from file -PLAYLISTSFOLDERPATH=`cat $PATHDATA/../settings/Playlists_Folders_Path` - -############################################## -# General RFID player control SWIPE OR PLACE -# General decision on how the player operates -# SWIPENOTPLACE = Swiping starts the player -# PLACENOTSWIPE = Placing the card starts player, removal stops it -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Swipe_or_Place ]; then - echo "SWIPENOTPLACE" > $PATHDATA/../settings/Swipe_or_Place - chmod 777 $PATHDATA/../settings/Swipe_or_Place -fi -# 2. then|or read value from file -SWIPEORPLACE=`cat $PATHDATA/../settings/Swipe_or_Place` - -############################################## -# Second swipe -# What happens when the same card is swiped a second time? -# RESTART => start the playlist again vs. PAUSE => toggle pause and play current -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Second_Swipe ]; then - echo "RESTART" > $PATHDATA/../settings/Second_Swipe - chmod 777 $PATHDATA/../settings/Second_Swipe -fi -# 2. then|or read value from file -SECONDSWIPE=`cat $PATHDATA/../settings/Second_Swipe` - -############################################## -# Second swipe Pause -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Second_Swipe_Pause ]; then - echo "2" > $PATHDATA/../settings/Second_Swipe_Pause - chmod 777 $PATHDATA/../settings/Second_Swipe_Pause -fi -# 2. then|or read value from file -SECONDSWIPEPAUSE=`cat $PATHDATA/../settings/Second_Swipe_Pause` - -############################################## -# Second swipe Pause Controls -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Second_Swipe_Pause_Controls ]; then - echo "ON" > $PATHDATA/../settings/Second_Swipe_Pause_Controls - chmod 777 $PATHDATA/../settings/Second_Swipe_Pause_Controls -fi -# 2. then|or read value from file -SECONDSWIPEPAUSECONTROLS=`cat $PATHDATA/../settings/Second_Swipe_Pause_Controls` - -############################################## -# Audio_iFace_Name -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Audio_iFace_Name ]; then - echo "PCM" > $PATHDATA/../settings/Audio_iFace_Name - chmod 777 $PATHDATA/../settings/Audio_iFace_Name -fi -# 2. then|or read value from file -AUDIOIFACENAME=`cat $PATHDATA/../settings/Audio_iFace_Name` - -############################################## -# Volume_Manager (mpd or amixer) -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Volume_Manager ]; then - echo "mpd" > $PATHDATA/../settings/Volume_Manager - chmod 777 $PATHDATA/../settings/Volume_Manager -fi -# 2. then|or read value from file -VOLUMEMANAGER=`cat $PATHDATA/../settings/Volume_Manager` - -############################################## -# Audio_Volume_Change_Step -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Audio_Volume_Change_Step ]; then - echo "3" > $PATHDATA/../settings/Audio_Volume_Change_Step - chmod 777 $PATHDATA/../settings/Audio_Volume_Change_Step -fi -# 2. then|or read value from file -AUDIOVOLCHANGESTEP=`cat $PATHDATA/../settings/Audio_Volume_Change_Step` - -############################################## -# Max_Volume_Limit -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Max_Volume_Limit ]; then - echo "100" > $PATHDATA/../settings/Max_Volume_Limit - chmod 777 $PATHDATA/../settings/Max_Volume_Limit -fi -# 2. then|or read value from file -AUDIOVOLMAXLIMIT=`cat $PATHDATA/../settings/Max_Volume_Limit` - -############################################## -# Min_Volume_Limit -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Min_Volume_Limit ]; then - echo "1" > $PATHDATA/../settings/Min_Volume_Limit - chmod 777 $PATHDATA/../settings/Min_Volume_Limit -fi -# 2. then|or read value from file -AUDIOVOLMINLIMIT=`cat $PATHDATA/../settings/Min_Volume_Limit` - -############################################## -# Startup_Volume -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Startup_Volume ]; then - echo "30" > $PATHDATA/../settings/Startup_Volume - chmod 777 $PATHDATA/../settings/Startup_Volume -fi -# 2. then|or read value from file -AUDIOVOLSTARTUP=`cat $PATHDATA/../settings/Startup_Volume` - -############################################## -# Change_Volume_Idle -# Change volume during idle (or only change it during Play and in the WebApp) -#TRUE=Change Volume during all Time (Default; FALSE=Change Volume only during "Play"; OnlyDown=It is possible to decrease Volume during Idle; OnlyUp=It is possible to increase Volume during Idle -# 1. create a default if file does not exist (set default do TRUE - Volume Change is possible every time) -if [ ! -f $PATHDATA/../settings/Change_Volume_Idle ]; then - echo "TRUE" > $PATHDATA/../settings/Change_Volume_Idle -fi -# 2. then|or read value from file -VOLCHANGEIDLE=`cat $PATHDATA/../settings/Change_Volume_Idle` - -############################################## -# Idle_Time_Before_Shutdown -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Idle_Time_Before_Shutdown ]; then - echo "0" > $PATHDATA/../settings/Idle_Time_Before_Shutdown - chmod 777 $PATHDATA/../settings/Idle_Time_Before_Shutdown -fi -# 2. then|or read value from file -IDLETIMESHUTDOWN=`cat $PATHDATA/../settings/Idle_Time_Before_Shutdown` - -############################################## -# Poweroff_Command -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Poweroff_Command ]; then - echo "sudo poweroff" > $PATHDATA/../settings/Poweroff_Command - chmod 777 $PATHDATA/../settings/Poweroff_Command -fi -# 2. then|or read value from file -POWEROFFCMD=`cat $PATHDATA/../settings/Poweroff_Command` - -############################################## -# ShowCover -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/ShowCover ]; then - echo "ON" > $PATHDATA/../settings/ShowCover - chmod 777 $PATHDATA/../settings/ShowCover -fi -# 2. then|or read value from file -SHOWCOVER=`cat $PATHDATA/../settings/ShowCover` - -############################################## -# Mail Wlan Ip Address -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/WlanIpMailAddr ]; then - echo "" > $PATHDATA/../settings/WlanIpMailAddr - chmod 777 $PATHDATA/../settings/WlanIpMailAddr -fi -# 2. then|or read value from file -MAILWLANIPADDR=`cat $PATHDATA/../settings/WlanIpMailAddr` - -############################################## -# Mail Wlan Ip Email Address -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/MailWlanIpYN ]; then - echo "OFF" > $PATHDATA/../settings/MailWlanIpYN - chmod 777 $PATHDATA/../settings/MailWlanIpYN -fi -# 2. then|or read value from file -MAILWLANIPYN=`cat $PATHDATA/../settings/MailWlanIpYN` - -############################################## -# Read IP address of Wlan after boot? -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/WlanIpReadYN ]; then - echo "OFF" > $PATHDATA/../settings/WlanIpReadYN - chmod 777 $PATHDATA/../settings/WlanIpReadYN -fi -# 2. then|or read value from file -READWLANIPYN=`cat $PATHDATA/../settings/WlanIpReadYN` - -############################################## -# edition -# read this always, do not write default - -# 1. create a default if file does not exist -#if [ ! -f $PATHDATA/../settings/edition ]; then -# echo "classic" > $PATHDATA/../settings/edition -# chmod 777 $PATHDATA/../settings/edition -#fi -# 2. then|or read value from file -chmod 777 $PATHDATA/../settings/edition -EDITION=`cat $PATHDATA/../settings/edition` - -############################################## -# Lang -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/Lang ]; then - echo "en-UK" > $PATHDATA/../settings/Lang - chmod 777 $PATHDATA/../settings/Lang -fi -# 2. then|or read value from file -LANG=`cat $PATHDATA/../settings/Lang` - -############################################## -# version -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/version ]; then - echo "unknown" > $PATHDATA/../settings/version - chmod 777 $PATHDATA/../settings/version -fi -# 2. then|or read value from file -VERSION=`cat $PATHDATA/../settings/version` - -############################################## -# CHAPTEREXTENSIONS -# Only files with the extensions listed will be scanned for chapters -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/CHAPTEREXTENSIONS ]; then - echo "mp4,m4a,m4b,m4r" > $PATHDATA/../settings/CHAPTEREXTENSIONS - chmod 777 $PATHDATA/../settings/CHAPTEREXTENSIONS -fi -# 2. then|or read value from file -CHAPTEREXTENSIONS=`cat $PATHDATA/../settings/CHAPTEREXTENSIONS` - -############################################## -# CHAPTERMINDURATION -# Only files with play length bigger than minimum will be scanned for chapters -# 1. create a default if file does not exist -if [ ! -f $PATHDATA/../settings/CHAPTERMINDURATION ]; then - echo "600" > $PATHDATA/../settings/CHAPTERMINDURATION - chmod 777 $PATHDATA/../settings/CHAPTERMINDURATION -fi -# 2. then|or read value from file -CHAPTERMINDURATION=`cat $PATHDATA/../settings/CHAPTERMINDURATION` - -############################################## -# read control card ids -# 1. read all values from file -CMDVOLUP=`grep 'CMDVOLUP' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDVOLUP=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` -CMDVOLDOWN=`grep 'CMDVOLDOWN' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDVOLDOWN=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` -CMDNEXT=`grep 'CMDNEXT' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDNEXT=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` -CMDPREV=`grep 'CMDPREV' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDPREV=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` -CMDREWIND=`grep 'CMDREWIND' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDREWIND=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` -CMDSEEKFORW=`grep 'CMDSEEKFORW' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDSEEKFORW=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` -CMDSEEKBACK=`grep 'CMDSEEKBACK' $PATHDATA/../settings/rfid_trigger_play.conf|tail -1|sed 's/CMDSEEKBACK=//g'|sed 's/"//g'|tr -d "\n"|grep -o '[0-9]*'` - -# AUDIOFOLDERSPATH -# PLAYLISTSFOLDERPATH -# SWIPEORPLACE -# SECONDSWIPE -# SECONDSWIPEPAUSE -# SECONDSWIPEPAUSECONTROLS -# AUDIOIFACENAME -# VOLUMEMANAGER -# AUDIOVOLCHANGESTEP -# AUDIOVOLMAXLIMIT -# AUDIOVOLMINLIMIT -# AUDIOVOLSTARTUP -# VOLCHANGEIDLE -# IDLETIMESHUTDOWN -# POWEROFFCMD -# SHOWCOVER -# MAILWLANIPYN -# MAILWLANIPADDR -# READWLANIPYN -# EDITION -# LANG -# VERSION -# CHAPTEREXTENSIONS -# CHAPTERMINDURATION -# CMDVOLUP -# CMDVOLDOWN -# CMDNEXT -# CMDPREV -# CMDREWIND -# CMDSEEKFORW -# CMDSEEKBACK - -######################################################### -# WRITE CONFIG FILE -rm "${PATHDATA}/../settings/global.conf" -echo "AUDIOFOLDERSPATH=\"${AUDIOFOLDERSPATH}\"" >> "${PATHDATA}/../settings/global.conf" -echo "PLAYLISTSFOLDERPATH=\"${PLAYLISTSFOLDERPATH}\"" >> "${PATHDATA}/../settings/global.conf" -echo "SWIPEORPLACE=\"${SWIPEORPLACE}\"" >> "${PATHDATA}/../settings/global.conf" -echo "SECONDSWIPE=\"${SECONDSWIPE}\"" >> "${PATHDATA}/../settings/global.conf" -echo "SECONDSWIPEPAUSE=\"${SECONDSWIPEPAUSE}\"" >> "${PATHDATA}/../settings/global.conf" -echo "SECONDSWIPEPAUSECONTROLS=\"${SECONDSWIPEPAUSECONTROLS}\"" >> "${PATHDATA}/../settings/global.conf" -echo "AUDIOIFACENAME=\"${AUDIOIFACENAME}\"" >> "${PATHDATA}/../settings/global.conf" -echo "VOLUMEMANAGER=\"${VOLUMEMANAGER}\"" >> "${PATHDATA}/../settings/global.conf" -echo "AUDIOVOLCHANGESTEP=\"${AUDIOVOLCHANGESTEP}\"" >> "${PATHDATA}/../settings/global.conf" -echo "AUDIOVOLMAXLIMIT=\"${AUDIOVOLMAXLIMIT}\"" >> "${PATHDATA}/../settings/global.conf" -echo "AUDIOVOLMINLIMIT=\"${AUDIOVOLMINLIMIT}\"" >> "${PATHDATA}/../settings/global.conf" -echo "AUDIOVOLSTARTUP=\"${AUDIOVOLSTARTUP}\"" >> "${PATHDATA}/../settings/global.conf" -echo "VOLCHANGEIDLE=\"${VOLCHANGEIDLE}\"" >> "${PATHDATA}/../settings/global.conf" -echo "IDLETIMESHUTDOWN=\"${IDLETIMESHUTDOWN}\"" >> "${PATHDATA}/../settings/global.conf" -echo "POWEROFFCMD=\"${POWEROFFCMD}\"" >> "${PATHDATA}/../settings/global.conf" -echo "SHOWCOVER=\"${SHOWCOVER}\"" >> "${PATHDATA}/../settings/global.conf" -echo "READWLANIPYN=\"${READWLANIPYN}\"" >> "${PATHDATA}/../settings/global.conf" -echo "EDITION=\"${EDITION}\"" >> "${PATHDATA}/../settings/global.conf" -echo "LANG=\"${LANG}\"" >> "${PATHDATA}/../settings/global.conf" -echo "VERSION=\"${VERSION}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CHAPTEREXTENSIONS=\"${CHAPTEREXTENSIONS}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CHAPTERMINDURATION=\"${CHAPTERMINDURATION}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDVOLUP=\"${CMDVOLUP}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDVOLDOWN=\"${CMDVOLDOWN}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDNEXT=\"${CMDNEXT}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDPREV=\"${CMDPREV}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDREWIND=\"${CMDREWIND}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDSEEKFORW=\"${CMDSEEKFORW}\"" >> "${PATHDATA}/../settings/global.conf" -echo "CMDSEEKBACK=\"${CMDSEEKBACK}\"" >> "${PATHDATA}/../settings/global.conf" - -# Work in progress: -#echo "MAILWLANIPYN=\"${MAILWLANIPYN}\"" >> "${PATHDATA}/../settings/global.conf" -#echo "MAILWLANIPADDR=\"${MAILWLANIPADDR}\"" >> "${PATHDATA}/../settings/global.conf" - -# change the read/write so that later this might also be editable through the web app -sudo chown -R pi:www-data ${PATHDATA}/../settings/global.conf -sudo chmod -R 777 ${PATHDATA}/../settings/global.conf diff --git a/scripts/installscripts/buster-install-default-with-autohotspot.sh b/scripts/installscripts/buster-install-default-with-autohotspot.sh deleted file mode 100755 index c7e748ab1..000000000 --- a/scripts/installscripts/buster-install-default-with-autohotspot.sh +++ /dev/null @@ -1,1427 +0,0 @@ -#!/usr/bin/env bash -# -# see https://github.com/MiczFlor/RPi-Jukebox-RFID for details -# -# NOTE: Running automated install (without interaction): -# Each install creates a file called PhonieboxInstall.conf -# in the folder /home/pi/ -# You can install the Phoniebox using such a config file -# which means you don't need to run the interactive install: -# -# 1. download the install file from github -# https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/scripts/installscripts -# (note: currently only works for buster and newer OS) -# 2. make the file executable: chmod +x -# 3. place the PhonieboxInstall.conf in the folder /home/pi/ -# 4. run the installscript with option -a like this: -# buster-install-default.sh -a - -# The absolute path to the folder which contains this script -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -GIT_BRANCH=${GIT_BRANCH:-master} -GIT_URL=${GIT_URL:-https://github.com/MiczFlor/RPi-Jukebox-RFID.git} -echo GIT_BRANCH $GIT_BRANCH -echo GIT_URL $GIT_URL - -DATETIME=$(date +"%Y%m%d_%H%M%S") - -SCRIPTNAME="$(basename $0)" -JOB="${SCRIPTNAME}" - -HOME_DIR="/home/pi" - -JUKEBOX_HOME_DIR="${HOME_DIR}/RPi-Jukebox-RFID" -LOGDIR="${HOME_DIR}"/phoniebox_logs -JUKEBOX_BACKUP_DIR="${HOME_DIR}/BACKUP" - -INTERACTIVE=true - -usage() { - printf "Usage: ${SCRIPTNAME} [-a] [-h]\n" - printf " -a\tautomatic/non-interactive mode\n" - printf " -h\thelp\n" - exit 0 -} - -while getopts ":ah" opt; -do - case ${opt} in - a ) INTERACTIVE=false - ;; - h ) usage - ;; - \? ) usage - ;; - esac -done - - -# Setup logger functions -# Input from http://www.ludovicocaldara.net/dba/bash-tips-5-output-logfile/ -log_open() { - [[ -d "${LOGDIR}" ]] || mkdir -p "${LOGDIR}" - PIPE="${LOGDIR}"/"${JOB}"_"${DATETIME}".pipe - mkfifo -m 700 "${PIPE}" - LOGFILE="${LOGDIR}"/"${JOB}"_"${DATETIME}".log - exec 3>&1 - tee "${LOGFILE}" <"${PIPE}" >&3 & - TEEPID=$! - exec 1>"${PIPE}" 2>&1 - PIPE_OPENED=1 -} - -log_close() { - if [ "${PIPE_OPENED}" ]; then - exec 1<&3 - sleep 0.2 - ps --pid "${TEEPID}" >/dev/null - if [ $? -eq 0 ] ; then - # a wait ${TEEPID} whould be better but some - # commands leave file descriptors open - sleep 1 - kill "${TEEPID}" - fi - rm "${PIPE}" - unset PIPE_OPENED - fi -} - - -welcome() { - clear - echo "##################################################### -# ___ __ ______ _ __________ ____ __ _ _ # -# / _ \/ // / __ \/ |/ / _/ __/( _ \ / \( \/ ) # -# / ___/ _ / /_/ / // // _/ ) _ (( O )) ( # -# /_/ /_//_/\____/_/|_/___/____/ (____/ \__/(_/\_) # -# # -##################################################### - -You are turning your Raspberry Pi into a Phoniebox. Good choice. -This INTERACTIVE INSTALL script requires you to be online and -will guide you through the configuration. - -If you want to run the AUTOMATED INSTALL (non-interactive) from -an existing configuration file, do the following: -1. exit this install script (press n) -2. place your PhonieboxInstall.conf in the folder /home/pi/ -3. run the installscript with option -a. For example like this: - ./home/pi/buster-install-default.sh -a - " - read -rp "Continue interactive installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - exit - ;; - *) - echo "Installation continues..." - ;; - esac -} - -reset_install_config_file() { - ##################################################### - # CONFIG FILE - # This file will contain all the data given in the - # following dialogue - # At a later stage, the install should also be done - # from such a config file with no user input. - - # Remove existing config file - #rm "${HOME_DIR}/PhonieboxInstall.conf" - # Create empty config file - #touch "${HOME_DIR}/PhonieboxInstall.conf" - #echo "# Phoniebox config" > "${HOME_DIR}/PhonieboxInstall.conf" - echo "# Phoniebox config" -} - -config_wifi() { - ##################################################### - # Ask if wifi config - - clear - - echo "##################################################### -# -# CONFIGURE WIFI -# -# Requires SSID, WiFi password and the static IP you want -# to assign to your Phoniebox. -# (Note: can be done manually later, if you are unsure.) -" -read -rp "Do you want to configure your WiFi? [Y/n] " response -echo "" -case "$response" in - [nN][oO]|[nN]) - WIFIconfig=NO - echo "You want to configure WiFi later." - # append variables to config file - echo "WIFIconfig=$WIFIconfig" >> "${HOME_DIR}/PhonieboxInstall.conf" - # make a fallback for WiFi Country Code, because we need that even without WiFi config - echo "WIFIcountryCode=DE" >> "${HOME_DIR}/PhonieboxInstall.conf" - ;; - *) - WIFIconfig=YES - #Ask for SSID - read -rp "* Type SSID name: " WIFIssid - #Ask for wifi country code - read -rp "* WiFi Country Code (e.g. DE, GB, CZ or US): " WIFIcountryCode - #Ask for password - read -rp "* Type password: " WIFIpass - #Ask for IP - read -rp "* Static IP (e.g. 192.168.1.199): " WIFIip - #Ask for Router IP - read -rp "* Router IP (e.g. 192.168.1.1): " WIFIipRouter - echo "" - echo "Your WiFi config:" - echo "SSID : $WIFIssid" - echo "WiFi Country Code : $WIFIcountryCode" - echo "Password : $WIFIpass" - echo "Static IP : $WIFIip" - echo "Router IP : $WIFIipRouter" - read -rp "Are these values correct? [Y/n] " response - echo "" - case "$response" in - [nN][oO]|[nN]) - echo "The values are incorrect." - read -rp "Hit ENTER to exit and start over." INPUT; exit - ;; - *) - # append variables to config file - { - echo "WIFIconfig=\"$WIFIconfig\""; - echo "WIFIcountryCode=\"$WIFIcountryCode\""; - echo "WIFIssid=\"$WIFIssid\""; - echo "WIFIpass=\"$WIFIpass\""; - echo "WIFIip=\"$WIFIip\""; - echo "WIFIipRouter=\"$WIFIipRouter\""; - } >> "${HOME_DIR}/PhonieboxInstall.conf" - ;; - esac - ;; -esac -read -rp "Hit ENTER to proceed to the next step." INPUT -} - -check_existing() { - local jukebox_dir="$1" - local backup_dir="$2" - local home_dir="$3" - - ##################################################### - # Check for existing Phoniebox - # - # In case there is no existing install, - # set the var now for later use: - EXISTINGuse=NO - - # The install will be in the home dir of user pi - # Move to home directory now to check - cd ~ || exit - if [ -d "${jukebox_dir}" ]; then - # Houston, we found something! - clear - echo "##################################################### -# -# . . . * alert * alert * alert * alert * . . . -# -# WARNING: an existing Phoniebox installation was found. -# -" - # check if we find the version number - if [ -f "${jukebox_dir}"/settings/version ]; then - #echo "The version of your installation is: $(cat ${jukebox_dir}/settings/version)" - - # get the current short commit hash of the repo - CURRENT_REMOTE_COMMIT="$(git ls-remote ${GIT_URL} ${GIT_BRANCH} | cut -c1-7)" - fi - echo "IMPORTANT: you can use the existing content and configuration" - echo "files for your new install." - echo "Whatever you chose to keep will be moved to the new install." - echo "Everything else will remain in a folder called 'BACKUP'. - " - - ### - # See if we find the PhonieboxInstall.conf file - # We need to do this first, because if we re-use the .conf file, we need to append - # the variables regarding the found content to the also found configuration file. - # That way, reading the configuration file for the (potentially) non-interactive - # install procedure will: - # a) overwrite whatever variables regarding re-cycling existing content which might - # be stored in the config file - # b) if there are no variables for dealing with re-cycled context, we will append - # them - to have them for this install - if [ -f "${jukebox_dir}"/settings/PhonieboxInstall.conf ]; then - # ask for re-using the found configuration file - echo "The configuration of your last Phoniebox install was found." - read -rp "Use existing configuration for this installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGusePhonieboxInstall=NO - ;; - *) - EXISTINGusePhonieboxInstall=YES - # Copy PhonieboxInstall.conf configuration file to settings folder - sudo cp "${jukebox_dir}"/settings/PhonieboxInstall.conf "${home_dir}"/PhonieboxInstall.conf - sudo chown pi:www-data "${home_dir}"/PhonieboxInstall.conf - sudo chmod 775 "${home_dir}"/PhonieboxInstall.conf - echo "The existing configuration will be used." - echo "Just a few more questions to answer." - read -rp "Hit ENTER to proceed to the next step." INPUT - clear - ;; - esac - fi - - # Delete or use existing installation? - read -rp "Re-use config, audio and RFID codes for the new install? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuse=NO - echo "Phoniebox will be a fresh install. The existing version will be dropped." - sudo rm -rf "${jukebox_dir}" - read -rp "Hit ENTER to proceed to the next step." INPUT - ;; - *) - EXISTINGuse=YES - # CREATE BACKUP - # delete existing BACKUP dir if exists - if [ -d "${backup_dir}" ]; then - sudo rm -r "${backup_dir}" - fi - # move install to BACKUP dir - mv "${jukebox_dir}" "${backup_dir}" - # delete .git dir - if [ -d "${backup_dir}"/.git ]; then - sudo rm -r "${backup_dir}"/.git - fi - # delete placeholder files so moving the folder content back later will not create git pull conflicts - rm "${backup_dir}"/shared/audiofolders/placeholder - rm "${backup_dir}"/shared/shortcuts/placeholder - - # ask for things to use - echo "Ok. You want to use stuff from the existing installation." - echo "What would you want to keep? Answer now." - read -rp "RFID config for system control (e.g. 'volume up' etc.)? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseRfidConf=NO - ;; - *) - EXISTINGuseRfidConf=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseRfidConf=$EXISTINGuseRfidConf" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "RFID shortcuts to play audio folders? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseRfidLinks=NO - ;; - *) - EXISTINGuseRfidLinks=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseRfidLinks=$EXISTINGuseRfidLinks" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "Audio folders: use existing? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseAudio=NO - ;; - *) - EXISTINGuseAudio=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseAudio=$EXISTINGuseAudio" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "Sound effects: use existing startup / shutdown sounds? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseSounds=NO - ;; - *) - EXISTINGuseSounds=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseSounds=$EXISTINGuseSounds" >> "${HOME_DIR}/PhonieboxInstall.conf" - - if [ "$(printf '%s\n' "2.1" "$(cat ~/BACKUP/settings/version-number)" | sort -V | head -n1)" = "2.1" ]; then - read -rp "GPIO: use existing file? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseGpio=NO - ;; - *) - EXISTINGuseGpio=YES - ;; - esac - else - echo "" - echo "Warning! -The configuration of GPIO-Devices has changed in the new version -and needs to be reconfigured. For further info check out the wiki: -https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons" - read -rp "Hit ENTER to proceed to the next step." INPUT - config_gpio - fi - # append variables to config file - echo "EXISTINGuseGpio=$EXISTINGuseGpio" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "Button USB Encoder: use existing device and button mapping? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseButtonUSBEncoder=NO - ;; - *) - EXISTINGuseButtonUSBEncoder=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseButtonUSBEncoder=$EXISTINGuseButtonUSBEncoder" >> "${HOME_DIR}/PhonieboxInstall.conf" - - echo "Thanks. Got it." - echo "The existing install can be found in the BACKUP directory." - read -rp "Hit ENTER to proceed to the next step." INPUT - ;; - esac - fi - # append variables to config file - echo "EXISTINGuse=$EXISTINGuse" >> "${HOME_DIR}/PhonieboxInstall.conf" - - # Check if we found a Phoniebox install configuration earlier and ask if to run this now - if [ "${EXISTINGusePhonieboxInstall}" == "YES" ]; then - clear - echo "Using the existing configuration, you can run a non-interactive install." - echo "This will re-cycle found content (specified just now) as well as the" - echo "system information from last time (wifi, audio interface, spotify, etc.)." - read -rp "Do you want to run a non-interactive installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - ;; - *) - cd "${home_dir}" - clear - ./buster-install-default.sh -a - exit - ;; - esac - fi -} - -config_audio_interface() { - ##################################################### - # Audio iFace - - clear - - echo "##################################################### -# -# CONFIGURE AUDIO INTERFACE (iFace) -# -# The default RPi audio interface is 'Headphone'. -# But this does not work for every setup. Here a list of -# available iFace names: -" - amixer scontrols - echo " " - read -rp "Use Headphone as iFace? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - read -rp "Type the iFace name you want to use:" AUDIOiFace - ;; - *) - AUDIOiFace="Headphone" - ;; - esac - # append variables to config file - echo "AUDIOiFace=\"$AUDIOiFace\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - echo "Your iFace is called '$AUDIOiFace'" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -config_spotify() { - ##################################################### - # Configure spotify - - clear - - echo "##################################################### -# -# OPTIONAL: INCLUDE SPOTIFY -# -# Note: if this is your first time installing a phoniebox -# it might be best to do a test install without Spotify -# to make sure all your hardware works. -# -# If you want to include Spotify, MUST have your -# credentials ready: -# -# * username -# * password -# * client_id -# * client_secret - -" - read -rp "Do you want to enable Spotify? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - SPOTinstall=NO - echo "You don't want spotify support." - ;; - *) - SPOTinstall=YES - clear - echo "##################################################### -# -# CREDENTIALS for Spotify -# -# Requires Spotify username, password, client_id and client_secret -# to get connection to Spotify. -# -# (Note: You need a device with browser to generate ID and SECRET) -# -# Please go to this website: -# https://www.mopidy.com/authenticate/ -# and follow the instructions. -# -# Your credential will appear on the site below the login button. -# Please note your client_id and client_secret! -# -" - read -rp "Type your Spotify username: " SPOTIuser - read -rp "Type your Spotify password: " SPOTIpass - read -rp "Type your client_id: " SPOTIclientid - read -rp "Type your client_secret: " SPOTIclientsecret - ;; - esac - # append variables to config file - { - echo "SPOTinstall=\"$SPOTinstall\""; - echo "SPOTIuser=\"$SPOTIuser\""; - echo "SPOTIpass=\"$SPOTIpass\""; - echo "SPOTIclientid=\"$SPOTIclientid\""; - echo "SPOTIclientsecret=\"$SPOTIclientsecret\"" - } >> "${HOME_DIR}/PhonieboxInstall.conf" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -config_mpd() { - ##################################################### - # Configure MPD - - clear - - echo "##################################################### -# -# CONFIGURE MPD -# -# MPD (Music Player Daemon) runs the audio output and must -# be configured. Do it now, if you are unsure. -# (Note: can be done manually later.) -" - read -rp "Do you want to configure MPD? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - MPDconfig=NO - echo "You want to configure MPD later." - ;; - *) - MPDconfig=YES - echo "MPD will be set up with default values." - ;; - esac - # append variables to config file - echo "MPDconfig=\"$MPDconfig\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -config_audio_folder() { - local jukebox_dir="$1" - - ##################################################### - # Folder path for audio files - # default: /home/pi/RPi-Jukebox-RFID/shared/audiofolders - - clear - - echo "##################################################### -# -# FOLDER CONTAINING AUDIO FILES -# -# The default location for folders containing audio files: -# ${jukebox_dir}/shared/audiofolders -# -# If unsure, keep it like this. If your files are somewhere -# else, you can specify the folder in the next step. -# IMPORTANT: the folder will not be created, only the path -# will be remembered. If you use a custom folder, you must -# create it. -" - - read -rp "Do you want to use the default location? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - echo "Please type the absolute path here (no trailing slash)." - echo "Default would be for example: ${jukebox_dir}/shared/audiofolders" - read -r DIRaudioFolders - ;; - *) - DIRaudioFolders="${jukebox_dir}/shared/audiofolders" - ;; - esac - # append variables to config file - echo "DIRaudioFolders=\"$DIRaudioFolders\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - echo "Your audio folders live in this dir:" - echo "${DIRaudioFolders}" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -check_variable() { - local variable=${1} - # check if variable exist and if it's empty - test -z "${!variable+x}" && echo "ERROR: \$${variable} is missing!" && fail=true && return - test "${!variable}" == "" && echo "ERROR: \$${variable} is empty!" && fail=true -} - -config_gpio() { - ##################################################### - # Configure GPIO - - clear - - echo "##################################################### -# -# ACTIVATE GPIO-Control -# -# Activation of the GPIO-Control-Service, which mangages Buttons -# or a Rotary Encoder for Volume and/or Track control. -# To configure the controls please consult the wiki: -# https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons -# It's also possible to activate the service later (see wiki). -" - read -rp "Do you want to activate the GPIO-Control-Service? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - GPIOconfig=NO - echo "You don't want to activate GPIO-Controls now." - ;; - *) - GPIOconfig=YES - echo "GPIO-Control-Service will be activated and set to default values." - ;; - esac - # append variables to config file - echo "GPIOconfig=\"$GPIOconfig\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - echo "" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -check_config_file() { - local install_conf="${HOME_DIR}/PhonieboxInstall.conf" - echo "Checking PhonieboxInstall.conf..." - # check that PhonieboxInstall.conf exists and is not empty - - # check if config file exists - if [[ -f "${install_conf}" ]]; then - # Source config file - source "${install_conf}" - cat "${install_conf}" - echo "" - else - echo "ERROR: ${install_conf} does not exist!" - exit 1 - fi - - fail=false - if [[ -z "${WIFIconfig+x}" ]]; then - echo "ERROR: \$WIFIconfig is missing or not set!" && fail=true - else - if [[ "$WIFIconfig" == "YES" ]]; then - check_variable "WIFIcountryCode" - check_variable "WIFIssid" - check_variable "WIFIpass" - check_variable "WIFIip" - check_variable "WIFIipRouter" - fi - fi - check_variable "EXISTINGuse" - check_variable "AUDIOiFace" - - if [[ -z "${SPOTinstall+x}" ]]; then - echo "ERROR: \$SPOTinstall is missing or not set!" && fail=true - else - if [ "$SPOTinstall" == "YES" ]; then - check_variable "SPOTIuser" - check_variable "SPOTIpass" - check_variable "SPOTIclientid" - check_variable "SPOTIclientsecret" - fi - fi - check_variable "MPDconfig" - check_variable "DIRaudioFolders" - check_variable "GPIOconfig" - - if [ "${fail}" == "true" ]; then - exit 1 - fi - - echo "" -} - -samba_config() { - local smb_conf="/etc/samba/smb.conf" - echo "Configuring Samba..." - # Samba configuration settings - # -rw-r--r-- 1 root root 9416 Apr 30 09:02 /etc/samba/smb.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/smb.conf.buster-default.sample ${smb_conf} - sudo chown root:root "${smb_conf}" - sudo chmod 644 "${smb_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${smb_conf}" - # Samba: create user 'pi' with password 'raspberry' - (echo "raspberry"; echo "raspberry") | sudo smbpasswd -s -a pi -} - -web_server_config() { - local lighthttpd_conf="/etc/lighttpd/lighttpd.conf" - local fastcgi_php_conf="/etc/lighttpd/conf-available/15-fastcgi-php.conf" - local php_ini="/etc/php/7.3/cgi/php.ini" - local sudoers="/etc/sudoers" - - echo "Configuring web server..." - # Web server configuration settings - # -rw-r--r-- 1 root root 1040 Apr 30 09:19 /etc/lighttpd/lighttpd.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/lighttpd.conf.buster-default.sample "${lighthttpd_conf}" - sudo chown root:root "${lighthttpd_conf}" - sudo chmod 644 "${lighthttpd_conf}" - - # Web server PHP7 fastcgi conf - # -rw-r--r-- 1 root root 398 Apr 30 09:35 /etc/lighttpd/conf-available/15-fastcgi-php.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/15-fastcgi-php.conf.buster-default.sample ${fastcgi_php_conf} - sudo chown root:root "${fastcgi_php_conf}" - sudo chmod 644 "${fastcgi_php_conf}" - - # settings for php.ini to support upload - # -rw-r--r-- 1 root root 70999 Jun 14 13:50 /etc/php/7.3/cgi/php.ini - sudo cp "${jukebox_dir}"/misc/sampleconfigs/php.ini.buster-default.sample ${php_ini} - sudo chown root:root "${php_ini}" - sudo chmod 644 "${php_ini}" - - # SUDO users (adding web server here) - # -r--r----- 1 root root 703 Nov 17 21:08 /etc/sudoers - sudo cp "${jukebox_dir}"/misc/sampleconfigs/sudoers.buster-default.sample ${sudoers} - sudo chown root:root "${sudoers}" - sudo chmod 440 "${sudoers}" -} - -install_main() { - local jukebox_dir="$1" - local apt_get="sudo apt-get -qq --yes" - local allow_downgrades="--allow-downgrades --allow-remove-essential --allow-change-held-packages" - - clear - - echo "##################################################### -# -# START INSTALLATION -# -# Good news: you completed the input. -# Let the install begin. -# -# Get yourself a cup of something. The install takes -# between 15 minutes to half an hour, depending on -# your Raspberry Pi and Internet connectivity. -# -# You will be prompted later to complete the installation. -" - - if [[ ${INTERACTIVE} == "true" ]]; then - read -rp "Do you want to start the installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - echo "Exiting the installation." - echo "Your configuration data was saved in this file:" - echo "${HOME_DIR}/PhonieboxInstall.conf" - echo - exit - ;; - esac - fi - - # Start logging here - log_open - - # Add conffile into logfile for better debugging - echo "################################################" - grep -v -e "SPOTI" -e "WIFIpass" "${HOME_DIR}/PhonieboxInstall.conf" - echo "################################################" - - ##################################################### - # INSTALLATION - - # Read install config as written so far - # (this might look stupid so far, but makes sense once - # the option to install from config file is introduced.) - # shellcheck source=scripts/installscripts/tests/ShellCheck/PhonieboxInstall.conf - . "${HOME_DIR}/PhonieboxInstall.conf" - - # power management of wifi: switch off to avoid disconnecting - sudo iwconfig wlan0 power off - - # create backup of /etc/resolv.conf - sudo cp /etc/resolv.conf /etc/resolv.conf.orig - - # Generate locales - sudo locale-gen "${LANG}" - - # Install required packages - ${apt_get} ${allow_downgrades} install apt-transport-https - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - - sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list - - ${apt_get} update - ${apt_get} upgrade - ${apt_get} install libspotify-dev - - # some packages are only available on raspberry pi's but not on test docker containers running on x86_64 machines - if [[ $(uname -m) =~ ^armv.+$ ]]; then - ${apt_get} ${allow_downgrades} install raspberrypi-kernel-headers - fi - - ${apt_get} ${allow_downgrades} install samba samba-common-bin gcc lighttpd php7.3-common php7.3-cgi php7.3 at mpd mpc mpg123 git ffmpeg resolvconf spi-tools - - # restore backup of /etc/resolv.conf in case installation of resolvconf cleared it - sudo cp /etc/resolv.conf.orig /etc/resolv.conf - - # prepare python3 - ${apt_get} ${allow_downgrades} install python3 python3-dev python3-pip python3-mutagen python3-gpiozero python3-spidev - - # use python3.7 as default - sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1 - - # Get github code - cd "${HOME_DIR}" || exit - git clone ${GIT_URL} --branch "${GIT_BRANCH}" - - # VERSION of installation - - # Get version number - VERSION_NO=`cat ${jukebox_dir}/settings/version-number` - - # add used git branch and commit hash to version file - USED_BRANCH="$(git --git-dir=${jukebox_dir}/.git rev-parse --abbrev-ref HEAD)" - - # add git commit hash to version file - COMMIT_NO="$(git --git-dir=${jukebox_dir}/.git describe --always)" - - echo "${VERSION_NO} - ${COMMIT_NO} - ${USED_BRANCH}" > ${jukebox_dir}/settings/version - chmod 777 ${jukebox_dir}/settings/version - - # Install required spotify packages - if [ "${SPOTinstall}" == "YES" ]; then - echo "Installing dependencies for Spotify support..." - # keep major verson 3 of mopidy - echo -e "Package: mopidy\nPin: version 3.*\nPin-Priority: 1001" | sudo tee /etc/apt/preferences.d/mopidy - - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - - sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list - ${apt_get} update - ${apt_get} upgrade - ${apt_get} ${allow_downgrades} install mopidy mopidy-mpd mopidy-local mopidy-spotify - ${apt_get} ${allow_downgrades} install libspotify12 python3-cffi python3-ply python3-pycparser python3-spotify - - # Install necessary Python packages - sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements-spotify.txt - fi - - local raw_github="https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID" - # I comment the following lines out for now. I think they come from splitti when he applied a hotfix in Feb 2020? - # Back then the master install script needed develop branch files. I think this is from that time...? - #sudo rm "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample - #wget -P "${jukebox_dir}"/misc/sampleconfigs/ "${raw_github}"/develop/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample - #sudo rm "${jukebox_dir}"/scripts/RegisterDevice.py - #wget -P "${jukebox_dir}"/scripts/ "${raw_github}"/develop/scripts/RegisterDevice.py - - # Install more required packages - echo "Installing additional Python packages..." - sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements.txt - - samba_config - - web_server_config - - # copy shell script for player - cp "${jukebox_dir}"/settings/rfid_trigger_play.conf.sample "${jukebox_dir}"/settings/rfid_trigger_play.conf - - # creating files containing editable values for configuration - echo "$AUDIOiFace" > "${jukebox_dir}"/settings/Audio_iFace_Name - echo "$DIRaudioFolders" > "${jukebox_dir}"/settings/Audio_Folders_Path - echo "3" > "${jukebox_dir}"/settings/Audio_Volume_Change_Step - echo "100" > "${jukebox_dir}"/settings/Max_Volume_Limit - echo "0" > "${jukebox_dir}"/settings/Idle_Time_Before_Shutdown - echo "RESTART" > "${jukebox_dir}"/settings/Second_Swipe - echo "${jukebox_dir}/playlists" > "${jukebox_dir}"/settings/Playlists_Folders_Path - echo "ON" > "${jukebox_dir}"/settings/ShowCover - - # sample file for debugging with all options set to FALSE - sudo cp "${jukebox_dir}"/settings/debugLogging.conf.sample "${jukebox_dir}"/settings/debugLogging.conf - sudo chmod 777 "${jukebox_dir}"/settings/debugLogging.conf - - # The new way of making the bash daemon is using the helperscripts - # creating the shortcuts and script from a CSV file. - # see scripts/helperscripts/AssignIDs4Shortcuts.php - - # create config file for web app from sample - sudo cp "${jukebox_dir}"/htdocs/config.php.sample "${jukebox_dir}"/htdocs/config.php - - # Starting web server and php7 - sudo lighttpd-enable-mod fastcgi - sudo lighttpd-enable-mod fastcgi-php - sudo service lighttpd force-reload - - # create copy of GPIO script - sudo cp "${jukebox_dir}"/misc/sampleconfigs/gpio-buttons.py.sample "${jukebox_dir}"/scripts/gpio-buttons.py - sudo chmod +x "${jukebox_dir}"/scripts/gpio-buttons.py - - # make sure bash scripts have the right settings - sudo chown pi:www-data "${jukebox_dir}"/scripts/*.sh - sudo chmod +x "${jukebox_dir}"/scripts/*.sh - sudo chown pi:www-data "${jukebox_dir}"/scripts/*.py - sudo chmod +x "${jukebox_dir}"/scripts/*.py - - # services to launch after boot using systemd - # -rw-r--r-- 1 root root 304 Apr 30 10:07 phoniebox-rfid-reader.service - # 1. delete old services (this is legacy, might throw errors but is necessary. Valid for versions < 1.1.8-beta) - local systemd_dir="/etc/systemd/system" - echo "### Deleting older versions of service daemons. This might throw errors, ignore them" - sudo systemctl disable idle-watchdog - sudo systemctl disable rfid-reader - sudo systemctl disable phoniebox-startup-sound - sudo systemctl disable gpio-buttons - sudo systemctl disable phoniebox-rotary-encoder - sudo systemctl disable phoniebox-gpio-buttons.service - sudo rm "${systemd_dir}"/rfid-reader.service - sudo rm "${systemd_dir}"/startup-sound.service - sudo rm "${systemd_dir}"/gpio-buttons.service - sudo rm "${systemd_dir}"/idle-watchdog.service - sudo rm "${systemd_dir}"/phoniebox-rotary-encoder.service - sudo rm "${systemd_dir}"/phoniebox-gpio-buttons.service - echo "### Done with erasing old daemons. Stop ignoring errors!" - # 2. install new ones - this is version > 1.1.8-beta - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample "${systemd_dir}"/phoniebox-rfid-reader.service - #startup sound now part of phoniebox-startup-scripts - #sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-sound.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-scripts.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-buttons.service.stretch-default.sample "${systemd_dir}"/phoniebox-gpio-buttons.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample "${systemd_dir}"/phoniebox-idle-watchdog.service - [[ "${GPIOconfig}" == "YES" ]] && sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-control.service.sample "${systemd_dir}"/phoniebox-gpio-control.service - sudo chown root:root "${systemd_dir}"/phoniebox-*.service - sudo chmod 644 "${systemd_dir}"/phoniebox-*.service - # enable the services needed - sudo systemctl enable phoniebox-idle-watchdog - sudo systemctl enable phoniebox-rfid-reader - #startup sound is part of phoniebox-startup-scripts now - #sudo systemctl enable phoniebox-startup-sound - sudo systemctl enable phoniebox-startup-scripts - sudo systemctl enable phoniebox-gpio-buttons - sudo systemctl enable phoniebox-rotary-encoder.service - - # copy mp3s for startup and shutdown sound to the right folder - cp "${jukebox_dir}"/misc/sampleconfigs/startupsound.mp3.sample "${jukebox_dir}"/shared/startupsound.mp3 - cp "${jukebox_dir}"/misc/sampleconfigs/shutdownsound.mp3.sample "${jukebox_dir}"/shared/shutdownsound.mp3 - - # Spotify config - if [ "${SPOTinstall}" == "YES" ]; then - local etc_mopidy_conf="/etc/mopidy/mopidy.conf" - local mopidy_conf="${HOME_DIR}/.config/mopidy/mopidy.conf" - echo "Configuring Spotify support..." - sudo systemctl disable mpd - sudo systemctl enable mopidy - # Install Config Files - sudo cp "${jukebox_dir}"/misc/sampleconfigs/locale.gen.sample /etc/locale.gen - sudo cp "${jukebox_dir}"/misc/sampleconfigs/locale.sample /etc/default/locale - sudo locale-gen - mkdir -p "${HOME_DIR}"/.config/mopidy - sudo cp "${jukebox_dir}"/misc/sampleconfigs/mopidy-etc.sample "${etc_mopidy_conf}" - cp "${jukebox_dir}"/misc/sampleconfigs/mopidy.sample "${mopidy_conf}" - # Change vars to match install config - sudo sed -i 's/%spotify_username%/'"$SPOTIuser"'/' "${etc_mopidy_conf}" - sudo sed -i 's/%spotify_password%/'"$SPOTIpass"'/' "${etc_mopidy_conf}" - sudo sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' "${etc_mopidy_conf}" - sudo sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${etc_mopidy_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${etc_mopidy_conf}" - sed -i 's/%spotify_username%/'"$SPOTIuser"'/' "${mopidy_conf}" - sed -i 's/%spotify_password%/'"$SPOTIpass"'/' "${mopidy_conf}" - sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' "${mopidy_conf}" - sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${mopidy_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mopidy_conf}" - fi - - # GPIO-Control - if [[ "${GPIOconfig}" == "YES" ]]; then - sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements-GPIO.txt - sudo systemctl enable phoniebox-gpio-control.service - if [[ ! -f ~/.config/phoniebox/gpio_settings.ini ]]; then - mkdir -p ~/.config/phoniebox - cp "${jukebox_dir}"/components/gpio_control/example_configs/gpio_settings.ini ~/.config/phoniebox/gpio_settings.ini - fi - fi - - if [ "${MPDconfig}" == "YES" ]; then - local mpd_conf="/etc/mpd.conf" - - echo "Configuring MPD..." - # MPD configuration - # -rw-r----- 1 mpd audio 14043 Jul 17 20:16 /etc/mpd.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/mpd.conf.buster-default.sample ${mpd_conf} - # Change vars to match install config - sudo sed -i 's/%AUDIOiFace%/'"$AUDIOiFace"'/' "${mpd_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mpd_conf}" - sudo chown mpd:audio "${mpd_conf}" - sudo chmod 640 "${mpd_conf}" - fi - - # set which version has been installed - if [ "${SPOTinstall}" == "YES" ]; then - echo "plusSpotify" > "${jukebox_dir}"/settings/edition - else - echo "classic" > "${jukebox_dir}"/settings/edition - fi - - # update mpc / mpd DB - mpc update - - # / INSTALLATION - ##################################################### -} - -wifi_settings() { - local sample_configs_dir="$1" - local dhcpcd_conf="$2" - local wpa_supplicant_conf="$3" - - ############################### - # WiFi settings (SSID password) - # - # https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md - # - # $WIFIssid - # $WIFIpass - # $WIFIip - # $WIFIipRouter - if [ "${WIFIconfig}" == "YES" ]; then - # DHCP configuration settings - echo "Setting ${dhcpcd_conf}..." - #-rw-rw-r-- 1 root netdev 0 Apr 17 11:25 /etc/dhcpcd.conf - sudo cp "${sample_configs_dir}"/dhcpcd.conf.buster-default-noHotspot.sample "${dhcpcd_conf}" - # Change IP for router and Phoniebox - sudo sed -i 's/%WIFIip%/'"$WIFIip"'/' "${dhcpcd_conf}" - sudo sed -i 's/%WIFIipRouter%/'"$WIFIipRouter"'/' "${dhcpcd_conf}" - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' "${dhcpcd_conf}" - # Change user:group and access mod - sudo chown root:netdev "${dhcpcd_conf}" - sudo chmod 664 "${dhcpcd_conf}" - - # WiFi SSID & Password - echo "Setting ${wpa_supplicant_conf}..." - # -rw-rw-r-- 1 root netdev 137 Jul 16 08:53 /etc/wpa_supplicant/wpa_supplicant.conf - sudo cp "${sample_configs_dir}"/wpa_supplicant.conf.buster-default.sample "${wpa_supplicant_conf}" - sudo sed -i 's/%WIFIssid%/'"$WIFIssid"'/' "${wpa_supplicant_conf}" - sudo sed -i 's/%WIFIpass%/'"$WIFIpass"'/' "${wpa_supplicant_conf}" - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' "${wpa_supplicant_conf}" - sudo chown root:netdev "${wpa_supplicant_conf}" - sudo chmod 664 "${wpa_supplicant_conf}" - fi - - # start DHCP - echo "Starting dhcpcd service..." - sudo service dhcpcd start - sudo systemctl enable dhcpcd - -# / WiFi settings (SSID password) -############################### -} - -existing_assets() { - local jukebox_dir="$1" - local backup_dir="$2" - - ##################################################### - # EXISTING ASSETS TO USE FROM EXISTING INSTALL - - if [ "${EXISTINGuse}" == "YES" ]; then - # RFID config for system control - if [ "${EXISTINGuseRfidConf}" == "YES" ]; then - # read old values and write them into new file (copied above already) - # do not overwrite but use 'sed' in case there are new vars in new version installed - - # Read the existing RFID config file line by line and use - # only lines which are separated (IFS) by '='. - while IFS='=' read -r key val ; do - # $var should be stripped of possible leading or trailing " - val=${val%\"} - val=${val#\"} - key=${key} - # Additional error check: key should not start with a hash and not be empty. - if [ ! "${key:0:1}" == '#' ] && [ -n "$key" ]; then - # Replace the matching value in the newly created conf file - sed -i 's/%'"$key"'%/'"$val"'/' "${jukebox_dir}"/settings/rfid_trigger_play.conf - fi - done <"${backup_dir}"/settings/rfid_trigger_play.conf - fi - - # RFID shortcuts for audio folders - if [ "${EXISTINGuseRfidLinks}" == "YES" ]; then - # copy from backup to new install - cp -R "${backup_dir}"/shared/shortcuts/* "${jukebox_dir}"/shared/shortcuts/ - fi - - # Audio folders: use existing - if [ "${EXISTINGuseAudio}" == "YES" ]; then - # copy from backup to new install - cp -R "${backup_dir}"/shared/audiofolders/* "$DIRaudioFolders/" - fi - - # GPIO: use existing file - if [ "${EXISTINGuseGpio}" == "YES" ]; then - # copy from backup to new install - cp "${backup_dir}"/settings/gpio_settings.ini "${jukebox_dir}"/settings/gpio_settings.ini - fi - - # Button USB Encoder: use existing file - if [ "${EXISTINGuseButtonUSBEncoder}" == "YES" ]; then - # copy from backup to new install - cp "${backup_dir}"/components/controls/buttons_usb_encoder/deviceName.txt "${jukebox_dir}"/components/controls/buttons_usb_encoder/deviceName.txt - cp "${backup_dir}"/components/controls/buttons_usb_encoder/buttonMap.json "${jukebox_dir}"/components/controls/buttons_usb_encoder/buttonMap.json - # make buttons_usb_encoder.py ready to be use from phoniebox-buttons-usb-encoder service - sudo chmod +x "${jukebox_dir}"/components/controls/buttons_usb_encoder/buttons_usb_encoder.py - # make sure service is still enabled by registering again - sudo cp -v "${jukebox_dir}"/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample /etc/systemd/system/phoniebox-buttons-usb-encoder.service - sudo systemctl start phoniebox-buttons-usb-encoder.service - sudo systemctl enable phoniebox-buttons-usb-encoder.service - fi - - # Sound effects: use existing startup / shutdown sounds - if [ "${EXISTINGuseSounds}" == "YES" ]; then - # copy from backup to new install - cp "${backup_dir}"/shared/startupsound.mp3 "${jukebox_dir}"/shared/startupsound.mp3 - cp "${backup_dir}"/shared/shutdownsound.mp3 "${jukebox_dir}"/shared/shutdownsound.mp3 - fi - - fi - - # / EXISTING ASSETS TO USE FROM EXISTING INSTALL - ################################################ -} - - -folder_access() { - local jukebox_dir="$1" - local user_group="$2" - local mod="$3" - - ##################################################### - # Folders and Access Settings - - echo "Setting owner and permissions for directories..." - - # create playlists folder - mkdir -p "${jukebox_dir}"/playlists - sudo chown -R "${user_group}" "${jukebox_dir}"/playlists - sudo chmod -R "${mod}" "${jukebox_dir}"/playlists - - # make sure the shared folder is accessible by the web server - sudo chown -R "${user_group}" "${jukebox_dir}"/shared - sudo chmod -R "${mod}" "${jukebox_dir}"/shared - - # make sure the htdocs folder can be changed by the web server - sudo chown -R "${user_group}" "${jukebox_dir}"/htdocs - sudo chmod -R "${mod}" "${jukebox_dir}"/htdocs - - sudo chown -R "${user_group}" "${jukebox_dir}"/settings - sudo chmod -R "${mod}" "${jukebox_dir}"/settings - - # logs dir accessible by pi and www-data - sudo chown "${user_group}" "${jukebox_dir}"/logs - sudo chmod "${mod}" "${jukebox_dir}"/logs - - # audio folders might be somewhere else, so treat them separately - sudo chown "${user_group}" "${DIRaudioFolders}" - sudo chmod "${mod}" "${DIRaudioFolders}" - - # make sure bash scripts have the right settings - sudo chown "${user_group}" "${jukebox_dir}"/scripts/*.sh - sudo chmod +x "${jukebox_dir}"/scripts/*.sh - sudo chown "${user_group}" "${jukebox_dir}"/scripts/*.py - sudo chmod +x "${jukebox_dir}"/scripts/*.py - - # set audio volume to 100% - # see: https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/54 - sudo amixer cset numid=1 100% - - # delete the global.conf file, in case somebody manually copied stuff back and forth - # this will be created the first time the Phoniebox is put to use by web app or RFID - GLOBAL_CONF="${jukebox_dir}"/settings/global.conf - if [ -f "${GLOBAL_CONF}" ]; then - echo "global.conf needs to be deleted." - rm "${GLOBAL_CONF}" - fi - - # / Access settings - ##################################################### -} - -autohotspot() { - local jukebox_dir="$1" - local apt_get="sudo apt-get -qq --yes" - - # adapted from https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection - - # required packages - ${apt_get} install dnsmasq hostapd - sudo systemctl unmask hostapd - sudo systemctl disable hostapd - sudo systemctl disable dnsmasq - - # configure DNS - if [ -f /etc/dnsmasq.conf ]; then - sudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.orig - sudo touch /etc/dnsmasq.conf - else - sudo touch /etc/dnsmasq.conf - fi - sudo bash -c 'cat << EOF > /etc/dnsmasq.conf -#AutoHotspot Config -#stop DNSmasq from using resolv.conf -no-resolv -#Interface to use -interface=wlan0 -bind-interfaces -dhcp-range=10.0.0.50,10.0.0.150,12h -EOF' - - # configure hotspot - if [ -f /etc/hostapd/hostapd.conf ]; then - sudo mv /etc/hostapd/hostapd.conf /etc/hostapd/hostapd.conf.orig - sudo touch /etc/hostapd/hostapd.conf - else - sudo touch /etc/hostapd/hostapd.conf - fi - sudo bash -c 'cat << EOF > /etc/hostapd/hostapd.conf -#2.4GHz setup wifi 80211 b,g,n -interface=wlan0 -driver=nl80211 -ssid=phoniebox -hw_mode=g -channel=8 -wmm_enabled=0 -macaddr_acl=0 -auth_algs=1 -ignore_broadcast_ssid=0 -wpa=2 -wpa_passphrase=PlayItLoud -wpa_key_mgmt=WPA-PSK -wpa_pairwise=CCMP TKIP -rsn_pairwise=CCMP - -#80211n - Change DE to your WiFi country code -country_code=DE -ieee80211n=1 -ieee80211d=1 -EOF' - - # configure Hotspot daemon - if [ -f /etc/default/hostapd ]; then - sudo mv /etc/default/hostapd /etc/default/hostapd.orig - sudo touch /etc/default/hostapd - else - sudo touch /etc/default/hostapd - fi - sudo bash -c 'cat << EOF > /etc/default/hostapd -DAEMON_CONF="/etc/hostapd/hostapd.conf" -EOF' - - if [ $(grep -v '^$' /etc/network/interfaces |wc -l) -gt 5 ]; then - sudo cp /etc/network/interfaces /etc/network/interfaces-backup - fi - - # disable powermanagement of wlan0 device - sudo iw wlan0 set power_save off - - if [[ ! $(grep "nohook wpa_supplicant" /etc/dhcpcd.conf) ]]; then - sudo echo -e "nohook wpa_supplicant" >> /etc/dhcpcd.conf - fi - - # create service to trigger hotspot - sudo bash -c 'cat << EOF > /etc/systemd/system/autohotspot.service -[Unit] -Description=Automatically generates an internet Hotspot when a valid ssid is not in range -After=multi-user.target -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=/usr/bin/autohotspot -[Install] -WantedBy=multi-user.target -EOF' - - sudo systemctl enable autohotspot.service - - sudo cp "${jukebox_dir}"/scripts/helperscripts/autohotspot /usr/bin/autohotspot - sudo chmod +x /usr/bin/autohotspot - - # create crontab entry - if [[ ! $(grep "autohotspot" /var/spool/cron/crontabs/pi) ]]; then - sudo bash -c 'cat << EOF >> /var/spool/cron/crontabs/pi -*/5 * * * * sudo /usr/bin/autohotspot >/dev/null 2>&1 -EOF' - fi - sudo chown pi:crontab /var/spool/cron/crontabs/pi - sudo chmod 600 /var/spool/cron/crontabs/pi - sudo /usr/bin/crontab /var/spool/cron/crontabs/pi - -} - -finish_installation() { - local jukebox_dir="$1" - echo " -# -# INSTALLATION FINISHED -# -##################################################### -" - - ##################################################### - # Register external device(s) - - echo "If you are using an RFID reader, connect it to your RPi." - echo "(In case your RFID reader required soldering, consult the manual.)" - # Use -e to display response of user in the logfile - read -e -r -p "Have you connected your RFID reader? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - ;; - *) - echo 'Please select the RFID reader you want to use' - options=("USB-Reader (e.g. Neuftech)" "RC522" "PN532" "Manual configuration" "Multiple RFID reader") - select opt in "${options[@]}"; do - case $opt in - "USB-Reader (e.g. Neuftech)") - cd "${jukebox_dir}"/scripts/ || exit - python3 RegisterDevice.py - sudo chown pi:www-data "${jukebox_dir}"/scripts/deviceName.txt - sudo chmod 644 "${jukebox_dir}"/scripts/deviceName.txt - break - ;; - "RC522") - bash "${jukebox_dir}"/components/rfid-reader/RC522/setup_rc522.sh - break - ;; - "PN532") - bash "${jukebox_dir}"/components/rfid-reader/PN532/setup_pn532.sh - break - ;; - "Manual configuration") - echo "Please configure your reader manually." - break - ;; - "Multiple RFID reader") - cd "${jukebox_dir}"/scripts/ || exit - sudo python3 RegisterDevice.py.Multi - break - ;; - *) - echo "This is not a number" - ;; - esac - done - esac - - echo - echo "DONE. Let the sounds begin." - echo "Find more information and documentation on the github account:" - echo "https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/" - - echo "Reboot is needed to activate all settings" - # Use -e to display response of user in the logfile - read -e -r -p "Would you like to reboot now? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - # Close logging - log_close - ;; - *) - # Close logging - log_close - sudo shutdown -r now - ;; - esac -} - -######## -# Main # -######## -main() { - if [[ ${INTERACTIVE} == "true" ]]; then - welcome - #reset_install_config_file - config_wifi - check_existing "${JUKEBOX_HOME_DIR}" "${JUKEBOX_BACKUP_DIR}" "${HOME_DIR}" - config_audio_interface - config_spotify - config_mpd - config_audio_folder "${JUKEBOX_HOME_DIR}" - config_gpio - else - echo "Non-interactive installation!" - check_config_file - # Skip interactive Samba WINS config dialog - echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections - fi - install_main "${JUKEBOX_HOME_DIR}" - wifi_settings "${JUKEBOX_HOME_DIR}/misc/sampleconfigs" "/etc/dhcpcd.conf" "/etc/wpa_supplicant/wpa_supplicant.conf" - existing_assets "${JUKEBOX_HOME_DIR}" "${JUKEBOX_BACKUP_DIR}" - folder_access "${JUKEBOX_HOME_DIR}" "pi:www-data" 775 - autohotspot "${JUKEBOX_HOME_DIR}" - - # Copy PhonieboxInstall.conf configuration file to settings folder - sudo cp "${HOME_DIR}/PhonieboxInstall.conf" "${JUKEBOX_HOME_DIR}/settings/" - sudo chown pi:www-data "${JUKEBOX_HOME_DIR}/settings/PhonieboxInstall.conf" - sudo chmod 775 "${JUKEBOX_HOME_DIR}/settings/PhonieboxInstall.conf" - - if [[ ${INTERACTIVE} == "true" ]]; then - finish_installation "${JUKEBOX_HOME_DIR}" - else - echo "Skipping USB device setup..." - echo "For manual registration of a USB card reader type:" - echo "python3 /home/pi/RPi-Jukebox-RFID/scripts/RegisterDevice.py" - echo " " - echo "Reboot is required to activate all settings!" - fi -} - -start=$(date +%s) - -main - -end=$(date +%s) -runtime=$((end-start)) -((h=${runtime}/3600)) -((m=(${runtime}%3600)/60)) -((s=${runtime}%60)) -echo "Done (in ${h}h ${m}m ${s}s)." - -##################################################### -# notes for things to do - -# CLEANUP -## remove dir BACKUP (possibly not, because we do this at the beginning after user confirms for latest config) -##################################################### diff --git a/scripts/installscripts/buster-install-default.sh b/scripts/installscripts/buster-install-default.sh deleted file mode 100755 index f0bf19944..000000000 --- a/scripts/installscripts/buster-install-default.sh +++ /dev/null @@ -1,1299 +0,0 @@ -#!/usr/bin/env bash -# -# see https://github.com/MiczFlor/RPi-Jukebox-RFID for details -# -# NOTE: Running automated install (without interaction): -# Each install creates a file called PhonieboxInstall.conf -# in the folder /home/pi/ -# You can install the Phoniebox using such a config file -# which means you don't need to run the interactive install: -# -# 1. download the install file from github -# https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/scripts/installscripts -# (note: currently only works for buster and newer OS) -# 2. make the file executable: chmod +x -# 3. place the PhonieboxInstall.conf in the folder /home/pi/ -# 4. run the installscript with option -a like this: -# buster-install-default.sh -a - -# The absolute path to the folder which contains this script -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -GIT_BRANCH=${GIT_BRANCH:-master} -GIT_URL=${GIT_URL:-https://github.com/MiczFlor/RPi-Jukebox-RFID.git} -echo GIT_BRANCH $GIT_BRANCH -echo GIT_URL $GIT_URL - -DATETIME=$(date +"%Y%m%d_%H%M%S") - -SCRIPTNAME="$(basename $0)" -JOB="${SCRIPTNAME}" - -HOME_DIR="/home/pi" - -JUKEBOX_HOME_DIR="${HOME_DIR}/RPi-Jukebox-RFID" -LOGDIR="${HOME_DIR}"/phoniebox_logs -JUKEBOX_BACKUP_DIR="${HOME_DIR}/BACKUP" - -INTERACTIVE=true - -usage() { - printf "Usage: ${SCRIPTNAME} [-a] [-h]\n" - printf " -a\tautomatic/non-interactive mode\n" - printf " -h\thelp\n" - exit 0 -} - -while getopts ":ah" opt; -do - case ${opt} in - a ) INTERACTIVE=false - ;; - h ) usage - ;; - \? ) usage - ;; - esac -done - - -# Setup logger functions -# Input from http://www.ludovicocaldara.net/dba/bash-tips-5-output-logfile/ -log_open() { - [[ -d "${LOGDIR}" ]] || mkdir -p "${LOGDIR}" - PIPE="${LOGDIR}"/"${JOB}"_"${DATETIME}".pipe - mkfifo -m 700 "${PIPE}" - LOGFILE="${LOGDIR}"/"${JOB}"_"${DATETIME}".log - exec 3>&1 - tee "${LOGFILE}" <"${PIPE}" >&3 & - TEEPID=$! - exec 1>"${PIPE}" 2>&1 - PIPE_OPENED=1 -} - -log_close() { - if [ "${PIPE_OPENED}" ]; then - exec 1<&3 - sleep 0.2 - ps --pid "${TEEPID}" >/dev/null - if [ $? -eq 0 ] ; then - # a wait ${TEEPID} whould be better but some - # commands leave file descriptors open - sleep 1 - kill "${TEEPID}" - fi - rm "${PIPE}" - unset PIPE_OPENED - fi -} - - -welcome() { - clear - echo "##################################################### -# ___ __ ______ _ __________ ____ __ _ _ # -# / _ \/ // / __ \/ |/ / _/ __/( _ \ / \( \/ ) # -# / ___/ _ / /_/ / // // _/ ) _ (( O )) ( # -# /_/ /_//_/\____/_/|_/___/____/ (____/ \__/(_/\_) # -# # -##################################################### - -You are turning your Raspberry Pi into a Phoniebox. Good choice. -This INTERACTIVE INSTALL script requires you to be online and -will guide you through the configuration. - -If you want to run the AUTOMATED INSTALL (non-interactive) from -an existing configuration file, do the following: -1. exit this install script (press n) -2. place your PhonieboxInstall.conf in the folder /home/pi/ -3. run the installscript with option -a. For example like this: - ./home/pi/buster-install-default.sh -a - " - read -rp "Continue interactive installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - exit - ;; - *) - echo "Installation continues..." - ;; - esac -} - -reset_install_config_file() { - ##################################################### - # CONFIG FILE - # This file will contain all the data given in the - # following dialogue - # At a later stage, the install should also be done - # from such a config file with no user input. - - # Remove existing config file - #rm "${HOME_DIR}/PhonieboxInstall.conf" - # Create empty config file - #touch "${HOME_DIR}/PhonieboxInstall.conf" - #echo "# Phoniebox config" > "${HOME_DIR}/PhonieboxInstall.conf" - echo "# Phoniebox config" -} - -config_wifi() { - ##################################################### - # Ask if wifi config - - clear - - echo "##################################################### -# -# CONFIGURE WIFI -# -# Requires SSID, WiFi password and the static IP you want -# to assign to your Phoniebox. -# (Note: can be done manually later, if you are unsure.) -" -read -rp "Do you want to configure your WiFi? [Y/n] " response -echo "" -case "$response" in - [nN][oO]|[nN]) - WIFIconfig=NO - echo "You want to configure WiFi later." - # append variables to config file - echo "WIFIconfig=$WIFIconfig" >> "${HOME_DIR}/PhonieboxInstall.conf" - # make a fallback for WiFi Country Code, because we need that even without WiFi config - echo "WIFIcountryCode=DE" >> "${HOME_DIR}/PhonieboxInstall.conf" - ;; - *) - WIFIconfig=YES - #Ask for SSID - read -rp "* Type SSID name: " WIFIssid - #Ask for wifi country code - read -rp "* WiFi Country Code (e.g. DE, GB, CZ or US): " WIFIcountryCode - #Ask for password - read -rp "* Type password: " WIFIpass - #Ask for IP - read -rp "* Static IP (e.g. 192.168.1.199): " WIFIip - #Ask for Router IP - read -rp "* Router IP (e.g. 192.168.1.1): " WIFIipRouter - echo "" - echo "Your WiFi config:" - echo "SSID : $WIFIssid" - echo "WiFi Country Code : $WIFIcountryCode" - echo "Password : $WIFIpass" - echo "Static IP : $WIFIip" - echo "Router IP : $WIFIipRouter" - read -rp "Are these values correct? [Y/n] " response - echo "" - case "$response" in - [nN][oO]|[nN]) - echo "The values are incorrect." - read -rp "Hit ENTER to exit and start over." INPUT; exit - ;; - *) - # append variables to config file - { - echo "WIFIconfig=\"$WIFIconfig\""; - echo "WIFIcountryCode=\"$WIFIcountryCode\""; - echo "WIFIssid=\"$WIFIssid\""; - echo "WIFIpass=\"$WIFIpass\""; - echo "WIFIip=\"$WIFIip\""; - echo "WIFIipRouter=\"$WIFIipRouter\""; - } >> "${HOME_DIR}/PhonieboxInstall.conf" - ;; - esac - ;; -esac -read -rp "Hit ENTER to proceed to the next step." INPUT -} - -check_existing() { - local jukebox_dir="$1" - local backup_dir="$2" - local home_dir="$3" - - ##################################################### - # Check for existing Phoniebox - # - # In case there is no existing install, - # set the var now for later use: - EXISTINGuse=NO - - # The install will be in the home dir of user pi - # Move to home directory now to check - cd ~ || exit - if [ -d "${jukebox_dir}" ]; then - # Houston, we found something! - clear - echo "##################################################### -# -# . . . * alert * alert * alert * alert * . . . -# -# WARNING: an existing Phoniebox installation was found. -# -" - # check if we find the version number - if [ -f "${jukebox_dir}"/settings/version ]; then - #echo "The version of your installation is: $(cat ${jukebox_dir}/settings/version)" - - # get the current short commit hash of the repo - CURRENT_REMOTE_COMMIT="$(git ls-remote ${GIT_URL} ${GIT_BRANCH} | cut -c1-7)" - fi - echo "IMPORTANT: you can use the existing content and configuration" - echo "files for your new install." - echo "Whatever you chose to keep will be moved to the new install." - echo "Everything else will remain in a folder called 'BACKUP'. - " - - ### - # See if we find the PhonieboxInstall.conf file - # We need to do this first, because if we re-use the .conf file, we need to append - # the variables regarding the found content to the also found configuration file. - # That way, reading the configuration file for the (potentially) non-interactive - # install procedure will: - # a) overwrite whatever variables regarding re-cycling existing content which might - # be stored in the config file - # b) if there are no variables for dealing with re-cycled context, we will append - # them - to have them for this install - if [ -f "${jukebox_dir}"/settings/PhonieboxInstall.conf ]; then - # ask for re-using the found configuration file - echo "The configuration of your last Phoniebox install was found." - read -rp "Use existing configuration for this installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGusePhonieboxInstall=NO - ;; - *) - EXISTINGusePhonieboxInstall=YES - # Copy PhonieboxInstall.conf configuration file to settings folder - sudo cp "${jukebox_dir}"/settings/PhonieboxInstall.conf "${home_dir}"/PhonieboxInstall.conf - sudo chown pi:www-data "${home_dir}"/PhonieboxInstall.conf - sudo chmod 775 "${home_dir}"/PhonieboxInstall.conf - echo "The existing configuration will be used." - echo "Just a few more questions to answer." - read -rp "Hit ENTER to proceed to the next step." INPUT - clear - ;; - esac - fi - - # Delete or use existing installation? - read -rp "Re-use config, audio and RFID codes for the new install? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuse=NO - echo "Phoniebox will be a fresh install. The existing version will be dropped." - sudo rm -rf "${jukebox_dir}" - read -rp "Hit ENTER to proceed to the next step." INPUT - ;; - *) - EXISTINGuse=YES - # CREATE BACKUP - # delete existing BACKUP dir if exists - if [ -d "${backup_dir}" ]; then - sudo rm -r "${backup_dir}" - fi - # move install to BACKUP dir - mv "${jukebox_dir}" "${backup_dir}" - # delete .git dir - if [ -d "${backup_dir}"/.git ]; then - sudo rm -r "${backup_dir}"/.git - fi - # delete placeholder files so moving the folder content back later will not create git pull conflicts - rm "${backup_dir}"/shared/audiofolders/placeholder - rm "${backup_dir}"/shared/shortcuts/placeholder - - # ask for things to use - echo "Ok. You want to use stuff from the existing installation." - echo "What would you want to keep? Answer now." - read -rp "RFID config for system control (e.g. 'volume up' etc.)? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseRfidConf=NO - ;; - *) - EXISTINGuseRfidConf=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseRfidConf=$EXISTINGuseRfidConf" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "RFID shortcuts to play audio folders? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseRfidLinks=NO - ;; - *) - EXISTINGuseRfidLinks=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseRfidLinks=$EXISTINGuseRfidLinks" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "Audio folders: use existing? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseAudio=NO - ;; - *) - EXISTINGuseAudio=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseAudio=$EXISTINGuseAudio" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "Sound effects: use existing startup / shutdown sounds? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseSounds=NO - ;; - *) - EXISTINGuseSounds=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseSounds=$EXISTINGuseSounds" >> "${HOME_DIR}/PhonieboxInstall.conf" - - if [ "$(printf '%s\n' "2.1" "$(cat ~/BACKUP/settings/version-number)" | sort -V | head -n1)" = "2.1" ]; then - read -rp "GPIO: use existing file? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseGpio=NO - ;; - *) - EXISTINGuseGpio=YES - ;; - esac - else - echo "" - echo "Warning! -The configuration of GPIO-Devices has changed in the new version -and needs to be reconfigured. For further info check out the wiki: -https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons" - read -rp "Hit ENTER to proceed to the next step." INPUT - config_gpio - fi - # append variables to config file - echo "EXISTINGuseGpio=$EXISTINGuseGpio" >> "${HOME_DIR}/PhonieboxInstall.conf" - - read -rp "Button USB Encoder: use existing device and button mapping? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseButtonUSBEncoder=NO - ;; - *) - EXISTINGuseButtonUSBEncoder=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseButtonUSBEncoder=$EXISTINGuseButtonUSBEncoder" >> "${HOME_DIR}/PhonieboxInstall.conf" - - echo "Thanks. Got it." - echo "The existing install can be found in the BACKUP directory." - read -rp "Hit ENTER to proceed to the next step." INPUT - ;; - esac - fi - # append variables to config file - echo "EXISTINGuse=$EXISTINGuse" >> "${HOME_DIR}/PhonieboxInstall.conf" - - # Check if we found a Phoniebox install configuration earlier and ask if to run this now - if [ "${EXISTINGusePhonieboxInstall}" == "YES" ]; then - clear - echo "Using the existing configuration, you can run a non-interactive install." - echo "This will re-cycle found content (specified just now) as well as the" - echo "system information from last time (wifi, audio interface, spotify, etc.)." - read -rp "Do you want to run a non-interactive installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - ;; - *) - cd "${home_dir}" - clear - ./buster-install-default.sh -a - exit - ;; - esac - fi -} - -config_audio_interface() { - ##################################################### - # Audio iFace - - clear - - echo "##################################################### -# -# CONFIGURE AUDIO INTERFACE (iFace) -# -# The default RPi audio interface is 'Headphone'. -# But this does not work for every setup. Here a list of -# available iFace names: -" - amixer scontrols - echo " " - read -rp "Use Headphone as iFace? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - read -rp "Type the iFace name you want to use:" AUDIOiFace - ;; - *) - AUDIOiFace="Headphone" - ;; - esac - # append variables to config file - echo "AUDIOiFace=\"$AUDIOiFace\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - echo "Your iFace is called '$AUDIOiFace'" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -config_spotify() { - ##################################################### - # Configure spotify - - clear - - echo "##################################################### -# -# OPTIONAL: INCLUDE SPOTIFY -# -# Note: if this is your first time installing a phoniebox -# it might be best to do a test install without Spotify -# to make sure all your hardware works. -# -# If you want to include Spotify, MUST have your -# credentials ready: -# -# * username -# * password -# * client_id -# * client_secret - -" - read -rp "Do you want to enable Spotify? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - SPOTinstall=NO - echo "You don't want spotify support." - ;; - *) - SPOTinstall=YES - clear - echo "##################################################### -# -# CREDENTIALS for Spotify -# -# Requires Spotify username, password, client_id and client_secret -# to get connection to Spotify. -# -# (Note: You need a device with browser to generate ID and SECRET) -# -# Please go to this website: -# https://www.mopidy.com/authenticate/ -# and follow the instructions. -# -# Your credential will appear on the site below the login button. -# Please note your client_id and client_secret! -# -" - read -rp "Type your Spotify username: " SPOTIuser - read -rp "Type your Spotify password: " SPOTIpass - read -rp "Type your client_id: " SPOTIclientid - read -rp "Type your client_secret: " SPOTIclientsecret - ;; - esac - # append variables to config file - { - echo "SPOTinstall=\"$SPOTinstall\""; - echo "SPOTIuser=\"$SPOTIuser\""; - echo "SPOTIpass=\"$SPOTIpass\""; - echo "SPOTIclientid=\"$SPOTIclientid\""; - echo "SPOTIclientsecret=\"$SPOTIclientsecret\"" - } >> "${HOME_DIR}/PhonieboxInstall.conf" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -config_mpd() { - ##################################################### - # Configure MPD - - clear - - echo "##################################################### -# -# CONFIGURE MPD -# -# MPD (Music Player Daemon) runs the audio output and must -# be configured. Do it now, if you are unsure. -# (Note: can be done manually later.) -" - read -rp "Do you want to configure MPD? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - MPDconfig=NO - echo "You want to configure MPD later." - ;; - *) - MPDconfig=YES - echo "MPD will be set up with default values." - ;; - esac - # append variables to config file - echo "MPDconfig=\"$MPDconfig\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -config_audio_folder() { - local jukebox_dir="$1" - - ##################################################### - # Folder path for audio files - # default: /home/pi/RPi-Jukebox-RFID/shared/audiofolders - - clear - - echo "##################################################### -# -# FOLDER CONTAINING AUDIO FILES -# -# The default location for folders containing audio files: -# ${jukebox_dir}/shared/audiofolders -# -# If unsure, keep it like this. If your files are somewhere -# else, you can specify the folder in the next step. -# IMPORTANT: the folder will not be created, only the path -# will be remembered. If you use a custom folder, you must -# create it. -" - - read -rp "Do you want to use the default location? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - echo "Please type the absolute path here (no trailing slash)." - echo "Default would be for example: ${jukebox_dir}/shared/audiofolders" - read -r DIRaudioFolders - ;; - *) - DIRaudioFolders="${jukebox_dir}/shared/audiofolders" - ;; - esac - # append variables to config file - echo "DIRaudioFolders=\"$DIRaudioFolders\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - echo "Your audio folders live in this dir:" - echo "${DIRaudioFolders}" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -check_variable() { - local variable=${1} - # check if variable exist and if it's empty - test -z "${!variable+x}" && echo "ERROR: \$${variable} is missing!" && fail=true && return - test "${!variable}" == "" && echo "ERROR: \$${variable} is empty!" && fail=true -} - -config_gpio() { - ##################################################### - # Configure GPIO - - clear - - echo "##################################################### -# -# ACTIVATE GPIO-Control -# -# Activation of the GPIO-Control-Service, which mangages Buttons -# or a Rotary Encoder for Volume and/or Track control. -# To configure the controls please consult the wiki: -# https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons -# It's also possible to activate the service later (see wiki). -" - read -rp "Do you want to activate the GPIO-Control-Service? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - GPIOconfig=NO - echo "You don't want to activate GPIO-Controls now." - ;; - *) - GPIOconfig=YES - echo "GPIO-Control-Service will be activated and set to default values." - ;; - esac - # append variables to config file - echo "GPIOconfig=\"$GPIOconfig\"" >> "${HOME_DIR}/PhonieboxInstall.conf" - echo "" - read -rp "Hit ENTER to proceed to the next step." INPUT -} - -check_config_file() { - local install_conf="${HOME_DIR}/PhonieboxInstall.conf" - echo "Checking PhonieboxInstall.conf..." - # check that PhonieboxInstall.conf exists and is not empty - - # check if config file exists - if [[ -f "${install_conf}" ]]; then - # Source config file - source "${install_conf}" - cat "${install_conf}" - echo "" - else - echo "ERROR: ${install_conf} does not exist!" - exit 1 - fi - - fail=false - if [[ -z "${WIFIconfig+x}" ]]; then - echo "ERROR: \$WIFIconfig is missing or not set!" && fail=true - else - if [[ "$WIFIconfig" == "YES" ]]; then - check_variable "WIFIcountryCode" - check_variable "WIFIssid" - check_variable "WIFIpass" - check_variable "WIFIip" - check_variable "WIFIipRouter" - fi - fi - check_variable "EXISTINGuse" - check_variable "AUDIOiFace" - - if [[ -z "${SPOTinstall+x}" ]]; then - echo "ERROR: \$SPOTinstall is missing or not set!" && fail=true - else - if [ "$SPOTinstall" == "YES" ]; then - check_variable "SPOTIuser" - check_variable "SPOTIpass" - check_variable "SPOTIclientid" - check_variable "SPOTIclientsecret" - fi - fi - check_variable "MPDconfig" - check_variable "DIRaudioFolders" - check_variable "GPIOconfig" - - if [ "${fail}" == "true" ]; then - exit 1 - fi - - echo "" -} - -samba_config() { - local smb_conf="/etc/samba/smb.conf" - echo "Configuring Samba..." - # Samba configuration settings - # -rw-r--r-- 1 root root 9416 Apr 30 09:02 /etc/samba/smb.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/smb.conf.buster-default.sample ${smb_conf} - sudo chown root:root "${smb_conf}" - sudo chmod 644 "${smb_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${smb_conf}" - # Samba: create user 'pi' with password 'raspberry' - (echo "raspberry"; echo "raspberry") | sudo smbpasswd -s -a pi -} - -web_server_config() { - local lighthttpd_conf="/etc/lighttpd/lighttpd.conf" - local fastcgi_php_conf="/etc/lighttpd/conf-available/15-fastcgi-php.conf" - local php_ini="/etc/php/7.3/cgi/php.ini" - local sudoers="/etc/sudoers" - - echo "Configuring web server..." - # Web server configuration settings - # -rw-r--r-- 1 root root 1040 Apr 30 09:19 /etc/lighttpd/lighttpd.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/lighttpd.conf.buster-default.sample "${lighthttpd_conf}" - sudo chown root:root "${lighthttpd_conf}" - sudo chmod 644 "${lighthttpd_conf}" - - # Web server PHP7 fastcgi conf - # -rw-r--r-- 1 root root 398 Apr 30 09:35 /etc/lighttpd/conf-available/15-fastcgi-php.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/15-fastcgi-php.conf.buster-default.sample ${fastcgi_php_conf} - sudo chown root:root "${fastcgi_php_conf}" - sudo chmod 644 "${fastcgi_php_conf}" - - # settings for php.ini to support upload - # -rw-r--r-- 1 root root 70999 Jun 14 13:50 /etc/php/7.3/cgi/php.ini - sudo cp "${jukebox_dir}"/misc/sampleconfigs/php.ini.buster-default.sample ${php_ini} - sudo chown root:root "${php_ini}" - sudo chmod 644 "${php_ini}" - - # SUDO users (adding web server here) - # -r--r----- 1 root root 703 Nov 17 21:08 /etc/sudoers - sudo cp "${jukebox_dir}"/misc/sampleconfigs/sudoers.buster-default.sample ${sudoers} - sudo chown root:root "${sudoers}" - sudo chmod 440 "${sudoers}" -} - -install_main() { - local jukebox_dir="$1" - local apt_get="sudo apt-get -qq --yes" - local allow_downgrades="--allow-downgrades --allow-remove-essential --allow-change-held-packages" - - clear - - echo "##################################################### -# -# START INSTALLATION -# -# Good news: you completed the input. -# Let the install begin. -# -# Get yourself a cup of something. The install takes -# between 15 minutes to half an hour, depending on -# your Raspberry Pi and Internet connectivity. -# -# You will be prompted later to complete the installation. -" - - if [[ ${INTERACTIVE} == "true" ]]; then - read -rp "Do you want to start the installation? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - echo "Exiting the installation." - echo "Your configuration data was saved in this file:" - echo "${HOME_DIR}/PhonieboxInstall.conf" - echo - exit - ;; - esac - fi - - # Start logging here - log_open - - # Add conffile into logfile for better debugging - echo "################################################" - grep -v -e "SPOTI" -e "WIFIpass" "${HOME_DIR}/PhonieboxInstall.conf" - echo "################################################" - - ##################################################### - # INSTALLATION - - # Read install config as written so far - # (this might look stupid so far, but makes sense once - # the option to install from config file is introduced.) - # shellcheck source=scripts/installscripts/tests/ShellCheck/PhonieboxInstall.conf - . "${HOME_DIR}/PhonieboxInstall.conf" - - # power management of wifi: switch off to avoid disconnecting - sudo iwconfig wlan0 power off - - # create backup of /etc/resolv.conf - sudo cp /etc/resolv.conf /etc/resolv.conf.orig - - # Generate locales - sudo locale-gen "${LANG}" - - # Install required packages - ${apt_get} ${allow_downgrades} install apt-transport-https - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - - sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list - - ${apt_get} update - ${apt_get} upgrade - ${apt_get} install libspotify-dev - - # some packages are only available on raspberry pi's but not on test docker containers running on x86_64 machines - if [[ $(uname -m) =~ ^armv.+$ ]]; then - ${apt_get} ${allow_downgrades} install raspberrypi-kernel-headers - fi - - ${apt_get} ${allow_downgrades} install samba samba-common-bin gcc lighttpd php7.3-common php7.3-cgi php7.3 at mpd mpc mpg123 git ffmpeg resolvconf spi-tools netcat alsa-tools - - # restore backup of /etc/resolv.conf in case installation of resolvconf cleared it - sudo cp /etc/resolv.conf.orig /etc/resolv.conf - - # prepare python3 - ${apt_get} ${allow_downgrades} install python3 python3-dev python3-pip python3-mutagen python3-gpiozero python3-spidev - - # use python3.7 as default - sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1 - - # Get github code - cd "${HOME_DIR}" || exit - git clone ${GIT_URL} --branch "${GIT_BRANCH}" - - # VERSION of installation - - # Get version number - VERSION_NO=`cat ${jukebox_dir}/settings/version-number` - - # add used git branch and commit hash to version file - USED_BRANCH="$(git --git-dir=${jukebox_dir}/.git rev-parse --abbrev-ref HEAD)" - - # add git commit hash to version file - COMMIT_NO="$(git --git-dir=${jukebox_dir}/.git describe --always)" - - echo "${VERSION_NO} - ${COMMIT_NO} - ${USED_BRANCH}" > ${jukebox_dir}/settings/version - chmod 777 ${jukebox_dir}/settings/version - - # Install required spotify packages - if [ "${SPOTinstall}" == "YES" ]; then - echo "Installing dependencies for Spotify support..." - # keep major verson 3 of mopidy - echo -e "Package: mopidy\nPin: version 3.*\nPin-Priority: 1001" | sudo tee /etc/apt/preferences.d/mopidy - - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - - sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list - ${apt_get} update - ${apt_get} upgrade - ${apt_get} ${allow_downgrades} install mopidy mopidy-mpd mopidy-local mopidy-spotify - ${apt_get} ${allow_downgrades} install libspotify12 python3-cffi python3-ply python3-pycparser python3-spotify - - # Install necessary Python packages - sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements-spotify.txt - fi - - # Install more required packages - echo "Installing additional Python packages..." - sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements.txt - - samba_config - - web_server_config - - # copy shell script for player - cp "${jukebox_dir}"/settings/rfid_trigger_play.conf.sample "${jukebox_dir}"/settings/rfid_trigger_play.conf - - # creating files containing editable values for configuration - echo "$AUDIOiFace" > "${jukebox_dir}"/settings/Audio_iFace_Name - echo "$DIRaudioFolders" > "${jukebox_dir}"/settings/Audio_Folders_Path - echo "3" > "${jukebox_dir}"/settings/Audio_Volume_Change_Step - echo "100" > "${jukebox_dir}"/settings/Max_Volume_Limit - echo "0" > "${jukebox_dir}"/settings/Idle_Time_Before_Shutdown - echo "RESTART" > "${jukebox_dir}"/settings/Second_Swipe - echo "${jukebox_dir}/playlists" > "${jukebox_dir}"/settings/Playlists_Folders_Path - echo "ON" > "${jukebox_dir}"/settings/ShowCover - - # sample file for debugging with all options set to FALSE - sudo cp "${jukebox_dir}"/settings/debugLogging.conf.sample "${jukebox_dir}"/settings/debugLogging.conf - sudo chmod 777 "${jukebox_dir}"/settings/debugLogging.conf - - # The new way of making the bash daemon is using the helperscripts - # creating the shortcuts and script from a CSV file. - # see scripts/helperscripts/AssignIDs4Shortcuts.php - - # create config file for web app from sample - sudo cp "${jukebox_dir}"/htdocs/config.php.sample "${jukebox_dir}"/htdocs/config.php - - # Starting web server and php7 - sudo lighttpd-enable-mod fastcgi - sudo lighttpd-enable-mod fastcgi-php - sudo service lighttpd force-reload - - # make sure bash scripts have the right settings - sudo chown pi:www-data "${jukebox_dir}"/scripts/*.sh - sudo chmod +x "${jukebox_dir}"/scripts/*.sh - sudo chown pi:www-data "${jukebox_dir}"/scripts/*.py - sudo chmod +x "${jukebox_dir}"/scripts/*.py - - # services to launch after boot using systemd - # -rw-r--r-- 1 root root 304 Apr 30 10:07 phoniebox-rfid-reader.service - # 1. delete old services (this is legacy, might throw errors but is necessary. Valid for versions < 1.1.8-beta) - local systemd_dir="/etc/systemd/system" - echo "### Deleting older versions of service daemons. This might throw errors, ignore them" - sudo systemctl disable idle-watchdog - sudo systemctl disable rfid-reader - sudo systemctl disable phoniebox-startup-sound - sudo systemctl disable gpio-buttons - sudo systemctl disable phoniebox-rotary-encoder - sudo systemctl disable phoniebox-gpio-buttons.service - sudo rm "${systemd_dir}"/rfid-reader.service - sudo rm "${systemd_dir}"/startup-sound.service - sudo rm "${systemd_dir}"/gpio-buttons.service - sudo rm "${systemd_dir}"/idle-watchdog.service - sudo rm "${systemd_dir}"/phoniebox-rotary-encoder.service - sudo rm "${systemd_dir}"/phoniebox-gpio-buttons.service - echo "### Done with erasing old daemons. Stop ignoring errors!" - # 2. install new ones - this is version > 1.1.8-beta - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample "${systemd_dir}"/phoniebox-rfid-reader.service - #startup sound now part of phoniebox-startup-scripts - #sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-sound.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-scripts.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample "${systemd_dir}"/phoniebox-idle-watchdog.service - [[ "${GPIOconfig}" == "YES" ]] && sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-control.service.sample "${systemd_dir}"/phoniebox-gpio-control.service - sudo chown root:root "${systemd_dir}"/phoniebox-*.service - sudo chmod 644 "${systemd_dir}"/phoniebox-*.service - # enable the services needed - sudo systemctl enable phoniebox-idle-watchdog - sudo systemctl enable phoniebox-rfid-reader - #startup sound is part of phoniebox-startup-scripts now - #sudo systemctl enable phoniebox-startup-sound - sudo systemctl enable phoniebox-startup-scripts - # copy mp3s for startup and shutdown sound to the right folder - cp "${jukebox_dir}"/misc/sampleconfigs/startupsound.mp3.sample "${jukebox_dir}"/shared/startupsound.mp3 - cp "${jukebox_dir}"/misc/sampleconfigs/shutdownsound.mp3.sample "${jukebox_dir}"/shared/shutdownsound.mp3 - - # Spotify config - if [ "${SPOTinstall}" == "YES" ]; then - local etc_mopidy_conf="/etc/mopidy/mopidy.conf" - local mopidy_conf="${HOME_DIR}/.config/mopidy/mopidy.conf" - echo "Configuring Spotify support..." - sudo systemctl disable mpd - sudo systemctl enable mopidy - # Install Config Files - sudo cp "${jukebox_dir}"/misc/sampleconfigs/locale.gen.sample /etc/locale.gen - sudo cp "${jukebox_dir}"/misc/sampleconfigs/locale.sample /etc/default/locale - sudo locale-gen - mkdir -p "${HOME_DIR}"/.config/mopidy - sudo cp "${jukebox_dir}"/misc/sampleconfigs/mopidy-etc.sample "${etc_mopidy_conf}" - cp "${jukebox_dir}"/misc/sampleconfigs/mopidy.sample "${mopidy_conf}" - # Change vars to match install config - sudo sed -i 's/%spotify_username%/'"$SPOTIuser"'/' "${etc_mopidy_conf}" - sudo sed -i 's/%spotify_password%/'"$SPOTIpass"'/' "${etc_mopidy_conf}" - sudo sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' "${etc_mopidy_conf}" - sudo sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${etc_mopidy_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${etc_mopidy_conf}" - sed -i 's/%spotify_username%/'"$SPOTIuser"'/' "${mopidy_conf}" - sed -i 's/%spotify_password%/'"$SPOTIpass"'/' "${mopidy_conf}" - sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' "${mopidy_conf}" - sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${mopidy_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mopidy_conf}" - fi - - # GPIO-Control - if [[ "${GPIOconfig}" == "YES" ]]; then - sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements-GPIO.txt - sudo systemctl enable phoniebox-gpio-control.service - if [[ ! -f "${jukebox_dir}"/settings/gpio_settings.ini ]]; then - cp "${jukebox_dir}"/misc/sampleconfigs/gpio_settings.ini.sample "${jukebox_dir}"/settings/gpio_settings.ini - fi - fi - - if [ "${MPDconfig}" == "YES" ]; then - local mpd_conf="/etc/mpd.conf" - - echo "Configuring MPD..." - # MPD configuration - # -rw-r----- 1 mpd audio 14043 Jul 17 20:16 /etc/mpd.conf - sudo cp "${jukebox_dir}"/misc/sampleconfigs/mpd.conf.buster-default.sample ${mpd_conf} - # Change vars to match install config - sudo sed -i 's/%AUDIOiFace%/'"$AUDIOiFace"'/' "${mpd_conf}" - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mpd_conf}" - sudo chown mpd:audio "${mpd_conf}" - sudo chmod 640 "${mpd_conf}" - fi - - # set which version has been installed - if [ "${SPOTinstall}" == "YES" ]; then - echo "plusSpotify" > "${jukebox_dir}"/settings/edition - else - echo "classic" > "${jukebox_dir}"/settings/edition - fi - - # update mpc / mpd DB - mpc update - - # / INSTALLATION - ##################################################### -} - -wifi_settings() { - local sample_configs_dir="$1" - local dhcpcd_conf="$2" - local wpa_supplicant_conf="$3" - - ############################### - # WiFi settings (SSID password) - # - # https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md - # - # $WIFIssid - # $WIFIpass - # $WIFIip - # $WIFIipRouter - if [ "${WIFIconfig}" == "YES" ]; then - # DHCP configuration settings - echo "Setting ${dhcpcd_conf}..." - #-rw-rw-r-- 1 root netdev 0 Apr 17 11:25 /etc/dhcpcd.conf - sudo cp "${sample_configs_dir}"/dhcpcd.conf.buster-default-noHotspot.sample "${dhcpcd_conf}" - # Change IP for router and Phoniebox - sudo sed -i 's/%WIFIip%/'"$WIFIip"'/' "${dhcpcd_conf}" - sudo sed -i 's/%WIFIipRouter%/'"$WIFIipRouter"'/' "${dhcpcd_conf}" - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' "${dhcpcd_conf}" - # Change user:group and access mod - sudo chown root:netdev "${dhcpcd_conf}" - sudo chmod 664 "${dhcpcd_conf}" - - # WiFi SSID & Password - echo "Setting ${wpa_supplicant_conf}..." - # -rw-rw-r-- 1 root netdev 137 Jul 16 08:53 /etc/wpa_supplicant/wpa_supplicant.conf - sudo cp "${sample_configs_dir}"/wpa_supplicant.conf.buster-default.sample "${wpa_supplicant_conf}" - sudo sed -i 's/%WIFIssid%/'"$WIFIssid"'/' "${wpa_supplicant_conf}" - sudo sed -i 's/%WIFIpass%/'"$WIFIpass"'/' "${wpa_supplicant_conf}" - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' "${wpa_supplicant_conf}" - sudo chown root:netdev "${wpa_supplicant_conf}" - sudo chmod 664 "${wpa_supplicant_conf}" - fi - - # start DHCP - echo "Starting dhcpcd service..." - sudo service dhcpcd start - sudo systemctl enable dhcpcd - -# / WiFi settings (SSID password) -############################### -} - -existing_assets() { - local jukebox_dir="$1" - local backup_dir="$2" - - ##################################################### - # EXISTING ASSETS TO USE FROM EXISTING INSTALL - - if [ "${EXISTINGuse}" == "YES" ]; then - # RFID config for system control - if [ "${EXISTINGuseRfidConf}" == "YES" ]; then - # read old values and write them into new file (copied above already) - # do not overwrite but use 'sed' in case there are new vars in new version installed - - # Read the existing RFID config file line by line and use - # only lines which are separated (IFS) by '='. - while IFS='=' read -r key val ; do - # $var should be stripped of possible leading or trailing " - val=${val%\"} - val=${val#\"} - key=${key} - # Additional error check: key should not start with a hash and not be empty. - if [ ! "${key:0:1}" == '#' ] && [ -n "$key" ]; then - # Replace the matching value in the newly created conf file - sed -i 's/%'"$key"'%/'"$val"'/' "${jukebox_dir}"/settings/rfid_trigger_play.conf - fi - done <"${backup_dir}"/settings/rfid_trigger_play.conf - fi - - # RFID shortcuts for audio folders - if [ "${EXISTINGuseRfidLinks}" == "YES" ]; then - # copy from backup to new install - cp -R "${backup_dir}"/shared/shortcuts/* "${jukebox_dir}"/shared/shortcuts/ - fi - - # Audio folders: use existing - if [ "${EXISTINGuseAudio}" == "YES" ]; then - # copy from backup to new install - cp -R "${backup_dir}"/shared/audiofolders/* "$DIRaudioFolders/" - fi - - # GPIO: use existing file - if [ "${EXISTINGuseGpio}" == "YES" ]; then - # copy from backup to new install - cp "${backup_dir}"/settings/gpio_settings.ini "${jukebox_dir}"/settings/gpio_settings.ini - fi - - # Button USB Encoder: use existing file - if [ "${EXISTINGuseButtonUSBEncoder}" == "YES" ]; then - # copy from backup to new install - cp "${backup_dir}"/components/controls/buttons_usb_encoder/deviceName.txt "${jukebox_dir}"/components/controls/buttons_usb_encoder/deviceName.txt - cp "${backup_dir}"/components/controls/buttons_usb_encoder/buttonMap.json "${jukebox_dir}"/components/controls/buttons_usb_encoder/buttonMap.json - # make buttons_usb_encoder.py ready to be use from phoniebox-buttons-usb-encoder service - sudo chmod +x "${jukebox_dir}"/components/controls/buttons_usb_encoder/buttons_usb_encoder.py - # make sure service is still enabled by registering again - sudo cp -v "${jukebox_dir}"/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample /etc/systemd/system/phoniebox-buttons-usb-encoder.service - sudo systemctl start phoniebox-buttons-usb-encoder.service - sudo systemctl enable phoniebox-buttons-usb-encoder.service - fi - - # Sound effects: use existing startup / shutdown sounds - if [ "${EXISTINGuseSounds}" == "YES" ]; then - # copy from backup to new install - cp "${backup_dir}"/shared/startupsound.mp3 "${jukebox_dir}"/shared/startupsound.mp3 - cp "${backup_dir}"/shared/shutdownsound.mp3 "${jukebox_dir}"/shared/shutdownsound.mp3 - fi - - fi - - # / EXISTING ASSETS TO USE FROM EXISTING INSTALL - ################################################ -} - - -folder_access() { - local jukebox_dir="$1" - local user_group="$2" - local mod="$3" - - ##################################################### - # Folders and Access Settings - - echo "Setting owner and permissions for directories..." - - # create playlists folder - mkdir -p "${jukebox_dir}"/playlists - sudo chown -R "${user_group}" "${jukebox_dir}"/playlists - sudo chmod -R "${mod}" "${jukebox_dir}"/playlists - - # make sure the shared folder is accessible by the web server - sudo chown -R "${user_group}" "${jukebox_dir}"/shared - sudo chmod -R "${mod}" "${jukebox_dir}"/shared - - # make sure the htdocs folder can be changed by the web server - sudo chown -R "${user_group}" "${jukebox_dir}"/htdocs - sudo chmod -R "${mod}" "${jukebox_dir}"/htdocs - - sudo chown -R "${user_group}" "${jukebox_dir}"/settings - sudo chmod -R "${mod}" "${jukebox_dir}"/settings - - # logs dir accessible by pi and www-data - sudo chown "${user_group}" "${jukebox_dir}"/logs - sudo chmod "${mod}" "${jukebox_dir}"/logs - - # audio folders might be somewhere else, so treat them separately - sudo chown "${user_group}" "${DIRaudioFolders}" - sudo chmod "${mod}" "${DIRaudioFolders}" - - # make sure bash scripts have the right settings - sudo chown "${user_group}" "${jukebox_dir}"/scripts/*.sh - sudo chmod +x "${jukebox_dir}"/scripts/*.sh - sudo chown "${user_group}" "${jukebox_dir}"/scripts/*.py - sudo chmod +x "${jukebox_dir}"/scripts/*.py - - # set audio volume to 100% - # see: https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/54 - sudo amixer cset numid=1 100% - - # delete the global.conf file, in case somebody manually copied stuff back and forth - # this will be created the first time the Phoniebox is put to use by web app or RFID - GLOBAL_CONF="${jukebox_dir}"/settings/global.conf - if [ -f "${GLOBAL_CONF}" ]; then - echo "global.conf needs to be deleted." - rm "${GLOBAL_CONF}" - fi - - # / Access settings - ##################################################### -} - -finish_installation() { - local jukebox_dir="$1" - echo " -# -# INSTALLATION FINISHED -# -##################################################### -" - - ##################################################### - # Register external device(s) - - echo "If you are using an RFID reader, connect it to your RPi." - echo "(In case your RFID reader required soldering, consult the manual.)" - # Use -e to display response of user in the logfile - read -e -r -p "Have you connected your RFID reader? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - ;; - *) - echo 'Please select the RFID reader you want to use' - options=("USB-Reader (e.g. Neuftech)" "RC522" "PN532" "Manual configuration" "Multiple RFID reader") - select opt in "${options[@]}"; do - case $opt in - "USB-Reader (e.g. Neuftech)") - cd "${jukebox_dir}"/scripts/ || exit - python3 RegisterDevice.py - sudo chown pi:www-data "${jukebox_dir}"/scripts/deviceName.txt - sudo chmod 644 "${jukebox_dir}"/scripts/deviceName.txt - break - ;; - "RC522") - bash "${jukebox_dir}"/components/rfid-reader/RC522/setup_rc522.sh - break - ;; - "PN532") - bash "${jukebox_dir}"/components/rfid-reader/PN532/setup_pn532.sh - break - ;; - "Manual configuration") - echo "Please configure your reader manually." - break - ;; - "Multiple RFID reader") - cd "${jukebox_dir}"/scripts/ || exit - sudo python3 RegisterDevice.py.Multi - break - ;; - *) - echo "This is not a number" - ;; - esac - done - esac - - echo - echo "DONE. Let the sounds begin." - echo "Find more information and documentation on the github account:" - echo "https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/" - - echo "Reboot is needed to activate all settings" - # Use -e to display response of user in the logfile - read -e -r -p "Would you like to reboot now? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - # Close logging - log_close - ;; - *) - # Close logging - log_close - sudo shutdown -r now - ;; - esac -} - -######## -# Main # -######## -main() { - # Skip interactive Samba WINS config dialog - echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections - - if [[ ${INTERACTIVE} == "true" ]]; then - welcome - #reset_install_config_file - config_wifi - check_existing "${JUKEBOX_HOME_DIR}" "${JUKEBOX_BACKUP_DIR}" "${HOME_DIR}" - config_audio_interface - config_spotify - config_mpd - config_audio_folder "${JUKEBOX_HOME_DIR}" - config_gpio - else - echo "Non-interactive installation!" - check_config_file - fi - install_main "${JUKEBOX_HOME_DIR}" - wifi_settings "${JUKEBOX_HOME_DIR}/misc/sampleconfigs" "/etc/dhcpcd.conf" "/etc/wpa_supplicant/wpa_supplicant.conf" - existing_assets "${JUKEBOX_HOME_DIR}" "${JUKEBOX_BACKUP_DIR}" - folder_access "${JUKEBOX_HOME_DIR}" "pi:www-data" 775 - - # Copy PhonieboxInstall.conf configuration file to settings folder - sudo cp "${HOME_DIR}/PhonieboxInstall.conf" "${JUKEBOX_HOME_DIR}/settings/" - sudo chown pi:www-data "${JUKEBOX_HOME_DIR}/settings/PhonieboxInstall.conf" - sudo chmod 775 "${JUKEBOX_HOME_DIR}/settings/PhonieboxInstall.conf" - - if [[ ${INTERACTIVE} == "true" ]]; then - finish_installation "${JUKEBOX_HOME_DIR}" - else - echo "Skipping USB device setup..." - echo "For manual registration of a USB card reader type:" - echo "python3 /home/pi/RPi-Jukebox-RFID/scripts/RegisterDevice.py" - echo " " - echo "Reboot is required to activate all settings!" - fi -} - -start=$(date +%s) - -main - -end=$(date +%s) -runtime=$((end-start)) -((h=${runtime}/3600)) -((m=(${runtime}%3600)/60)) -((s=${runtime}%60)) -echo "Done (in ${h}h ${m}m ${s}s)." - -##################################################### -# notes for things to do - -# CLEANUP -## remove dir BACKUP (possibly not, because we do this at the beginning after user confirms for latest config) -##################################################### diff --git a/scripts/installscripts/stretch-install-default-HotspotAddOn.sh b/scripts/installscripts/stretch-install-default-HotspotAddOn.sh deleted file mode 100644 index a7f9aa276..000000000 --- a/scripts/installscripts/stretch-install-default-HotspotAddOn.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -# DO NOT USE UNTIL THIS LINE HAS DISAPPEARED -# -# This script turns the Phoniebox into a Hotspot -# IF there is no other WiFi network to connect to. -# Needs to run on top of a finished install. And might -# not work if you are not using a Pi3 or ZERO with Wifi. -# See for more information: -# http://www.raspberryconnect.com/network/item/331-raspberry-pi-auto-wifi-hotspot-switch-no-internet-routing -# -# Belongs to: https://github.com/MiczFlor/RPi-Jukebox-RFID - -##################################################### -# Ask if access point - -clear - -echo "##################################################### -# -# CONFIGURE ACCESS POINT / HOTSPOT -# -# If you take your Phoniebox on the road and it is not -# connected to a WiFi network, it can automatically turn -# into an access point and show up as SSID 'phoniebox'. -# This will work for RPi3 out of the box. It might not -# work for other models and WiFi cards. -# (Note: can be done manually later, if you are unsure.) -" -read -r -p "Do you want to configure as Access Point? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - ACCESSconfig=NO - echo "You don't want to configure as an Access Point." - echo "Hit ENTER to proceed to the next step." - read -r - ;; - *) - ACCESSconfig=YES - ;; -esac -# append variables to config file -echo "ACCESSconfig=\"$ACCESSconfig\"" >> "${PATHDATA}/PhonieboxInstall.conf" - - -######################## -# Access Point / Hotspot -# http://www.raspberryconnect.com/network/item/331-raspberry-pi-auto-wifi-hotspot-switch-no-internet-routing -if [ $ACCESSconfig == "IGNOREFORNOWYES" ] -then - -# -# NOT IMPLEMENTED YET -# - # Work in progress, so keep in mind: BACKUP conf files for ACCESS POINT - # cp /etc/hostapd/hostapd.conf hostapd.conf.stretch.sample - # cp /etc/default/hostapd hostapd.stretch.sample - # cp /etc/dnsmasq.conf dnsmasq.conf.stretch.sample - # cp /etc/network/interfaces interfaces.stretch.sample - # WIFIcountryCode - # https://www.cisco.com/en/US/products/ps6305/products_configuration_guide_chapter09186a00804ddd8a.html - - # Remove dns-root-data - sudo apt-get purge dns-root-data - # Install packages - sudo apt-get install hostapd dnsmasq iw - # enter Y when prompted - - # The installers will have set up the programme so they run when the pi is started. - # For this setup they only need to be started if the home router is not found. - # So automatic startup needs to be disabled. This is done with the following commands: - sudo systemctl disable hostapd - sudo systemctl disable dnsmasq - - # -rw-r--r-- 1 root root 345 Aug 13 17:55 /etc/hostapd/hostapd.conf - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/hostapd.conf.stretch-default2-Hotspot.sample /etc/hostapd/hostapd.conf - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' /etc/hostapd/hostapd.conf - sudo chmod 644 /etc/hostapd/hostapd.conf - sudo chown root:root /etc/hostapd/hostapd.conf - - # -rw-r--r-- 1 root root 794 Aug 13 18:40 /etc/default/hostapd - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/hostapd.stretch-default2-Hotspot.sample /etc/default/hostapd - sudo chmod 644 /etc/default/hostapd - sudo chown root:root /etc/default/hostapd - - # -rw-r--r-- 1 root root 82 Jul 17 15:13 /etc/network/interfaces - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/interfaces.stretch-default2-Hotspot.sample /etc/network/interfaces - sudo chmod 644 /etc/network/interfaces - sudo chown root:root /etc/network/interfaces - - # DHCP configuration settings - #-rw-rw-r-- 1 root netdev 0 Apr 17 11:25 /etc/dhcpcd.conf - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/dhcpcd.conf.stretch-default2-Hotspot.sample /etc/dhcpcd.conf - # Change IP for router and Phoniebox - sudo sed -i 's/%WIFIip%/'"$WIFIip"'/' /etc/dhcpcd.conf - sudo sed -i 's/%WIFIipRouter%/'"$WIFIipRouter"'/' /etc/dhcpcd.conf - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' /etc/dhcpcd.conf - # Change user:group and access mod - sudo chown root:netdev /etc/dhcpcd.conf - sudo chmod 664 /etc/dhcpcd.conf - - # /usr/bin/autohotspot - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/autohotspot.stretch-default2-Hotspot.sample /usr/bin/autohotspot - sudo chown root:root /usr/bin/autohotspot - sudo chmod 644 /usr/bin/autohotspot - sudo chmod +x /usr/bin/autohotspot - - # /etc/systemd/system/autohotspot.service - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/autohotspot.service.stretch-default2-Hotspot.sample /etc/systemd/system/autohotspot.service - sudo chown root:root /etc/systemd/system/autohotspot.service - sudo chmod 644 /etc/systemd/system/autohotspot.service - sudo systemctl enable autohotspot.service - - echo " - ######################## - # Hotspot (Access Point) - NOTE: - The network 'phoniebox' appears only when away from your usual WiFi. - You can connect from any device with the password 'PlayItLoud'. - In your browser, open the IP '10.0.0.10' to access the web app. - " -fi - -# / Access Point -################ \ No newline at end of file diff --git a/scripts/installscripts/stretch-install-default.sh b/scripts/installscripts/stretch-install-default.sh deleted file mode 100755 index 49843d7cf..000000000 --- a/scripts/installscripts/stretch-install-default.sh +++ /dev/null @@ -1,894 +0,0 @@ -#!/bin/bash -# -# see https://github.com/MiczFlor/RPi-Jukebox-RFID for details -# Especially the docs folder for documentation - -# The absolute path to the folder which contains this script -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -GIT_BRANCH=${GIT_BRANCH:-master} - -clear -echo "##################################################### -# ___ __ ______ _ __________ ____ __ _ _ # -# / _ \/ // / __ \/ |/ / _/ __/( _ \ / \( \/ ) # -# / ___/ _ / /_/ / // // _/ ) _ (( O )) ( # -# /_/ /_//_/\____/_/|_/___/____/ (____/ \__/(_/\_) # -# # -##################################################### - -Welcome to the installation script. - -This script will install Phoniebox on your Raspberry Pi. -To do so, you must be online. The install script can -automatically configure: - -* WiFi settings (SSID, password and static IP) - -All these are optional and can also be done later -manually. - -If you are ready, hit ENTER" -read -r INPUT - -##################################################### -# CONFIG FILE -# This file will contain all the data given in the -# following dialogue -# At a later stage, the install should also be done -# from such a config file with no user input. - -# Remove existing config file -rm PhonieboxInstall.conf -# Create empty config file -touch PhonieboxInstall.conf -echo "# Phoniebox config" > "${PATHDATA}/PhonieboxInstall.conf" - -##################################################### -# Ask if wifi config - -clear - -echo "##################################################### -# -# CONFIGURE WIFI -# -# Requires SSID, WiFi password and the static IP you want -# to assign to your Phoniebox. -# (Note: can be done manually later, if you are unsure.) -" -read -r -p "Do you want to configure your WiFi? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - WIFIconfig=NO - echo "You want to configure WiFi later." - echo "Hit ENTER to proceed to the next step." - read -r INPUT - # append variables to config file - echo "WIFIconfig=$WIFIconfig" >> "${PATHDATA}/PhonieboxInstall.conf" - # make a fallback for WiFi Country Code, because we need that even without WiFi config - echo "WIFIcountryCode=DE" >> "${PATHDATA}/PhonieboxInstall.conf" - ;; - *) - WIFIconfig=YES - #Ask for ssid - echo "* Type SSID name" - read -r INPUT - WIFIssid="$INPUT" - #Ask for wifi country code - echo "* WiFi Country Code (e.g. DE, GB, CZ or US)" - read -r INPUT - WIFIcountryCode="$INPUT" - #Ask for password - echo "* Type password" - read -r INPUT - WIFIpass="$INPUT" - #Ask for IP - echo "* Static IP (e.g. 192.168.1.199)" - read -r INPUT - WIFIip="$INPUT" - #Ask for Router IP - echo "* Router IP (e.g. 192.168.1.1)" - read -r INPUT - WIFIipRouter="$INPUT" - echo "Your WiFi config:" - echo "SSID : $WIFIssid" - echo "WiFi Country Code : $WIFIcountryCode" - echo "Password : $WIFIpass" - echo "Static IP : $WIFIip" - echo "Router IP : $WIFIipRouter" - read -r -p "Are these values correct? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - echo "The values are incorrect." - echo "Hit ENTER to exit and start over." - read -r INPUT; exit - ;; - *) - # append variables to config file - echo "WIFIconfig=\"$WIFIconfig\"" >> "${PATHDATA}/PhonieboxInstall.conf" - echo "WIFIcountryCode=\"$WIFIcountryCode\"" >> "${PATHDATA}/PhonieboxInstall.conf" - echo "WIFIssid=\"$WIFIssid\"" >> "${PATHDATA}/PhonieboxInstall.conf" - echo "WIFIpass=\"$WIFIpass\"" >> "${PATHDATA}/PhonieboxInstall.conf" - echo "WIFIip=\"$WIFIip\"" >> "${PATHDATA}/PhonieboxInstall.conf" - echo "WIFIipRouter=\"$WIFIipRouter\"" >> "${PATHDATA}/PhonieboxInstall.conf" - ;; - esac - ;; -esac - -##################################################### -# Check for existing Phoniebox -# -# In case there is no existing install, -# set the var now for later use: -EXISTINGuse=NO - -# The install will be in the home dir of user pi -# Move to home directory now to check -cd || exit -if [ -d /home/pi/RPi-Jukebox-RFID ]; then - # Houston, we found something! - clear -echo "##################################################### -# -# . . . * alert * alert * alert * alert * . . . -# -# WARNING: an existing Phoniebox installation was found. -# -" - # check if we find the version number - if [ -f /home/pi/RPi-Jukebox-RFID/settings/version ]; then - echo "The version of your installation is: $(cat RPi-Jukebox-RFID/settings/version)" - fi - echo "IMPORTANT: you can use the existing content and configuration files for your new install." - echo "Whatever you chose to keep will be moved to the new install." - echo "Everything else will remain in a folder called 'BACKUP'. - " - # Delete or use existing installation? - read -r -p "Re-use config, audio and RFID codes for the new install? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuse=NO - echo "Phoniebox will be a fresh install. The existing version will be dropped." - echo "Hit ENTER to proceed to the next step." - sudo rm -rf RPi-Jukebox-RFID - read -r INPUT - ;; - *) - EXISTINGuse=YES - # CREATE BACKUP - # delete existing BACKUP dir if exists - if [ -d BACKUP ]; then - sudo rm -r BACKUP - fi - # move install to BACKUP dir - mv RPi-Jukebox-RFID BACKUP - # delete .git dir - if [ -d BACKUP/.git ]; then - sudo rm -r BACKUP/.git - fi - # delete placeholder files so moving the folder content back later will not create git pull conflicts - rm BACKUP/shared/audiofolders/placeholder - rm BACKUP/shared/shortcuts/placeholder - - # ask for things to use - echo "Ok. You want to use stuff from the existing installation." - echo "What would you want to keep? Answer now." - read -r -p "RFID config for system control (e.g. 'volume up' etc.)? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseRfidConf=NO - ;; - *) - EXISTINGuseRfidConf=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseRfidConf=$EXISTINGuseRfidConf" >> "${PATHDATA}/PhonieboxInstall.conf" - - read -r -p "RFID shortcuts to play audio folders? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseRfidLinks=NO - ;; - *) - EXISTINGuseRfidLinks=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseRfidLinks=$EXISTINGuseRfidLinks" >> "${PATHDATA}/PhonieboxInstall.conf" - - read -r -p "Audio folders: use existing? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseAudio=NO - ;; - *) - EXISTINGuseAudio=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseAudio=$EXISTINGuseAudio" >> "${PATHDATA}/PhonieboxInstall.conf" - - read -r -p "GPIO: use existing file? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseGpio=NO - ;; - *) - EXISTINGuseGpio=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseGpio=$EXISTINGuseGpio" >> "${PATHDATA}/PhonieboxInstall.conf" - - read -r -p "Sound effects: use existing startup / shutdown sounds? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseSounds=NO - ;; - *) - EXISTINGuseSounds=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseSounds=$EXISTINGuseSounds" >> "${PATHDATA}/PhonieboxInstall.conf" - - read -r -p "Button USB Encoder: use existing device and button mapping? [Y/n] " response - case "$response" in - [nN][oO]|[nN]) - EXISTINGuseButtonUSBEncoder=NO - ;; - *) - EXISTINGuseButtonUSBEncoder=YES - ;; - esac - # append variables to config file - echo "EXISTINGuseButtonUSBEncoder=$EXISTINGuseButtonUSBEncoder" >> "${PATHDATA}/PhonieboxInstall.conf" - - echo "Thanks. Got it." - echo "The existing install can be found in the BACKUP directory." - echo "Hit ENTER to proceed to the next step." - read -r INPUT - ;; - esac -fi -# append variables to config file -echo "EXISTINGuse=$EXISTINGuse" >> "${PATHDATA}/PhonieboxInstall.conf" - -##################################################### -# Audio iFace - -clear - -echo "##################################################### -# -# CONFIGURE AUDIO INTERFACE (iFace) -# -# By default for the RPi the audio interface would be 'PCM'. -# But this does not work for every setup, alternatives are -# 'Master' or 'Speaker'. Other external sound cards might -# use different interface names. -# To list all available iFace names, type 'amixer scontrols' -# in the terminal. -" -read -r -p "Use PCM as iFace? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - echo "Type the iFace name you want to use:" - read -r INPUT - AUDIOiFace="$INPUT" - ;; - *) - AUDIOiFace="PCM" - ;; -esac -# append variables to config file -echo "AUDIOiFace=\"$AUDIOiFace\"" >> "${PATHDATA}/PhonieboxInstall.conf" -echo "Your iFace is called'$AUDIOiFace'" -echo "Hit ENTER to proceed to the next step." -read -r INPUT - -##################################################### -# Configure spotify - -clear - -echo "##################################################### -# -# OPTIONAL: INCLUDE SPOTIFY SUPPORT -# -# Spotify uses Mopidy for audio output and must -# be configured. Do it now, or never. -# (Note: To add this later, you must re-install phoniebox) -" -read -r -p "Do you want to install Mopidy? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - SPOTinstall=NO - echo "You don't want spotify support." - echo "Hit ENTER to proceed to the next step." - read -r INPUT - ;; - *) - SPOTinstall=YES - clear - echo "This was a great decision! Mopidy will be set up." - echo "##################################################### -# -# CONFIGURE MOPIDY -# -# Requires spotify username, password, client_id and client_secret -# to get connection to Spotify. -# -# (Note: You need a device with browser to generate ID and SECRET) -# -# Please go to this website: -# https://www.mopidy.com/authenticate/ -# and follow the instructions. -# -# Your credential will appear on the site below the login button. -# Please note your client_id and client_secret! -# -" - echo "" - echo "Type your Spotify username:" - read -r INPUT - SPOTIuser="$INPUT" - echo "" - echo "Type your Spotify password:" - read -r INPUT - SPOTIpass="$INPUT" - echo "" - echo "Type your client_id:" - read -r INPUT - SPOTIclientid="$INPUT" - echo "" - echo "Type your client_secret:" - read -r INPUT - SPOTIclientsecret="$INPUT" - echo "" - echo "Hit ENTER to proceed to the next step." - read -r INPUT - ;; -esac -# append variables to config file -{ - echo "SPOTinstall=\"$SPOTinstall\""; - echo "SPOTIuser=\"$SPOTIuser\""; - echo "SPOTIpass=\"$SPOTIpass\""; - echo "SPOTIclientid=\"$SPOTIclientid\""; - echo "SPOTIclientsecret=\"$SPOTIclientsecret\"" -} >> "${PATHDATA}/PhonieboxInstall.conf" - -if [ $SPOTinstall == "NO" ]; then -##################################################### -# Configure MPD - -clear - -echo "##################################################### -# -# CONFIGURE MPD -# -# MPD (Music Player Daemon) runs the audio output and must -# be configured. Do it now, if you are unsure. -# (Note: can be done manually later.) -" -read -r -p "Do you want to configure MPD? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - MPDconfig=NO - echo "You want to configure MPD later." - echo "Hit ENTER to proceed to the next step." - read -r INPUT - ;; - *) - MPDconfig=YES - echo "MPD will be set up with default values." - echo "Hit ENTER to proceed to the next step." - read -r INPUT - ;; -esac -# append variables to config file -echo "MPDconfig=\"$MPDconfig\"" >> "${PATHDATA}/PhonieboxInstall.conf" -fi - -##################################################### -# Folder path for audio files -# default: /home/pi/RPi-Jukebox-RFID/shared/audiofolders - -clear - -echo "##################################################### -# -# FOLDER CONTAINING AUDIO FILES -# -# The default location for folders containing audio files: -# /home/pi/RPi-Jukebox-RFID/shared/audiofolders -# -# If unsure, keep it like this. If your files are somewhere -# else, you can specify the folder in the next step. -# IMPORTANT: the folder will not be created, only the path -# will be remembered. If you use a custom folder, you must -# create it. -" - -read -r -p "Do you want to use the default location? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - echo "Please type the absolute path here (no trailing slash)." - echo "Default would be for example:" - echo "/home/pi/RPi-Jukebox-RFID/shared/audiofolders" - read -r INPUT - DIRaudioFolders="$INPUT" - ;; - *) - DIRaudioFolders="/home/pi/RPi-Jukebox-RFID/shared/audiofolders" - ;; -esac -# append variables to config file -echo "DIRaudioFolders=\"$DIRaudioFolders\"" >> "${PATHDATA}/PhonieboxInstall.conf" -echo "Your audio folders live in this dir:" -echo $DIRaudioFolders -echo "Hit ENTER to proceed to the next step." -read -r INPUT - -clear - -echo "##################################################### -# -# START INSTALLATION -# -# Good news: you completed the input. -# Let the install begin. -# -# Get yourself a cup of something. The install takes -# between 15 minutes to half an hour, depending on -# your Raspberry Pi and Internet connectivity. -# -# You will be prompted later to complete the installation. -" - -read -r -p "Do you want to start the installation? [Y/n] " response -case "$response" in - [nN][oO]|[nN]) - echo "Exiting the installation." - echo "Your configuration data was saved in this file:" - echo "${PATHDATA}/PhonieboxInstall.conf" - echo - exit - ;; -esac - -##################################################### -# INSTALLATION - -# Read install config as written so far -# (this might look stupid so far, but makes sense once -# the option to install from config file is introduced.) -# shellcheck source=scripts/installscripts/tests/ShellCheck/PhonieboxInstall.conf -. "${PATHDATA}/PhonieboxInstall.conf" - -# power management of wifi: switch off to avoid disconnecting -sudo iwconfig wlan0 power off - -# Install required packages -sudo apt-get update -sudo apt-get --yes --force-yes install apt-transport-https samba samba-common-bin gcc linux-headers-4.9 lighttpd php7.0-common php7.0-cgi php7.0 at mpd mpc mpg123 git ffmpeg - -# prepare for python2 and python3 -sudo apt-get --yes --force-yes install python-dev python-pip python-mutagen python-gpiozero python-spidev -sudo apt-get --yes --force-yes install python3-dev python3-pip python3-mutagen python3-gpiozero python3-spidev - -# use python3.5 as default -sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.5 1 -# Install required spotify packages -if [ $SPOTinstall == "YES" ] -then - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - - sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/stretch.list - sudo apt-get update - sudo apt-get --yes --force-yes install mopidy - sudo apt-get --yes --force-yes install libspotify12 python-cffi python-ply python-pycparser python-spotify - sudo apt-get --yes --force-yes install libspotify12 python3-cffi python3-ply python3-pycparser - sudo python3 -m pip install spotify - sudo rm -rf /usr/lib/python2.7/dist-packages/mopidy_spotify* - sudo rm -rf /usr/lib/python2.7/dist-packages/Mopidy_Spotify-* - cd || exit - sudo rm -rf mopidy-spotify - git clone -b fix/web_api_playlists --single-branch https://github.com/princemaxwell/mopidy-spotify.git - cd mopidy-spotify || exit - sudo python setup.py install - cd || exit - # should be removed, if Mopidy-Iris can be installed normally - # pylast >= 3.0.0 removed the python2 support - sudo pip install pylast==2.4.0 - sudo pip install Mopidy-Iris -fi - -# Get github code -cd /home/pi/ || exit -git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git --branch "${GIT_BRANCH}" - -# check, which branch was cloned -git status - -# the following three lines are needed as long as this is not the master branch: -cd /home/pi/RPi-Jukebox-RFID || exit -git fetch - -# Install more required packages -sudo pip install -r requirements.txt -sudo pip3 install -r /home/pi/RPi-Jukebox-RFID/components/rfid-reader/PN532/requirements.txt - -# actually, for the time being most of the requirements are run here (again). -# the requirements.txt version seems to throw errors. Help if you can to fix this: - -sudo pip install "evdev == 0.7.0" -sudo pip install --upgrade youtube_dl -sudo pip install spidev -sudo pip install git+git://github.com/lthiery/SPI-Py.git#egg=spi-py -sudo pip install pyserial -sudo pip install RPi.GPIO -sudo pip install pi-rc522 - -sudo python3 -m pip install "evdev == 0.7.0" - -# Switch of WiFi power management -sudo iwconfig wlan0 power off - -# Samba configuration settings -# -rw-r--r-- 1 root root 9416 Apr 30 09:02 /etc/samba/smb.conf -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/smb.conf.stretch-default2.sample /etc/samba/smb.conf -sudo chown root:root /etc/samba/smb.conf -sudo chmod 644 /etc/samba/smb.conf -# for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash -sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' /etc/samba/smb.conf -# Samba: create user 'pi' with password 'raspberry' -(echo "raspberry"; echo "raspberry") | sudo smbpasswd -s -a pi - -# Web server configuration settings -# -rw-r--r-- 1 root root 1040 Apr 30 09:19 /etc/lighttpd/lighttpd.conf -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/lighttpd.conf.stretch-default.sample /etc/lighttpd/lighttpd.conf -sudo chown root:root /etc/lighttpd/lighttpd.conf -sudo chmod 644 /etc/lighttpd/lighttpd.conf - -# Web server PHP7 fastcgi conf -# -rw-r--r-- 1 root root 398 Apr 30 09:35 /etc/lighttpd/conf-available/15-fastcgi-php.conf -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/15-fastcgi-php.conf.stretch-default.sample /etc/lighttpd/conf-available/15-fastcgi-php.conf -sudo chown root:root /etc/lighttpd/conf-available/15-fastcgi-php.conf -sudo chmod 644 /etc/lighttpd/conf-available/15-fastcgi-php.conf -# settings for php.ini to support upload -# -rw-r--r-- 1 root root 70999 Jun 14 13:50 /etc/php/7.0/fpm/php.ini -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/php.ini.stretch-default.sample /etc/php/7.0/fpm/php.ini -sudo chown root:root /etc/php/7.0/fpm/php.ini -sudo chmod 644 /etc/php/7.0/fpm/php.ini - -# SUDO users (adding web server here) -# -r--r----- 1 root root 703 Nov 17 21:08 /etc/sudoers -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/sudoers.stretch-default.sample /etc/sudoers -sudo chown root:root /etc/sudoers -sudo chmod 440 /etc/sudoers - -# copy shell script for player -cp /home/pi/RPi-Jukebox-RFID/settings/rfid_trigger_play.conf.sample /home/pi/RPi-Jukebox-RFID/settings/rfid_trigger_play.conf - -# creating files containing editable values for configuration -# DISCONTINUED: now done by MPD? echo "PCM" > /home/pi/RPi-Jukebox-RFID/settings/Audio_iFace_Name -echo "$AUDIOiFace" > /home/pi/RPi-Jukebox-RFID/settings/Audio_iFace_Name -echo "$DIRaudioFolders" > /home/pi/RPi-Jukebox-RFID/settings/Audio_Folders_Path -echo "3" > /home/pi/RPi-Jukebox-RFID/settings/Audio_Volume_Change_Step -echo "100" > /home/pi/RPi-Jukebox-RFID/settings/Max_Volume_Limit -echo "0" > /home/pi/RPi-Jukebox-RFID/settings/Idle_Time_Before_Shutdown -echo "RESTART" > /home/pi/RPi-Jukebox-RFID/settings/Second_Swipe -echo "/home/pi/RPi-Jukebox-RFID/playlists" > /home/pi/RPi-Jukebox-RFID/settings/Playlists_Folders_Path -echo "ON" > /home/pi/RPi-Jukebox-RFID/settings/ShowCover - -# The new way of making the bash daemon is using the helperscripts -# creating the shortcuts and script from a CSV file. -# see scripts/helperscripts/AssignIDs4Shortcuts.php - -# create config file for web app from sample -sudo cp /home/pi/RPi-Jukebox-RFID/htdocs/config.php.sample /home/pi/RPi-Jukebox-RFID/htdocs/config.php - -# Starting web server and php7 -sudo lighttpd-enable-mod fastcgi -sudo lighttpd-enable-mod fastcgi-php -sudo service lighttpd force-reload -sudo service php7.0-fpm restart - -# create copy of GPIO script -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/gpio-buttons.py.sample /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py -sudo chmod +x /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py - -# make sure bash scripts have the right settings -sudo chown pi:www-data /home/pi/RPi-Jukebox-RFID/scripts/*.sh -sudo chmod +x /home/pi/RPi-Jukebox-RFID/scripts/*.sh -sudo chown pi:www-data /home/pi/RPi-Jukebox-RFID/scripts/*.py -sudo chmod +x /home/pi/RPi-Jukebox-RFID/scripts/*.py - -# services to launch after boot using systemd -# -rw-r--r-- 1 root root 304 Apr 30 10:07 phoniebox-rfid-reader.service -# 1. delete old services (this is legacy, might throw errors but is necessary. Valid for versions < 1.1.8-beta) -echo "### Deleting older versions of service daemons. This might throw errors, ignore them" -sudo systemctl disable idle-watchdog -sudo systemctl disable rfid-reader -sudo systemctl disable startup-sound -sudo systemctl disable gpio-buttons -sudo rm /etc/systemd/system/rfid-reader.service -sudo rm /etc/systemd/system/startup-sound.service -sudo rm /etc/systemd/system/gpio-buttons.service -sudo rm /etc/systemd/system/idle-watchdog.service -echo "### Done with erasing old daemons. Stop ignoring errors!" -# 2. install new ones - this is version > 1.1.8-beta -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample /etc/systemd/system/phoniebox-rfid-reader.service -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample /etc/systemd/system/phoniebox-startup-sound.service -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-gpio-buttons.service.stretch-default.sample /etc/systemd/system/phoniebox-gpio-buttons.service -sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample /etc/systemd/system/phoniebox-idle-watchdog.service -sudo chown root:root /etc/systemd/system/phoniebox-rfid-reader.service -sudo chown root:root /etc/systemd/system/phoniebox-startup-sound.service -sudo chown root:root /etc/systemd/system/phoniebox-gpio-buttons.service -sudo chown root:root /etc/systemd/system/phoniebox-idle-watchdog.service -sudo chmod 644 /etc/systemd/system/phoniebox-rfid-reader.service -sudo chmod 644 /etc/systemd/system/phoniebox-startup-sound.service -sudo chmod 644 /etc/systemd/system/phoniebox-gpio-buttons.service -sudo chmod 644 /etc/systemd/system/phoniebox-idle-watchdog.service -# enable the services needed -sudo systemctl enable phoniebox-idle-watchdog -sudo systemctl enable phoniebox-rfid-reader -sudo systemctl enable phoniebox-startup-sound -sudo systemctl enable phoniebox-gpio-buttons - -# copy mp3s for startup and shutdown sound to the right folder -cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/startupsound.mp3.sample /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 -cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/shutdownsound.mp3.sample /home/pi/RPi-Jukebox-RFID/shared/shutdownsound.mp3 - -if [ $SPOTinstall == "NO" ] -then - # MPD configuration - # -rw-r----- 1 mpd audio 14043 Jul 17 20:16 /etc/mpd.conf - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/mpd.conf.sample /etc/mpd.conf - # Change vars to match install config - sudo sed -i 's/%AUDIOiFace%/'"$AUDIOiFace"'/' /etc/mpd.conf - # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash - sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' /etc/mpd.conf - echo "classic" > /home/pi/RPi-Jukebox-RFID/settings/edition - sudo chown mpd:audio /etc/mpd.conf - sudo chmod 640 /etc/mpd.conf - # update mpc / mpd DB - mpc update -fi - -if [ $SPOTinstall == "YES" ] -then - sudo systemctl disable mpd - sudo systemctl enable mopidy - # Install Config Files - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/locale.gen.sample /etc/locale.gen - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/locale.sample /etc/default/locale - sudo locale-gen - sudo mkdir /home/pi/.config - sudo mkdir /home/pi/.config/mopidy - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/mopidy-etc.sample /etc/mopidy/mopidy.conf - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/mopidy.sample ~/.config/mopidy/mopidy.conf - echo "plusSpotify" > /home/pi/RPi-Jukebox-RFID/settings/edition - # Change vars to match install config - sudo sed -i 's/%spotify_username%/'"$SPOTIuser"'/' /etc/mopidy/mopidy.conf - sudo sed -i 's/%spotify_password%/'"$SPOTIpass"'/' /etc/mopidy/mopidy.conf - sudo sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' /etc/mopidy/mopidy.conf - sudo sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' /etc/mopidy/mopidy.conf - sudo sed -i 's/%spotify_username%/'"$SPOTIuser"'/' ~/.config/mopidy/mopidy.conf - sudo sed -i 's/%spotify_password%/'"$SPOTIpass"'/' ~/.config/mopidy/mopidy.conf - sudo sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' ~/.config/mopidy/mopidy.conf - sudo sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' ~/.config/mopidy/mopidy.conf -fi - -############################### -# WiFi settings (SSID password) -# -# https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md -# -# $WIFIssid -# $WIFIpass -# $WIFIip -# $WIFIipRouter -if [ $WIFIconfig == "YES" ] -then - # DHCP configuration settings - #-rw-rw-r-- 1 root netdev 0 Apr 17 11:25 /etc/dhcpcd.conf - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/dhcpcd.conf.stretch-default2-noHotspot.sample /etc/dhcpcd.conf - # Change IP for router and Phoniebox - sudo sed -i 's/%WIFIip%/'"$WIFIip"'/' /etc/dhcpcd.conf - sudo sed -i 's/%WIFIipRouter%/'"$WIFIipRouter"'/' /etc/dhcpcd.conf - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' /etc/dhcpcd.conf - # Change user:group and access mod - sudo chown root:netdev /etc/dhcpcd.conf - sudo chmod 664 /etc/dhcpcd.conf - - # WiFi SSID & Password - # -rw-rw-r-- 1 root netdev 137 Jul 16 08:53 /etc/wpa_supplicant/wpa_supplicant.conf - sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/wpa_supplicant.conf.stretch.sample /etc/wpa_supplicant/wpa_supplicant.conf - sudo sed -i 's/%WIFIssid%/'"$WIFIssid"'/' /etc/wpa_supplicant/wpa_supplicant.conf - sudo sed -i 's/%WIFIpass%/'"$WIFIpass"'/' /etc/wpa_supplicant/wpa_supplicant.conf - sudo sed -i 's/%WIFIcountryCode%/'"$WIFIcountryCode"'/' /etc/wpa_supplicant/wpa_supplicant.conf - sudo chown root:netdev /etc/wpa_supplicant/wpa_supplicant.conf - sudo chmod 664 /etc/wpa_supplicant/wpa_supplicant.conf -fi - -# start DHCP -sudo service dhcpcd start -sudo systemctl enable dhcpcd - -# / WiFi settings (SSID password) -############################### - -# / INSTALLATION -##################################################### - -##################################################### -# EXISTING ASSETS TO USE FROM EXISTING INSTALL - -if [ $EXISTINGuse == "YES" ] -then - - # RFID config for system control - if [ $EXISTINGuseRfidConf == "YES" ] - then - # read old values and write them into new file (copied above already) - # do not overwrite but use 'sed' in case there are new vars in new version installed - - # Read the existing RFID config file line by line and use - # only lines which are separated (IFS) by '='. - while IFS='=' read -r key val ; do - # $var should be stripped of possible leading or trailing " - val=${val%\"} - val=${val#\"} - key=${key} - # Additional error check: key should not start with a hash and not be empty. - if [ ! "${key:0:1}" == '#' ] && [ -n "$key" ] - then - # Replace the matching value in the newly created conf file - sed -i 's/%'"$key"'%/'"$val"'/' /home/pi/RPi-Jukebox-RFID/settings/rfid_trigger_play.conf - fi - done /dev/null | grep 'installed') ]]; then - echo " ${package} is installed" - else - echo " ERROR: ${package} is not installed" - ((failed_tests++)) - fi - ((tests++)) - done -} - -verify_pip_packages() { - local modules="evdev spi-py youtube_dl pyserial RPi.GPIO" - local modules_spotify="Mopidy-Iris" - local modules_pn532="py532lib" - local modules_rc522="pi-rc522" - local deviceName="${JUKEBOX_HOME_DIR}"/scripts/deviceName.txt - - printf "\nTESTING installed pip modules...\n\n" - - # also check for spotify pip modules if it has been installed - if [[ "${SPOTinstall}" == "YES" ]]; then - modules="${modules} ${modules_spotify}" - fi - - if [[ -f "${deviceName}" ]]; then - # RC522 reader is used - if grep -Fxq "${deviceName}" MFRC522 - then - modules="${modules} ${modules_rc522}" - fi - - # PN532 reader is used - if grep -Fxq "${deviceName}" PN532 - then - modules="${modules} ${modules_pn532}" - fi - fi - - for module in ${modules} - do - if [[ $(pip3 show "${module}") ]]; then - echo " ${module} is installed" - else - echo " ERROR: pip module ${module} is not installed" - ((failed_tests++)) - fi - ((tests++)) - done -} - -verify_samba_config() { - printf "\nTESTING samba config...\n\n" - check_chmod_chown 644 root root "/etc/samba" "smb.conf" - - check_file_contains_string "path=${DIRaudioFolders}" "/etc/samba/smb.conf" -} - -verify_webserver_config() { - printf "\nTESTING webserver config...\n\n" - check_chmod_chown 644 root root "/etc/lighttpd" "lighttpd.conf" - check_chmod_chown 644 root root "/etc/lighttpd/conf-available" "15-fastcgi-php.conf" - check_chmod_chown 644 root root "/etc/php/7.3/cgi" "php.ini" - check_chmod_chown 440 root root "/etc" "sudoers" - - # Bonus TODO: check that fastcgi and fastcgi-php mods are enabled -} - -verify_systemd_services() { - printf "\nTESTING systemd services...\n\n" - # check that services exist - check_chmod_chown 644 root root "/etc/systemd/system" "phoniebox-rfid-reader.service phoniebox-startup-scripts.service phoniebox-gpio-control.service phoniebox-idle-watchdog.service" - - # check that phoniebox services are enabled - check_service_enablement phoniebox-idle-watchdog enabled - check_service_enablement phoniebox-rfid-reader enabled - check_service_enablement phoniebox-startup-scripts enabled - check_service_enablement phoniebox-gpio-control enabled -} - -verify_spotify_config() { - local etc_mopidy_conf="/etc/mopidy/mopidy.conf" - local mopidy_conf="${HOME_DIR}/.config/mopidy/mopidy.conf" - - printf "\nTESTING spotify config...\n\n" - - check_file_contains_string "username = ${SPOTIuser}" "${etc_mopidy_conf}" - check_file_contains_string "password = ${SPOTIpass}" "${etc_mopidy_conf}" - check_file_contains_string "client_id = ${SPOTIclientid}" "${etc_mopidy_conf}" - check_file_contains_string "client_secret = ${SPOTIclientsecret}" "${etc_mopidy_conf}" - check_file_contains_string "media_dir = ${DIRaudioFolders}" "${etc_mopidy_conf}" - - check_file_contains_string "username = ${SPOTIuser}" "${mopidy_conf}" - check_file_contains_string "password = ${SPOTIpass}" "${mopidy_conf}" - check_file_contains_string "client_id = ${SPOTIclientid}" "${mopidy_conf}" - check_file_contains_string "client_secret = ${SPOTIclientsecret}" "${mopidy_conf}" - check_file_contains_string "media_dir = ${DIRaudioFolders}" "${mopidy_conf}" - - # check that mopidy service is enabled - check_service_enablement mopidy enabled - # check that mpd service is disabled - check_service_enablement mpd disabled -} - -verify_mpd_config() { - local mpd_conf="/etc/mpd.conf" - - printf "\nTESTING mpd config...\n\n" - - check_file_contains_string "^[[:blank:]]\+mixer_control[[:blank:]]\+\"${AUDIOiFace}\"" "${mpd_conf}" - check_file_contains_string "^music_directory[[:blank:]]\+\"${DIRaudioFolders}\"" "${mpd_conf}" - - check_chmod_chown 640 mpd audio "/etc" "mpd.conf" - - # check that mpd service is enabled, when Spotify support is not installed - if [[ "${SPOTinstall}" == "NO" ]]; then - check_service_enablement mpd enabled - fi -} - -verify_folder_access() { - local jukebox_dir="${HOME_DIR}/RPi-Jukebox-RFID" - printf "\nTESTING folder access...\n\n" - - # check owner and permissions - check_chmod_chown 775 pi www-data "${jukebox_dir}" "playlists shared htdocs settings" - # ${DIRaudioFolders} => "testing" "audiofolders" - check_chmod_chown 775 pi www-data "${DIRaudioFolders}/.." "audiofolders" - - #find .sh and .py scripts that are NOT executable - local count=$(find . -maxdepth 1 -type f \( -name "*.sh" -o -name "*.py" \) ! -executable | wc -l) - if [[ "${count}" -gt 0 ]]; then - echo " ERROR: found ${count} '*.sh' and/or '*.py' files that are NOT executable:" - find . -maxdepth 1 -type f \( -name "*.sh" -o -name "*.py" \) ! -executable - ((failed_tests++)) - fi - ((tests++)) -} - -main() { - printf "\nTesting installation:\n" - verify_conf_file - if [[ "$WIFIconfig" == "YES" ]]; then - verify_wifi_settings - fi - verify_apt_packages - verify_pip_packages - verify_samba_config - verify_webserver_config - verify_systemd_services - if [[ "${SPOTinstall}" == "YES" ]]; then - verify_spotify_config - fi - verify_mpd_config - verify_folder_access -} - -start=$(date +%s) -main -end=$(date +%s) - -runtime=$((end-start)) -((h=${runtime}/3600)) -((m=($runtime%3600)/60)) -((s=$runtime%60)) - -if [[ "${failed_tests}" -gt 0 ]]; then - echo "${failed_tests} Test(s) failed (of ${tests} tests) (in ${h}h ${m}m ${s}s)." - exit 1 -else - echo "${tests} tests done in ${h}h ${m}m ${s}s." -fi - diff --git a/scripts/playlist_recursive_by_folder.php b/scripts/playlist_recursive_by_folder.php deleted file mode 100755 index 8d62de3ad..000000000 --- a/scripts/playlist_recursive_by_folder.php +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/php - $value) { - // drop directories - if(is_dir($folder."/".$value)){ - unset($folder_files[$key]); - } - // drop config files - if($folder."/".$value == $folder."/folder.conf"){ - unset($folder_files[$key]); - } - // drop cover files - if($folder."/".$value == $folder."/cover.jpg"){ - unset($folder_files[$key]); - } - // drop title files - if($folder."/".$value == $folder."/title.txt"){ - unset($folder_files[$key]); - } - } - // some debugging info - if($debug == "true") { - print "\$folder_files cleaned:"; - print_r($folder_files); - } - - /* - * relative path from the $Audio_Folders_Path_Playlist folder - * which is also set in the mpd.conf - */ - if ($edition == "plusSpotify") { - // M3U will contain local:track: path for mopidy - foreach ($folder_files as $key => $value) { - $folder_files[$key] = "local:track:".str_replace("%2F", "/", rawurlencode(str_replace($Audio_Folders_Path."/", "", $folder."/".$value))); - } - } elseif ($edition == "classic") { - // M3U will contain normal relative path - foreach ($folder_files as $key => $value) { - $folder_files[$key] = substr($Audio_Folders_Path."/".$folder."/".$value, strlen($Audio_Folders_Path) + 1, strlen($folder."/".$value)); - } - } - /* - * order the remaining files - if any... - * NOTE: podcast content is NOT ordered - because they are an ordered playlist already - */ - usort($folder_files, 'strnatcasecmp'); - } - /* - * push files to playlist - */ - $files_playlist = array_merge($files_playlist, $folder_files); - //} -} - -$return = ""; -foreach($files_playlist as $file_playlist) { - if($file_playlist != "") { - $return .= $file_playlist."\n"; - } -} -print $return; -//print trim($return); - -?> diff --git a/scripts/playout_controls.sh b/scripts/playout_controls.sh deleted file mode 100755 index 483f1410f..000000000 --- a/scripts/playout_controls.sh +++ /dev/null @@ -1,1035 +0,0 @@ -#!/bin/bash - -# This shell script contains all the functionality to control -# playout and change volume and the like. -# This script is called from the web app and the bash script. -# The purpose is to have all playout logic in one place, this -# makes further development and potential replacement of -# the playout player easier. - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -mtime=`date +%s` - - -# USAGE EXAMPLES: -# -# shutdown RPi: -# ./playout_controls.sh -c=shutdown -# -# set volume to 80% -# ./playout_controls.sh -c=setvolume -v=80 -# -# VALID COMMANDS: -# shutdown -# shutdownsilent -# shutdownafter -# shutdownvolumereduction -# reboot -# scan -# mute -# setvolume -# setmaxvolume -# setstartupvolume -# getstartupvolume -# setvolumetostartup -# volumeup -# volumedown -# getchapters -# getvolume -# getmaxvolume -# setvolstep -# getvolstep -# playerstop -# playerstopafter -# playernext -# playerprev -# playernextchapter -# playerprevchapter -# playerpause -# playerpauseforce -# playerplay -# playerremove -# playermoveup -# playermovedown -# playerreplay -# playerrepeat -# playershuffle -# playlistclear -# playlistaddplay -# playlistadd -# playlistappend -# playlistreset -# playsinglefile -# getidletime -# setidletime -# disablewifi -# enablewifi -# togglewifi -# recordstart -# recordstop -# recordplaylatest -# readwifiipoverspeaker - -# The absolute path to the folder which contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. ${PATHDATA}/../settings/debugLogging.conf - -if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "########### SCRIPT playout_controls.sh ($NOW) ##" >> ${PATHDATA}/../logs/debug.log; fi - -########################################################### -# Read global configuration file (and create if not exists) -# create the global configuration file from single files - if it does not exist -if [ ! -f ${PATHDATA}/../settings/global.conf ]; then - . ${PATHDATA}/inc.writeGlobalConfig.sh -fi -. ${PATHDATA}/../settings/global.conf -########################################################### - -################################# -# path to file storing the current volume level -# this file does not need to exist -# it will be created or deleted by this script -VOLFILE=${PATHDATA}/../settings/Audio_Volume_Level - -############################################################# - -# Get args from command line (see Usage above) -# Read the args passed on by the command line -# see following file for details: -. ${PATHDATA}/inc.readArgsFromCommandLine.sh - -if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "VAR COMMAND: ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi -if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "VAR VALUE: ${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - -# Regex that declares commands for which the following code can be shortcut -# and we can immediately jump to the switch-case statement. Increases execution -# speed of these commands. -shortcutCommands="^(setvolume|volumedown|volumeup|mute)$" - -# Run the code from this block only, if the current command is not in "shortcutCommands" -if [[ ! "$COMMAND" =~ $shortcutCommands ]] -then - - function dbg { - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then - echo "$1" >> ${PATHDATA}/../logs/debug.log; - fi - } - - function sec_to_ms() { - SECONDSPART="$(cut -d '.' -f 1 <<< "$1")" - MILLISECONDSPART="$(cut -d '.' -f 2 <<< "$1")" - MILLISECONDSPART_NORMALIZED="$(echo "$MILLISECONDSPART" | cut -c1-3 | sed 's/^0*//')" - - if [[ "" == "$SECONDSPART" ]]; then - SECONDSPART="0" - fi - - if [[ "" == "$MILLISECONDSPART_NORMALIZED" ]]; then - MILLISECONDSPART_NORMALIZED="0" - fi - echo "$((${SECONDSPART} * 1000 + ${MILLISECONDSPART_NORMALIZED}))" - } - - AUDIO_FOLDERS_PATH=$(cat "${PATHDATA}/../settings/Audio_Folders_Path") - - CURRENT_SONG_INFO=$(echo -e "currentsong\nclose" | nc -w 1 localhost 6600) - CURRENT_SONG_FILE=$(echo "$CURRENT_SONG_INFO" | grep -o -P '(?<=file: ).*') - CURRENT_SONG_FILE_ABS="${AUDIO_FOLDERS_PATH}/${CURRENT_SONG_FILE}" - dbg "current file: $CURRENT_SONG_FILE_ABS" - - CURRENT_SONG_DIR="$(dirname -- "$CURRENT_SONG_FILE_ABS")" - CURRENT_SONG_BASENAME="$(basename -- "${CURRENT_SONG_FILE_ABS}")" - CURRENT_SONG_FILE_EXT="${CURRENT_SONG_BASENAME##*.}" - CURRENT_SONG_ELAPSED=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=elapsed: ).*') - CURRENT_SONG_DURATION=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=duration: ).*') - - CHAPTERS_FILE="${CURRENT_SONG_DIR}/${CURRENT_SONG_BASENAME%.*}.chapters.json" - dbg "chapters file: $CHAPTERS_FILE" - - if [ "$(grep -wo "$CURRENT_SONG_FILE_EXT" <<< "$CHAPTEREXTENSIONS")" == "$CURRENT_SONG_FILE_EXT" ]; then - CHAPTER_SUPPORT_FOR_EXTENSION="1" - else - CHAPTER_SUPPORT_FOR_EXTENSION="0" - fi - dbg "chapters for extension enabled: $CHAPTER_SUPPORT_FOR_EXTENSION" - - - if [ "$(printf "${CURRENT_SONG_DURATION}\n${CHAPTERMINDURATION}\n" | sort -g | head -1)" == "${CHAPTERMINDURATION}" ]; then - CHAPTER_SUPPORT_FOR_DURATION="1" - else - CHAPTER_SUPPORT_FOR_DURATION="0" - fi - dbg "chapters for duration enabled: $CHAPTER_SUPPORT_FOR_DURATION" - - if [ "${CHAPTER_SUPPORT_FOR_EXTENSION}${CHAPTER_SUPPORT_FOR_DURATION}" == "11" ]; then - if ! [ -f "${CHAPTERS_FILE}" ]; then - CHAPTERS_COUNT="0" - dbg "chaptes file does not exist - export triggered" - ffprobe -i "${CURRENT_SONG_FILE_ABS}" -print_format json -show_chapters -loglevel error > "${CHAPTERS_FILE}" & - else - CHAPTERS_COUNT="$(grep '"id":' "${CHAPTERS_FILE}" | wc -l )" - dbg "chapters file does exist, chapter count: $CHAPTERS_COUNT" - fi - - CHAPTER_START_TIMES="$( ( echo -e $CURRENT_SONG_ELAPSED & grep 'start_time' "$CHAPTERS_FILE" | cut -d '"' -f 4 | sed 's/000$//') | sort -V)" - ELAPSED_MATCH_CHAPTER_COUNT=$(grep "$CURRENT_SONG_ELAPSED" <<< "$CHAPTER_START_TIMES" | wc -l) - - # elapsed and chapter start exactly match -> skip one line - if [ "$ELAPSED_MATCH_CHAPTER_COUNT" == "2" ]; then - PREV_CHAPTER_START=$(grep "$CURRENT_SONG_ELAPSED" -B 1 <<< "$CHAPTER_START_TIMES" | head -n1) - CURRENT_CHAPTER_START="$CURRENT_SONG_ELAPSED" - else - PREV_CHAPTER_START=$(grep "$CURRENT_SONG_ELAPSED" -B 2 <<< "$CHAPTER_START_TIMES" | head -n1) - CURRENT_CHAPTER_START=$(grep "$CURRENT_SONG_ELAPSED" -B 1 <<< "$CHAPTER_START_TIMES" | head -n1) - fi - - NEXT_CHAPTER_START=$(grep "$CURRENT_SONG_ELAPSED" -A 1 <<< "$CHAPTER_START_TIMES" | tail -n1) - fi - - # SHUFFLE_STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=random: ).*') -fi # END COMMANDS SHORTCUT - -case $COMMAND in - shutdown) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - while : - do - apt=1 - sudo lsof /var/lib/apt/lists/lock > /dev/null - apt=$(($apt * $?)) - sudo lsof /var/lib/dpkg/lock > /dev/null - apt=$(($apt * $?)) - sudo lsof /var/cache/apt/archives/lock > /dev/null - apt=$(($apt * $?)) - if [ $apt -eq 0 ]; then - sleep 5 - else - break - fi - done - ${PATHDATA}/resume_play.sh -c=savepos && mpc clear - #remove shuffle mode if active - SHUFFLE_STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=random: ).*') - if [ "$SHUFFLE_STATUS" == 1 ] ; then mpc random off; fi - sleep 1 - /usr/bin/mpg123 ${PATHDATA}/../shared/shutdownsound.mp3 - sleep 3 - ${POWEROFFCMD} - ;; - shutdownsilent) - # doesn't play a shutdown sound - while : - do - apt=1 - sudo lsof /var/lib/apt/lists/lock > /dev/null - apt=$(($apt * $?)) - sudo lsof /var/lib/dpkg/lock > /dev/null - apt=$(($apt * $?)) - sudo lsof /var/cache/apt/archives/lock > /dev/null - apt=$(($apt * $?)) - if [ $apt -eq 0 ]; then - sleep 5 - else - break - fi - done - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - ${PATHDATA}/resume_play.sh -c=savepos && mpc clear - #remove shuffle mode if active - SHUFFLE_STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=random: ).*') - if [ "$SHUFFLE_STATUS" == 1 ] ; then mpc random off; fi - ${POWEROFFCMD} - ;; - shutdownafter) - # remove shutdown times if existent - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - for i in `sudo atq -q t | awk '{print $1}'`;do sudo atrm $i;done - # -c=shutdownafter -v=0 is to remove the shutdown timer - if [ ${VALUE} -gt 0 ]; - then - # shutdown pi after ${VALUE} minutes - echo "${PATHDATA}/playout_controls.sh -c=shutdownsilent" | at -q t now + ${VALUE} minute - fi - ;; - shutdownvolumereduction) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # remove existing volume and shutdown commands - for i in `sudo atq -q r | awk '{print $1}'`;do sudo atrm $i;done - for i in `sudo atq -q q | awk '{print $1}'`;do sudo atrm $i;done - # get current volume in percent - VOLPERCENT=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*') - # divide current volume by 10 to get a step size for reducing the volume - VOLSTEP=`expr $((VOLPERCENT / 10))`; - # divide VALUE by 10, volume will be reduced every TIMESTEP minutes (e.g. for a value of "30" it will be every "3" minutes) - TIMESTEP=`expr $((VALUE / 10))`; - # loop 10 times to reduce the volume by VOLSTEP every TIMESTEP minutes - for i in $(seq 1 10); do - VOLPERCENT=`expr ${VOLPERCENT} - ${VOLSTEP}`; echo "${PATHDATA}/playout_controls.sh -c=setvolume -v="$VOLPERCENT | at -q r now + `expr $(((i * TIMESTEP)-1))` minute; - done - # schedule shutdown after VALUE minutes - if [ ${VALUE} -gt 0 ]; - then - # schedule shutdown after VALUE minutes - echo "${PATHDATA}/playout_controls.sh -c=shutdownsilent" | at -q q now + ${VALUE} minute - fi - ;; - reboot) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - ${PATHDATA}/resume_play.sh -c=savepos && mpc clear - #remove shuffle mode if active - SHUFFLE_STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=random: ).*') - if [ "$SHUFFLE_STATUS" == 1 ] ; then mpc random off; fi - sudo reboot - ;; - scan) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - ${PATHDATA}/resume_play.sh -c=savepos && mpc clear - #remove shuffle mode if active - SHUFFLE_STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=random: ).*') - if [ "$SHUFFLE_STATUS" == 1 ] ; then mpc random off; fi - sudo systemctl stop mopidy - sudo mopidyctl local scan - sudo systemctl start mopidy - ;; - mute) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} | VOLUMEMANAGER:${VOLUMEMANAGER}" >> ${PATHDATA}/../logs/debug.log; fi - if [ ! -f $VOLFILE ]; then - # $VOLFILE does NOT exist == audio on - # read volume in percent and write to $VOLFILE - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sget \'$AUDIOIFACENAME\' | grep -Po -m 1 '(?<=\[)[^]]*(?=%])' > $VOLFILE - else - # manage volume with mpd - echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*' > $VOLFILE - fi - # set volume to 0% - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' 0% - else - # manage volume with mpd - echo -e setvol 0\\nclose | nc -w 1 localhost 6600 - fi - else - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - else - # manage volume with mpd - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - fi - # delete $VOLFILE - rm -f $VOLFILE - fi - ;; - setvolume) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} | VOLUMEMANAGER:${VOLUMEMANAGER}" >> ${PATHDATA}/../logs/debug.log; fi - #increase volume only if VOLPERCENT is below the max volume limit and above min volume limit - if [ ${VALUE} -le $AUDIOVOLMAXLIMIT ] && [ ${VALUE} -ge $AUDIOVOLMINLIMIT ]; - then - # set volume level in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' $VALUE% - else - # manage volume with mpd - echo -e setvol $VALUE\\nclose | nc -w 1 localhost 6600 - fi - else - if [ ${VALUE} -gt $AUDIOVOLMAXLIMIT ]; - then - # if we are over the max volume limit, set the volume to maxvol - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' $AUDIOVOLMAXLIMIT% - else - # manage volume with mpd - echo -e setvol $AUDIOVOLMAXLIMIT\\nclose | nc -w 1 localhost 6600 - fi - fi - if [ ${VALUE} -lt $AUDIOVOLMINLIMIT ]; - then - # if we are unter the min volume limit, set the volume to minvol - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' $AUDIOVOLMINLIMIT% - else - # manage volume with mpd - echo -e setvol $AUDIOVOLMINLIMIT\\nclose | nc -w 1 localhost 6600 - fi - fi - fi - ;; - volumeup) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - #check for volume change during idle - if [ $VOLCHANGEIDLE == "FALSE" ] || [ $VOLCHANGEIDLE == "OnlyDown" ]; - then - PLAYSTATE=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=state: ).*') - if [ "$PLAYSTATE" != "play" ] - then - #Volume change is not allowed - leave program - exit 1 - fi - fi - if [ ! -f $VOLFILE ]; then - if [ -z ${VALUE} ]; then - VALUE=1 - fi - # $VOLFILE does NOT exist == audio on - # read volume in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - VOLPERCENT=`amixer sget \'$AUDIOIFACENAME\' | grep -Po -m 1 '(?<=\[)[^]]*(?=%])'` - else - # manage volume with mpd - VOLPERCENT=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*') - fi - # increase by $AUDIOVOLCHANGESTEP - VOLPERCENT=`expr ${VOLPERCENT} + \( ${AUDIOVOLCHANGESTEP} \* ${VALUE} \)` - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " VOLPERCENT:${VOLPERCENT} | VOLUMEMANAGER:${VOLUMEMANAGER}" >> ${PATHDATA}/../logs/debug.log; fi - #increase volume only if VOLPERCENT is below the max volume limit - if [ $VOLPERCENT -le $AUDIOVOLMAXLIMIT ]; - then - # set volume level in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' ${VOLPERCENT}% - else - # manage volume with mpd - echo -e setvol +$VOLPERCENT\\nclose | nc -w 1 localhost 6600 - fi - else - # if we are over the max volume limit, set the volume to maxvol - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' ${AUDIOVOLMAXLIMIT}% - else - # manage volume with mpd - echo -e setvol $AUDIOVOLMAXLIMIT\\nclose | nc -w 1 localhost 6600 - fi - fi - else - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - else - # manage volume with mpd - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - fi - # delete $VOLFILE - rm -f $VOLFILE - fi - ;; - volumedown) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - #check for volume change during idle - if [ $VOLCHANGEIDLE == "FALSE" ] || [ $VOLCHANGEIDLE == "OnlyUp" ]; - then - PLAYSTATE=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=state: ).*') - if [ "$PLAYSTATE" != "play" ] - then - #Volume change is not allowed - leave program - exit 1 - fi - fi - if [ ! -f $VOLFILE ]; then - if [ -z ${VALUE} ]; then - VALUE=1 - fi - # $VOLFILE does NOT exist == audio on - # read volume in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - VOLPERCENT=`amixer sget \'$AUDIOIFACENAME\' | grep -Po -m 1 '(?<=\[)[^]]*(?=%])'` - else - # manage volume with mpd - VOLPERCENT=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*') - fi - # decrease by $AUDIOVOLCHANGESTEP - VOLPERCENT=`expr ${VOLPERCENT} - \( ${AUDIOVOLCHANGESTEP} \* ${VALUE} \)` - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " VOLPERCENT:${VOLPERCENT} | VOLUMEMANAGER:${VOLUMEMANAGER}" >> ${PATHDATA}/../logs/debug.log; fi - #decrease volume only if VOLPERCENT is above the min volume limit - if [ $VOLPERCENT -ge $AUDIOVOLMINLIMIT ]; - then - # set volume level in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' ${VOLPERCENT}% - else - # manage volume with mpd - echo -e setvol +$VOLPERCENT\\nclose | nc -w 1 localhost 6600 - fi - else - # if we are below the min volume limit, set the volume to minvol - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' ${AUDIOVOLMINLIMIT}% - else - # manage volume with mpd - echo -e setvol $AUDIOVOLMINLIMIT\\nclose | nc -w 1 localhost 6600 - fi - fi - else - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - else - # manage volume with mpd - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - fi - # delete $VOLFILE - rm -f $VOLFILE - fi - ;; - getchapters) - if [ -f "${CHAPTERS_FILE}" ]; then cat "${CHAPTERS_FILE}"; fi - ;; - getvolume) - # read volume in percent - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "# ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - VOLPERCENT=`amixer sget \'$AUDIOIFACENAME\' | grep -Po -m 1 '(?<=\[)[^]]*(?=%])'` - else - # manage volume with mpd - VOLPERCENT=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*') - fi - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " VOLPERCENT:${VOLPERCENT} | VOLUMEMANAGER:${VOLUMEMANAGER}" >> ${PATHDATA}/../logs/debug.log; fi - echo $VOLPERCENT - ;; - setmaxvolume) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # read volume in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - VOLPERCENT=`amixer sget \'$AUDIOIFACENAME\' | grep -Po -m 1 '(?<=\[)[^]]*(?=%])'` - else - # manage volume with mpd - VOLPERCENT=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=volume: ).*') - fi - # if volume of the box is greater than wanted maxvolume, set volume to maxvolume - if [ $VOLPERCENT -gt ${VALUE} ]; - then - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer sset \'$AUDIOIFACENAME\' ${VALUE}% - else - # manage volume with mpd - echo -e setvol ${VALUE} | nc -w 1 localhost 6600 - fi - fi - # if startupvolume is greater than wanted maxvolume, set startupvolume to maxvolume - if [ ${AUDIOVOLSTARTUP} -gt ${VALUE} ]; - then - # write new value to file - echo "$VALUE" > ${PATHDATA}/../settings/Startup_Volume - fi - # write new value to file - echo "$VALUE" > ${PATHDATA}/../settings/Max_Volume_Limit - # create global config file because individual setting got changed - . ${PATHDATA}/inc.writeGlobalConfig.sh - ;; - getmaxvolume) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - echo $AUDIOVOLMAXLIMIT - ;; - setvolstep) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # write new value to file - echo "$VALUE" > ${PATHDATA}/../settings/Audio_Volume_Change_Step - # create global config file because individual setting got changed - . ${PATHDATA}/inc.writeGlobalConfig.sh - ;; - getvolstep) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - echo $AUDIOVOLCHANGESTEP - ;; - setstartupvolume) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # if value is greater than wanted maxvolume, set value to maxvolume - if [ ${VALUE} -gt $AUDIOVOLMAXLIMIT ]; - then - VALUE=$AUDIOVOLMAXLIMIT; - fi - # write new value to file - echo "$VALUE" > ${PATHDATA}/../settings/Startup_Volume - # create global config file because individual setting got changed - . ${PATHDATA}/inc.writeGlobalConfig.sh - ;; - getstartupvolume) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - echo ${AUDIOVOLSTARTUP} - ;; - setvolumetostartup) - echo "setvolumetostartup $(expr `date +%s` - $mtime) s" - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # check if startup-volume is disabled - if [ "${AUDIOVOLSTARTUP}" == 0 ]; then - exit 1 - else - # set volume level in percent - if [ "${VOLUMEMANAGER}" == "amixer" ]; then - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - amixer -q sset \'$AUDIOIFACENAME\' ${AUDIOVOLSTARTUP}% - echo "after amixer setvolumetostartup $(expr `date +%s` - $mtime) s" - else - # manage volume with mpd - echo -e setvol ${AUDIOVOLSTARTUP}\\nclose | nc -w 1 localhost 6600 - fi - - fi - echo "after setvolumetostartup $(expr `date +%s` - $mtime) s" - ;; - playerstop) - # stop the player - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - ${PATHDATA}/resume_play.sh -c=savepos && mpc stop - #if [ -e $AUDIOFOLDERSPATH/playing.txt ] - #then - # sudo rm $AUDIOFOLDERSPATH/playing.txt - #fi - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "remove playing.txt" >> ${PATHDATA}/../logs/debug.log; fi - ;; - playerstopafter) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # remove playerstop timer if existent - for i in `sudo atq -q s | awk '{print $1}'`;do sudo atrm $i;done - # stop player after ${VALUE} minutes - if [ ${VALUE} -gt 0 ]; - then - echo "${PATHDATA}/resume_play.sh -c=savepos && mpc stop" | at -q s now + ${VALUE} minute - fi - ;; - playernext) - # play next track in playlist (==folder) - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - - mpc next - ;; - playerprev) - # play previous track in playlist (==folder) - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - - mpc prev - ;; - playerprevchapter) - CURRENT_SONG_ELAPSED_MS=$(sec_to_ms "$CURRENT_SONG_ELAPSED") - CURRENT_CHAPTER_START_MS=$(sec_to_ms "$CURRENT_CHAPTER_START") - CHAPTER_DIFF_ELAPSED_CURRENT_MS=$(($CURRENT_SONG_ELAPSED_MS-$CURRENT_CHAPTER_START_MS)) - - # if elapsed - current > 5.000 => seek current chapter - # if elapsed - current <= 5.000 => seek prev chapter - # if prev === 0.000 && elapsed < 5.000 => prev track? (don't do that) - if [ "$CHAPTER_DIFF_ELAPSED_CURRENT_MS" -gt 5000 ]; then - dbg "chapter is already running for longer, seek to current chapter: $SEEK_POS" - echo -e "seekcur $CURRENT_CHAPTER_START\nclose" | nc -w 1 localhost 6600 - else - dbg "chapter just started, seek to prev chapter $PREV_CHAPTER_START" - echo -e "seekcur $PREV_CHAPTER_START\nclose" | nc -w 1 localhost 6600 - fi - ;; - playernextchapter) - # if next === elapsed => next track - if ! [ "$NEXT_CHAPTER_START" == "$CURRENT_SONG_ELAPSED" ]; then - dbg "next chapter $NEXT_CHAPTER_START" - echo -e "seekcur $NEXT_CHAPTER_START\nclose" | nc -w 1 localhost 6600 - else - dbg "next chapter not available, last chapter already playing" - fi - ;; - playerrewind) - # play the first track in playlist (==folder) - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - - mpc play 1 - ;; - playerpause) - # toggle current track - # mpc knows "pause", which pauses only, and "toggle" which pauses and unpauses, whatever is needed - # Why on earth has this been called pause instead of toggle? :-) - - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - mpc toggle - ;; - playerpauseforce) - # pause current track with optional delay - if [ -n ${VALUE} ]; - then - /bin/sleep $VALUE - fi - mpc pause - ;; - playerplay) - # play / resume current track - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "Attempting to play: $VALUE" >> ${PATHDATA}/../logs/debug.log; fi - # May be called with e.g. -v=1 to start a track in the middle of the playlist. - # Note: the numbering of the tracks starts with 0, so -v=1 starts the second track - # of the playlist - # Another note: "mpc play 1" starts the first track (!) - - # Change some settings according to current folder IF the folder.conf exists - . ${PATHDATA}/inc.settingsFolderSpecific.sh - - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - - # No checking for resume if the audio is paused, just unpause it - PLAYSTATE=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=state: ).*') - if [ "$PLAYSTATE" == "pause" ] - then - echo -e "play $VALUE\nclose" | nc -w 1 localhost 6600 - else - #${PATHDATA}/resume_play.sh -c=resume -v=$VALUE - mpc play $VALUE - fi - ;; - playerremove) - # remove selected song position - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "Attempting to remove: $VALUE" >> ${PATHDATA}/../logs/debug.log; fi - - # Change some settings according to current folder IF the folder.conf exists - . ${PATHDATA}/inc.settingsFolderSpecific.sh - - mpc del $VALUE - ;; - playermoveup) - # remove selected song position - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "Attempting to move: $VALUE" >> ${PATHDATA}/../logs/debug.log; fi - - # Change some settings according to current folder IF the folder.conf exists - . ${PATHDATA}/inc.settingsFolderSpecific.sh - - mpc move $(($VALUE)) $(($VALUE-1)) - ;; - playermovedown) - # remove selected song position - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "Attempting to move: $VALUE" >> ${PATHDATA}/../logs/debug.log; fi - - # Change some settings according to current folder IF the folder.conf exists - . ${PATHDATA}/inc.settingsFolderSpecific.sh - - mpc move $(($VALUE)) $(($VALUE+1)) - ;; - playerseek) - # jumps back and forward in track. - # Usage: ./playout_controls.sh -c=playerseek -v=+15 to jump 15 seconds ahead - # ./playout_controls.sh -c=playerseek -v=-10 to jump 10 seconds back - # Note: Not using "mpc seek" here as it fails if one tries to jump ahead of the beginning of the track - # (e.g. "mpc seek -15" executed at an elapsed time of 10 seconds let the player hang). - # mpd seekcur can handle this. - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - - # if value does not start with + or - (relative seek), perform an absolute seek - if [[ $VALUE =~ ^[0-9] ]]; then - # seek absolute position - echo -e "seekcur $VALUE\nclose" | nc -w 1 localhost 6600 - else - # Seek negative value doesn't work in mpd anymore. - # solution taken from: https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/1031 - # if there are issues, please comment in that thread - CUR_POS=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=elapsed: ).*' | awk '{print int($1)}') - NEW_POS=$(($CUR_POS + $VALUE)) - echo -e "seekcur $NEW_POS\nclose" | nc -w 1 localhost 6600 - fi - ;; - playerreplay) - # start the playing track from beginning - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - mpc seek 0 - ;; - playerrepeat) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - # repeats a single track or a playlist. - # Remark: If "single" is "on" but "repeat" is "off", the playout stops after the current song. - # This command may be called with ./playout_controls.sh -c=playerrepeat -v=single, playlist or off - - case ${VALUE} in - single) - mpc repeat on - mpc single on - ;; - playlist) - mpc repeat on - mpc single off - ;; - *) - mpc repeat off - mpc single off - ;; - esac - ;; - playershuffle) - # toogles shuffle mode on/off (not only the current playlist but for the whole mpd) - # this is why a check if "random on" has to be done for shutdown and reboot - # This command may be called with ./playout_controls.sh -c=playershuffle - mpc shuffle - ;; - playlistclear) - # clear playlist - ${PATHDATA}/resume_play.sh -c=savepos - mpc clear - ;; - playlistaddplay) - # add to playlist (and play) - # this command clears the playlist, loads a new playlist and plays it. It also handles the resume play feature. - # FOLDER = rel path from audiofolders - # VALUE = name of playlist - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " playlistaddplay playlist name VALUE: $VALUE" >> ${PATHDATA}/../logs/debug.log; fi - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " playlistaddplay FOLDER: $FOLDER" >> ${PATHDATA}/../logs/debug.log; fi - - # NEW VERSION: - # Read the current config file (include will execute == read) - . "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" - - # load playlist - mpc clear - mpc load "${VALUE//\//SLASH}" - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "mpc load "${VALUE//\//SLASH} >> ${PATHDATA}/../logs/debug.log; fi - - # Change some settings according to current folder IF the folder.conf exists - #. ${PATHDATA}/inc.settingsFolderSpecific.sh - - # check if we switch to single file playout - ${PATHDATA}/single_play.sh -c=single_check -d="${FOLDER}" - - # check if we shuffle the playlist - ${PATHDATA}/shuffle_play.sh -c=shuffle_check -d="${FOLDER}" - - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) - # amixer sset \'$AUDIOIFACENAME\' `<$VOLFILE`% - # delete $VOLFILE - rm -f $VOLFILE - fi - - # Now load and play - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "mpc load "${VALUE//\//SLASH}" && ${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}"" >> ${PATHDATA}/../logs/debug.log; fi - ${PATHDATA}/resume_play.sh -c=resume -d="${FOLDER}" - - # write latest folder played to settings file - sudo echo ${FOLDER} > ${PATHDATA}/../settings/Latest_Folder_Played - sudo chown pi:www-data ${PATHDATA}/../settings/Latest_Folder_Played - sudo chmod 777 ${PATHDATA}/../settings/Latest_Folder_Played - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " echo ${FOLDER} > ${PATHDATA}/../settings/Latest_Folder_Played" >> ${PATHDATA}/../logs/debug.log; fi - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " VAR Latest_Folder_Played: ${FOLDER}" >> ${PATHDATA}/../logs/debug.log; fi - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " # end playout_controls.sh playlistaddplay" >> ${PATHDATA}/../logs/debug.log; fi - - # OLD VERSION (pre 20190302 - delete once the new version really seems to work): - # call shuffle_check HERE to enable/disable folder-based shuffling - # (mpc shuffle is different to random, because when you shuffle before playing, - # you start your playlist with a different track EVERYTIME. With random you EVER - # has the first song and random from track 2. - #mpc load "${VALUE//\//SLASH}" && ${PATHDATA}/shuffle_play.sh -c=shuffle_check && ${PATHDATA}/single_play.sh -c=single_check && ${PATHDATA}/resume_play.sh -c=resume - #mpc load "${VALUE//\//SLASH}" && ${PATHDATA}/single_play.sh -c=single_check && ${PATHDATA}/resume_play.sh -c=resume - - ;; - playlistadd) - # add to playlist, no autoplay - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - # save playlist playing - mpc load "${VALUE}" - ;; - playlistappend) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - mpc add "${VALUE}" - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # delete $VOLFILE - rm -f $VOLFILE - fi - mpc play - ;; - playlistreset) - if [ -e $PATHDATA/../shared/audiofolders/$FOLDERPATH/lastplayed.dat ] - then - echo "" > $PATHDATA/../shared/audiofolders/$FOLDERPATH/lastplayed.dat - fi - mpc play 1 - ;; - playsinglefile) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - mpc clear - mpc add "${VALUE}" - mpc repeat off - mpc single on - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # delete $VOLFILE - rm -f $VOLFILE - fi - mpc play - ;; - setidletime) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - # write new value to file - echo "$VALUE" > ${PATHDATA}/../settings/Idle_Time_Before_Shutdown - # create global config file because individual setting got changed - . ${PATHDATA}/inc.writeGlobalConfig.sh - # restart service to apply the new value - sudo systemctl restart phoniebox-idle-watchdog.service & - ;; - getidletime) - echo $IDLETIMESHUTDOWN - ;; - enablewifi) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - rfkill unblock wifi - ;; - disablewifi) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # see https://forum-raspberrypi.de/forum/thread/25696-bluetooth-und-wlan-deaktivieren/#pid226072 seems to disable wifi, - # as good as it gets - rfkill block wifi - ;; - togglewifi) - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi - # function to allow toggle the wifi state - # Build special for franzformator - rfkill list wifi | grep -i "Soft blocked: no" > /dev/null 2>&1 - WIFI_SOFTBLOCK_RESULT=$? - wpa_cli -i wlan0 status | grep 'ip_address' > /dev/null 2>&1 - WIFI_IP_RESULT=$? - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " WIFI_IP_RESULT='${WIFI_IP_RESULT}' WIFI_SOFTBLOCK_RESULT='${WIFI_SOFTBLOCK_RESULT}'" >> ${PATHDATA}/../logs/debug.log; fi - if [ $WIFI_SOFTBLOCK_RESULT -eq 0 ] && [ $WIFI_IP_RESULT -eq 0 ] - then - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " Wifi will now be deactivated" >> ${PATHDATA}/../logs/debug.log; fi - echo "Wifi will now be deactivated" - rfkill block wifi - else - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " Wifi will now be activated" >> ${PATHDATA}/../logs/debug.log; fi - echo "Wifi will now be activated" - rfkill unblock wifi - fi - ;; - recordstart) - #mkdir $AUDIOFOLDERSPATH/Recordings - #kill the potential current playback - sudo pkill aplay - #start recorder if not already started - if ! pgrep -x "arecord" > /dev/null - then - echo "start recorder" - arecord -D plughw:1 --duration=${VALUE} -f cd -vv $AUDIOFOLDERSPATH/Recordings/$(date +"%Y-%m-%d_%H-%M-%S").wav & - else - echo "device is already recording" - fi - ;; - recordstop) - #kill arecord instances - sudo pkill arecord - ;; - recordplaylatest) - #kill arecord and aplay instances - sudo pkill arecord - sudo pkill aplay - # Unmute if muted - if [ -f $VOLFILE ]; then - # $VOLFILE DOES exist == audio off - # read volume level from $VOLFILE and set as percent - echo -e setvol `<$VOLFILE`\\nclose | nc -w 1 localhost 6600 - # delete $VOLFILE - rm -f $VOLFILE - fi - aplay `ls $AUDIOFOLDERSPATH/Recordings/*.wav -1t|head -1` - ;; - readwifiipoverspeaker) - # will read out the IP address over the Pi's speaker. - # Why? Imagine to go to a new wifi, hook up and not know where to point your browser - cd /home/pi/RPi-Jukebox-RFID/misc/ - # delete older mp3 (in case process was interrupted) - sudo rm WifiIp.mp3 - /usr/bin/php /home/pi/RPi-Jukebox-RFID/scripts/helperscripts/cli_ReadWifiIp.php - ;; - *) - echo Unknown COMMAND $COMMAND VALUE $VALUE - if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "Unknown COMMAND ${COMMAND} VALUE ${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - ;; -esac diff --git a/scripts/python-phoniebox/ConfigParserExtended.py b/scripts/python-phoniebox/ConfigParserExtended.py deleted file mode 100644 index 3227210e5..000000000 --- a/scripts/python-phoniebox/ConfigParserExtended.py +++ /dev/null @@ -1,31 +0,0 @@ -# from configparser import RawConfigParser -import configparser - - -class ConfigParserExtended(configparser.ConfigParser): - - def as_dict(self, section="all"): - if section == "all": - d = self.__dict__['_sections'] - else: - d = self.__dict__['_sections'][section] - return d - - def as_json(self, section="all"): - import json - if section == "all": - d = self.__dict__['_sections'] - else: - d = self.__dict__['_sections'][section] - return json.dumps(d, separators=(',', ':'), indent=4, sort_keys=True, - ensure_ascii=False).encode('utf8') - - def print_ini(self, section="all"): - if section == "all": - sections = self.sections() - else: - sections = [section] - for section_name in sections: - print("[{}]".format(section_name)) - for key, value in self.items(section_name): - print('{} = {}'.format(key, value)) diff --git a/scripts/python-phoniebox/LICENSE b/scripts/python-phoniebox/LICENSE deleted file mode 100644 index e27d99892..000000000 --- a/scripts/python-phoniebox/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 laclaro - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/scripts/python-phoniebox/Phoniebox.py b/scripts/python-phoniebox/Phoniebox.py deleted file mode 100755 index b4f2e2b1a..000000000 --- a/scripts/python-phoniebox/Phoniebox.py +++ /dev/null @@ -1,412 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__name__ = "Phoniebox" - -import configparser # needed only for the exception types ?! -from ConfigParserExtended import ConfigParserExtended -import codecs -import subprocess # needed for aplay call -import os, sys -from time import sleep -from mpd import MPDClient - -# get absolute path of this script -dir_path = os.path.dirname(os.path.realpath(__file__)) -defaultconfigFilePath = os.path.join(dir_path, './phoniebox.conf') - - -# TODO: externalize helper functions for the package. How? -def is_int(s): - """ return True if string is an int """ - try: - int(s) - return True - except ValueError: - return False - - -def str2bool(s): - """ convert string to a python boolean """ - return s.lower() in ("yes", "true", "t", "1") - - -def str2num(s): - """ convert string to an int or a float """ - try: - return int(s) - except ValueError: - return float(s) - - -def find_modified_files(path, since): - modified_files = [] - for root, dirs, files in os.walk(path): - for basename in files: - filename = os.path.join(path, basename) - status = os.stat(filename) - if status.st_mtime > since: - modified_files.append(filename) - return modified_files - - -def file_modified(filename, since): - if os.stat(filename).st_mtime > since: - return True - else: - return False - - -class Phoniebox(object): - - def __init__(self, configFilePath=defaultconfigFilePath): - print("Using configuration file {}".format(configFilePath)) - self.read_config(configFilePath) - # read cardAssignments from given card assignments file - card_assignments_file = self.get_setting("phoniebox", "card_assignments_file") - self.cardAssignments = self.read_cardAssignments() - if self.get_setting("phoniebox", "translate_legacy_cardassignments", "bool") is True: - self.log("Translating legacy cardAssignment config from folder.conf files.", 3) - legacy_cardAssignments = self.translate_legacy_cardAssignments() - self.update_cardAssignments(legacy_cardAssignments) - - def log(self, msg, level=3): - """ level based logging to stdout """ - log_level_map = {0: None, 1: "error", 2: "warning", 3: "info", 4: "extended", 5: "debug"} - log_level = int(self.get_setting("phoniebox", "log_level")) - if log_level >= level and log_level != -1: - print("{}: {}".format(log_level_map[level].upper(), msg)) - - def mpd_init_connection(self): - """ connect to mpd """ - host = self.get_setting("mpd", "host") - if host == -1: - host = "localhost" - port = self.get_setting("mpd", "port") - if port == -1: - port = 6600 - timeout = self.get_setting("mpd", "timeout") - if timeout == -1: - timeout = 3 - - self.client = MPDClient() - self.client.host = host - self.client.port = port - self.client.timeout = timeout - - # ret = self.mpd_connect_timeout() - if self.mpd_connect_timeout() != 0: - sys.exit() - else: - self.log("connected to MPD with settings host = {}, port = {}, timeout = {}".format(host, port, timeout), 3) - - def mpd_connect_timeout(self): - """ establishes the connection to MPD when disconnected """ - success = False - runtime = 0 - try: - self.client.disconnect() - except: - pass - while success is not True and runtime <= self.client.timeout: - try: - self.client.connect(self.client.host, self.client.port) - success = True - self.log("Connected to MPD at {} on port {}.".format(self.client.host, self.client.port), 5) - return 0 - except: - self.log("Could not connect to MPD, retrying.", 5) - sleep(0.2) - runtime += 0.2 - if runtime >= self.client.timeout: - self.log("Could not connect to MPD for {}s, giving up.".format(self.client.timeout), 2) - return 1 - - def do_second_swipe(self): - """ react to the second swipe of the same card according to settings""" - second_swipe_map = {'default': self.do_restart_playlist, - 'restart': self.do_restart_playlist, - 'restart_track': self.do_restart_track, - 'stop': self.do_stop, - 'pause': self.do_toggle, - 'noaudioplay': self.do_pass, - 'skipnext': self.do_next, - } - setting_key = "second_swipe" - map_key = self.config.get("phoniebox", setting_key) - try: - second_swipe_map[map_key]() - except KeyError as e: - self.log("Unknown setting \"{} = {}\", using \"{} = default\".".format(setting_key, map_key, setting_key), 5) - second_swipe_map['default']() - - def do_restart_playlist(self): - """ restart the same playlist from the beginning """ - # TODO: Any reason not to just start the first item in the current playlist? - self.mpd_connect_timeout() - self.set_mpd_playmode(self.lastplayedID) - self.play_mpd(self.get_cardsetting(self.lastplayedID, "uri")) - - def do_restart_track(self): - """ restart currently playing track """ - self.mpd_connect_timeout() - mpd_status = self.client.status() - self.set_mpd_playmode(self.lastplayedID) - # restart current track - self.client.play(mpd_status['song']) - - def do_start_playlist(self, cardid): - """ restart the same playlist, eventually resume """ - if self.get_cardsetting(self.lastplayedID, "resume"): - self.resume(self.lastplayedID, "save") - self.mpd_connect_timeout() - self.set_mpd_playmode(cardid) - self.play_mpd(self.get_cardsetting(cardid, "uri")) - if self.get_cardsetting(cardid, "resume"): - self.resume(cardid, "resume") - self.lastplayedID = cardid - - def do_toggle(self): - """ toggle play/pause """ - self.mpd_connect_timeout() - status = self.client.status() - if status['state'] == "play": - self.client.pause() - else: - self.client.play() - - def do_pass(self): - """ do nothing (on second swipe with noaudioplay) """ - pass - - def do_next(self): - """ skip to next track or restart playlist if stopped (on second swipe with noaudioplay) """ - self.mpd_connect_timeout() - status = self.client.status() - # start playlist if in stop state or there is only one song in the playlist (virtually loop) - if (status["state"] == "stop") or (status["playlistlength"] == "1"): - self.do_restart_playlist() - else: - self.client.next() - - def do_stop(self): - """ do nothing (on second swipe with noaudioplay) """ - self.mpd_connect_timeout() - self.client.stop() - - def play_alsa(self, audiofile): - """ pause mpd and play file on alsa player """ - self.mpd_connect_timeout() - self.client.pause() - # TODO: use the standard audio device or set them via phoniebox.conf - subprocess.call(["aplay -q -Dsysdefault:CARD=sndrpijustboomd " + audiofile], shell=True) - subprocess.call(["aplay -q -Dsysdefault " + audiofile], shell=True) - - def play_mpd(self, uri): - """ play uri in mpd """ - self.mpd_connect_timeout() - self.client.clear() - self.client.add(uri) - self.client.play() - self.log("phoniebox: playing {}".format(uri.encode('utf-8')), 3) - - # TODO: is there a better way to check for "value not present" than to return -1? - def get_setting(self, section, key, opt_type="string"): - """ get a setting from configFile file or cardAssignmentsFile - if not present, return -1 - """ - try: - num = str2num(section) - parser = self.cardAssignments - except ValueError: - parser = self.config - - try: - opt = parser.get(section, key) - except configparser.NoOptionError: - print("No option {} in section {}".format(key, section)) - return -1 - except configparser.NoSectionError: - print("No section {}".format(section)) - return -1 - if "bool" in opt_type.lower(): - return str2bool(opt) - else: - try: - return str2num(opt) - except ValueError: - return opt - - def get_cardsetting(self, cardid, key, opt_type="string"): - """ catches Errors """ - return self.get_setting(cardid, key, opt_type) - - def mpd_init_settings(self): - """ set initial mpd state: - max_volume - initial_volume """ - mpd_status = self.client.status() - max_volume = self.get_setting("phoniebox", "max_volume") - init_volume = self.get_setting("phoniebox", "init_volume") - if max_volume == -1: - max_volume = 100 # the absolute max_volume is 100% - if init_volume == -1: - init_volume = 0 # to be able to compare - if max_volume < init_volume: - self.log("init_volume cannot exceed max_volume.", 2) - init_volume = max_volume # do not exceed max_volume - if mpd_status["volume"] > max_volume: - self.client.setvol(init_volume) - - def set_mpd_playmode(self, cardid): - """ set playmode in mpd according to card settings """ - playmode_defaults_map = {"repeat": 0, "random": 0, "single": 0, "consume": 0} - set_playmode_map = {"repeat": self.client.repeat, - "random": self.client.random, - "single": self.client.single, - "consume": self.client.consume} - for key in set_playmode_map.keys(): - # option is set if config file contains "option = 1" or just "option" without value. - playmode_setting = self.get_cardsetting(cardid, key) - if playmode_setting == -1 or playmode_setting == 1: - playmode_setting = 1 - else: - playmode_setting = playmode_defaults_map[key] - # set value - set_playmode_map[key](playmode_setting) - self.log("setting mpd {} = {}".format(key, playmode_setting), 5) - - def resume(self, cardid, action="resume"): - """ seek to saved position if resume is activated """ - self.mpd_connect_timeout() - mpd_status = self.client.status() - print(mpd_status) - if action in ["resume", "restore"]: - opt_resume = self.get_cardsetting(cardid, "resume") - if opt_resume == -1 or opt_resume == 1: - resume_elapsed = self.get_cardsetting(cardid, "resume_elapsed") - resume_song = self.get_cardsetting(cardid, "resume_song") - if resume_song == -1: - resume_song = 0 - if resume_elapsed != -1 and resume_elapsed != 0: - self.log("{}: resume song {} at time {}s".format(cardid, - self.get_cardsetting(cardid, "resume_song"), - self.get_cardsetting(cardid, "resume_elapsed")), 5) - self.client.seek(resume_song, resume_elapsed) - elif action in ["save", "store"]: - try: - self.log("{}: save state, song {} at time {}s".format(cardid, - mpd_status["song"], mpd_status["elapsed"]), 5) - self.cardAssignments.set(cardid, "resume_elapsed", - mpd_status["elapsed"]) - self.cardAssignments.set(cardid, "resume_song", - mpd_status["song"]) - except KeyError as e: - print("KeyError: {}".format(e)) - except ValueError as e: - print("ValueError: {}".format(e)) - - def read_cardAssignments(self): - card_assignments_file = self.config.get("phoniebox", "card_assignments_file") - parser = ConfigParserExtended(allow_no_value=True) - dataset = parser.read(card_assignments_file) - if len(dataset) != 1: - raise ValueError("Config file {} not found!".format(card_assignments_file)) - return parser - - def update_cardAssignments(self, static_cardAssignments): - """card_assignments_file = self.config.get("phoniebox","card_assignments_file") - parser = ConfigParserExtended(allow_no_value=True) - dataset = parser.read(card_assignments_file) - if len(dataset) != 1: - raise ValueError("Config file {} not found!".format(card_assignments_file)) - # if cardAssignments is still empty, store new cardAssignments directly - # otherwise compare new values with old values and update only certain values - if hasattr(self, 'cardAssignments'): - self.debug("cardAssignments already set, updating data in memory with new data from file {}".format(card_assignments_file)) - static_cardAssignments = parser""" - self.log("Updating changes in cardAssignments from disk.", 3) - keep_cardsettings = ["resume_song", "resume_elapsed"] - common_sections = list(set(static_cardAssignments.sections()).intersection(self.cardAssignments.sections())) - for section in common_sections: - for option in keep_cardsettings: - if self.cardAssignments.has_option(section, option): - value = self.cardAssignments.get(section, option) - static_cardAssignments.set(section, option, value) - self.log("Updating cardid {} with \"{} = {}\".".format(section, option, value), 5) - # finally assign new values - self.cardAssignments = static_cardAssignments - - def read_config(self, configFilePath=defaultconfigFilePath): - """ read config variables from file """ - configParser = ConfigParserExtended(allow_no_value=True, interpolation=configparser.BasicInterpolation()) - dataset = configParser.read(configFilePath) - if len(dataset) != 1: - raise ValueError("Config file {} not found!".format(configFilePath)) - self.config = configParser - - def translate_legacy_cardAssignments(self, last_translate_legacy_cardAssignments=0): - """ reads the card settings data from the old scheme an translates them """ - shortcuts_path = self.get_setting("phoniebox", "shortcuts_path") - audiofolders_path = self.get_setting("phoniebox", "audiofolders_path") - if shortcuts_path != -1: - configParser = ConfigParserExtended() - shortcut_files = [f for f in os.listdir(shortcuts_path) if os.path.isfile(os.path.join(shortcuts_path, f)) and is_int(f)] - - # filename is the cardid - for filename in shortcut_files: - with open(os.path.join(shortcuts_path, filename)) as f: - uri = f.readline().strip().decode('utf-8') - - # add default settings - if filename not in configParser.sections(): - self.log("Adding section {} to cardAssignments".format(filename), 5) - configParser.add_section(filename) - configParser[filename] = self.config["default_cardsettings"] - configParser.set(filename, "cardid", filename) - configParser.set(filename, "uri", uri) - # translate and add folder.conf settings if they contradict default_cardsettings - cardsettings_map = {"CURRENTFILENAME": None, - "ELAPSED": "resume_elapsed", - "PLAYSTATUS": None, - "RESUME": "resume", - "SHUFFLE": "random", - "LOOP": "repeat"} - folderconf = os.path.join(audiofolders_path, uri, "folder.conf") - if os.path.isfile(folderconf) and file_modified(folderconf, last_translate_legacy_cardAssignments): - with open(folderconf) as f: - lines = f.readlines() - cardsettings_old = dict([l.strip().replace('"', '').split("=") for l in lines]) - for key in cardsettings_old.keys(): - if cardsettings_map[key] is not None: - # ignore 0 and OFF values, drop settings that have None in cardsettings_map - if key != "ELAPSED": - if cardsettings_old[key] != "0" and cardsettings_old[key] != "OFF": - configParser.set(filename, cardsettings_map[key], "1") - else: - configParser.set(filename, cardsettings_map[key], "0") - else: - try: - elapsed_val = float(cardsettings_old[key]) - except ValueError: - elaped_val = 0 - configParser.set(filename, cardsettings_map[key], str(elapsed_val)) - return configParser - - def write_new_cardAssignments(self): - """ updates the cardsettings with according to playstate """ - card_assignments_file = self.config.get("phoniebox", "card_assignments_file") - self.log("Write new card assignments to file {}.".format(card_assignments_file), 3) - with codecs.open(card_assignments_file, 'w', 'utf-8') as f: - self.cardAssignments.write(f) - - def print_to_file(self, filename, string): - """ simple function to write a string to a file """ - with codecs.open(filename, 'w', 'utf-8') as f: - f.write(string) - - -if __name__ == "__main__": - print("This module is not to be run! Use \"from Phoniebox import Phoniebox\" instead!") -else: - print("Phoniebox imported. Use \"box = Phoniebox(configFile)\" to get it working.") diff --git a/scripts/python-phoniebox/PhonieboxConfigChanger.py b/scripts/python-phoniebox/PhonieboxConfigChanger.py deleted file mode 100755 index 4c3c9bd46..000000000 --- a/scripts/python-phoniebox/PhonieboxConfigChanger.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# import json -import os, sys, signal -# from mpd import MPDClient -import configparser -# from RawConfigParserExtended import RawConfigParserExtended -from Phoniebox import Phoniebox - -# get absolute path of this script -dir_path = os.path.dirname(os.path.realpath(__file__)) -defaultconfigFilePath = os.path.join(dir_path, './phoniebox.conf') - - -def is_int(s): - """ return True if string is an int """ - try: - int(s) - return True - except ValueError: - return False - - -def str2bool(s): - """ convert string to a python boolean """ - return s.lower() in ("yes", "true", "t", "1") - - -def str2num(s): - """ convert string to an int or a float """ - try: - return int(s) - except ValueError: - return float(s) - - -class PhonieboxConfigChanger(Phoniebox): - - def __init__(self, configFilePath=defaultconfigFilePath): - Phoniebox.__init__(self, configFilePath) - - def assigncard(self, cardid, uri): - section = cardid - # set uri and cardid for card (section = cardid) - if not section in self.cardAssignments.sections(): - self.cardAssignments.add_section(section) - self.cardAssignments.set(section, "cardid", cardid) - self.cardAssignments.set(section, "uri", uri) - # write updated assignments to file - with open(self.config['card_assignments_file'], 'w') as cardAssignmentsFile: - self.cardAssignments.write(cardAssignmentsFile) - - def removecard(self, cardid): - section = cardid - if section in self.cardAssignments.sections(): - self.cardAssignments.remove_section(section) - # write updated assignments to file - with open(self.config['card_assignments_file'], 'w') as f: - self.cardAssignments.write(f) - - def set(self, section, key, value): - try: - num = int(section) - parser = self.cardAssignments - config_file = self.config.get("phoniebox", "card_assignments_file") - except ValueError: - parser = self.config - # update value - try: - parser.set(section, key, value) - self.debug("Set {} = {} in section {}".format(key, value, section)) - except configparser.NoSectionError as e: - raise e - - def get(self, section, t="ini"): - try: - num = int(section) - parser = self.cardAssignments - except ValueError: - parser = self.config - - if t == "json": - print(parser.as_json(section)) - elif t == "dict": - print(parser.as_dict(section)) - else: - print(parser.print_ini(section)) - - def print_usage(self): - print("Usage: {} set ".format(sys.argv[0])) - - -def main(self): - - cmdlist = ["assigncard", "removecard", "set", "get"] - - if len(sys.argv) < 1: - sys.exit() - else: - if sys.argv[1] in cmdlist: - configFilePath = defaultconfigFilePath - cmd = sys.argv[1] - shift = 0 - else: - configFilePath = sys.argv[1] - cmd = sys.argv[2] - shift = 1 - - ConfigChanger = PhonieboxConfigChanger(configFilePath) - try: - if cmd == "assigncard": - cardid = sys.argv[2+shift] - uri = sys.argv[3+shift] - ConfigChanger.assigncard(cardid, uri) - elif cmd == "removecard": - cardid = sys.argv[2+shift] - ConfigChanger.removecard(cardid) - elif cmd == "set": - section = sys.argv[2+shift] - key = sys.argv[3+shift] - value = sys.argv[4+shift] - ConfigChanger.set(section, key, value) - elif cmd == "get": - section = sys.argv[2+shift] - try: - t = sys.argv[3+shift] - except: - t = "ini" - ConfigChanger.get(section, t) - else: - # will never be reached - print("supported commands are {} and {}".format(", ".join(cmdlist[:-1]), cmdlist[-1])) - except: - self.print_usage() - - -if __name__ == "__main__": - main() diff --git a/scripts/python-phoniebox/README.md b/scripts/python-phoniebox/README.md deleted file mode 100644 index 35d1242d2..000000000 --- a/scripts/python-phoniebox/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# python-phoniebox -python daemon to be used with the RPi-Jukebox-RFID by MiczFlor - -https://github.com/MiczFlor/RPi-Jukebox-RFID - - Make sure to edit the defaultconfigFilePath in the file Phoniebox.py to match the path of the phoniebox.conf file. diff --git a/scripts/python-phoniebox/RawConfigParserExtended.py b/scripts/python-phoniebox/RawConfigParserExtended.py deleted file mode 100755 index 49cd28ff9..000000000 --- a/scripts/python-phoniebox/RawConfigParserExtended.py +++ /dev/null @@ -1,31 +0,0 @@ -# from configparser import RawConfigParser -import configparser - - -class RawConfigParserExtended(configparser.RawConfigParser): - - def as_dict(self, section="all"): - if section == "all": - d = self.__dict__['_sections'] - else: - d = self.__dict__['_sections'][section] - return d - - def as_json(self, section="all"): - import json - if section == "all": - d = self.__dict__['_sections'] - else: - d = self.__dict__['_sections'][section] - return json.dumps(d, separators=(',', ':'), indent=4, sort_keys=True, - ensure_ascii=False).encode('utf8') - - def print_ini(self, section="all"): - if section == "all": - sections = self.sections() - else: - sections = [section] - for section_name in sections: - print("[{}]".format(section_name)) - for key, value in self.items(section_name): - print('{} = {}'.format(key, value)) diff --git a/scripts/python-phoniebox/Reader.py b/scripts/python-phoniebox/Reader.py deleted file mode 100755 index 927912f8f..000000000 --- a/scripts/python-phoniebox/Reader.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# This alternative Reader.py script was meant to cover not only USB readers but more. -# It can be used to replace Reader.py if you have readers such as -# MFRC522 or RDM6300. -# Please use the github issue threads to share bugs and improvements -# or create pull requests. - -import os.path -import sys -import serial -import string -import RPi.GPIO as GPIO - -from evdev import InputDevice, categorize, ecodes, list_devices -import pirc522 - - -def get_devices(): - devices = [InputDevice(fn) for fn in list_devices()] - devices.append(NonUsbDevice('MFRC522')) - devices.append(NonUsbDevice('RDM6300')) - return devices - - -class NonUsbDevice(object): - name = None - - def __init__(self, name): - self.name = name - - -class UsbReader(object): - def __init__(self, device): - self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" - self.dev = device - - def readCard(self): - from select import select - stri = '' - key = '' - while key != 'KEY_ENTER': - select([self.dev], [], []) - for event in self.dev.read(): - if event.type == 1 and event.value == 1: - stri += self.keys[event.code] - key = ecodes.KEY[event.code] - return stri[:-1] - - -class Mfrc522Reader(object): - def __init__(self): - self.device = pirc522.RFID() - - def readCard(self): - # Scan for cards - self.device.wait_for_tag() - (error, tag_type) = self.device.request() - - if not error: - print("Card detected.") - # Perform anti-collision detection to find card uid - (error, uid) = self.device.anticoll() - if not error: - return ''.join((str(x) for x in uid)) - - print("No Device ID found.") - return None - - @staticmethod - def cleanup(): - GPIO.cleanup() - - -class Rdm6300Reader: - def __init__(self): - device = '/dev/ttyS0' - baudrate = 9600 - ser_timeout = 0.1 - self.last_card_id = '' - try: - self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) - except serial.SerialException as e: - print(e) - exit(1) - - def readCard(self): - byte_card_id = b'' - - try: - while True: - try: - read_byte = self.rfid_serial.read() - - if read_byte == b'\x02': # start byte - while read_byte != b'\x03': # end bye - read_byte = self.rfid_serial.read() - byte_card_id += read_byte - - card_id = byte_card_id.decode('utf-8') - byte_card_id = '' - card_id = ''.join(x for x in card_id if x in string.printable) - - # Only return UUIDs with correct length - if len(card_id) == 12 and card_id != self.last_card_id: - self.last_card_id = card_id - self.rfid_serial.reset_input_buffer() - return self.last_card_id - - else: # wrong UUID length or already send that UUID last time - self.rfid_serial.reset_input_buffer() - - except ValueError as ve: - print(ve) - - except serial.SerialException as se: - print(se) - - def cleanup(self): - self.rfid_serial.close() - - -class Reader(object): - def __init__(self): - path = os.path.dirname(os.path.realpath(__file__)) - if not os.path.isfile(path + '/deviceName.txt'): - sys.exit('Please run RegisterDevice.py first') - else: - with open(path + '/deviceName.txt', 'r') as f: - device_name = f.read() - - if device_name == 'MFRC522': - self.reader = Mfrc522Reader() - elif device_name == 'RDM6300': - self.reader = Rdm6300Reader() - else: - try: - device = [device for device in get_devices() if device.name == device_name][0] - self.reader = UsbReader(device) - except IndexError: - sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) diff --git a/scripts/python-phoniebox/__init__.py b/scripts/python-phoniebox/__init__.py deleted file mode 100755 index e69de29bb..000000000 diff --git a/scripts/python-phoniebox/deviceName.txt b/scripts/python-phoniebox/deviceName.txt deleted file mode 100755 index a404db7da..000000000 --- a/scripts/python-phoniebox/deviceName.txt +++ /dev/null @@ -1 +0,0 @@ -MFRC522 \ No newline at end of file diff --git a/scripts/python-phoniebox/helpers_unused_atm/__init__.py b/scripts/python-phoniebox/helpers_unused_atm/__init__.py deleted file mode 100755 index e69de29bb..000000000 diff --git a/scripts/python-phoniebox/helpers_unused_atm/helpers.py b/scripts/python-phoniebox/helpers_unused_atm/helpers.py deleted file mode 100755 index 68aef4b68..000000000 --- a/scripts/python-phoniebox/helpers_unused_atm/helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -__name__ = "helpers" - - -def is_int(s): - """ return True if string is an int """ - try: - int(s) - return True - except ValueError: - return False - - -def str2bool(s): - """ convert string to a python boolean """ - return s.lower() in ("yes", "true", "t", "1") - - -def str2num(s): - """ convert string to an int or a float """ - try: - return int(s) - except ValueError: - return float(s) diff --git a/scripts/python-phoniebox/phoniebox.conf b/scripts/python-phoniebox/phoniebox.conf deleted file mode 100755 index d52c9fdbf..000000000 --- a/scripts/python-phoniebox/phoniebox.conf +++ /dev/null @@ -1,62 +0,0 @@ -[phoniebox] -# log level -# 0: no output -# 1: error -# 2: warning -# 3: info (default) -# 4: not used -# 5: debug -log_level = 3 - -# time in seconds to pause detection after swipe (default: 0.5) -debounce_time = 0.5 - -# setup directories -base_path = /home/pi/RPi-Jukebox-RFID/ -audiofolders_path = %(base_path)s/shared/audiofolders -card_assignments_file = %(base_path)s/settings/Card_Assignments.txt -# card detection sound will be played on swipe by aplay (default: none) -card_detection_sound = %(base_path)s/shared/card_detection_sound.wav -# PhonieboxDaemon startup sound will be played by aplay (default: none) -startup_sound = %(base_path)s/shared/startupsound.wav - -# file to log detected card IDs. Required for web interface -Latest_RFID_file = %(base_path)s/shared/latestID.txt - -# use the old-style folder.conf files as card-assignments -# which enables sticking to the legacy-web interface -# resume-settings are not imported, but kept up-to-date by PhonieboxDaemon -translate_legacy_cardassignments = 1 -# the legacy shortcut files are only used to find the legacy cardassignments -shortcuts_path = %(base_path)s/shared/shortcuts/ - -# store card assignments and resume data regularly on disk (default: 30) -store_card_assignments = 30 - -# action for second swipe of the same RFID card. Possible values: -# restart (default), restart_track, stop, pause, skipnext or next, noaudioplay -# note that the combination of "second_swipe = restart" for an RFID card -# with random in the cardsettings behaves similar to "skipnext" with random enabled. -second_swipe = skipnext -# seconds to wait until second swipe is possible (default: 0) -second_swipe_delay = 0 - -# volume settings -init_volume = 65 -max_volume = 80 -# only used by rotary_volume.py -volume_step = 2 - -[mpd] -# mpd connection settings (default: localhost:6600) -host = localhost -port = 6600 -timeout = 5 - -[default_cardsettings] -# default settings for newly registered or translated RFID cards -repeat = 0 -resume = 1 -random = 1 -single = 0 -consume = 0 diff --git a/scripts/resume_play.sh b/scripts/resume_play.sh deleted file mode 100755 index 5482bb3db..000000000 --- a/scripts/resume_play.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/bin/bash - -# This script saves or restores the last position (song and time) in a playlist (=folder) -# Saving and restoring will only be made if a "lastplayed.dat" file is found in the folder where the -# audio is stored. -# Usage: -# Save the position: ./resume_play.sh -c=savepos -# Restore position and play or play from playlist position: ./resume_play-sh -c=resume -v=playlist_pos -# Enable resume for folder: ./resume_play-sh -c=enableresume -v=foldername_in_audiofolders -# Disable resume for folder: ./resume_play-sh -c=disableresume -v=foldername_in_audiofolders -# -# Call this script with "savepos" everytime -# - before you clear the playlist (mpc clear) -# - before you stop the player -# - before you shutdown the Pi (maybe not necessary as mpc stores the position between reboots, but it feels saver) - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -# Read the args passed on by the command line -# see following file for details: -. $PATHDATA/inc.readArgsFromCommandLine.sh - -########################################################### -# Read global configuration file (and create is not exists) -# create the global configuration file from single files - if it does not exist -if [ ! -f $PATHDATA/../settings/global.conf ]; then - . inc.writeGlobalConfig.sh -fi -. $PATHDATA/../settings/global.conf -########################################################### - -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "#START##### SCRIPT resume_play.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "VAR AUDIOFOLDERSPATH: $AUDIOFOLDERSPATH" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "VAR COMMAND: $COMMAND" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "VAR VALUE: $VALUE" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - - -# Some error checking: if folder.conf does not exist, create default -if [ ! -e "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" ] -then - # now we create a default folder.conf file by calling this script - # with the command param createDefaultFolderConf - # (see script for details) - # the $FOLDER would not need to be passed on, because it is already set in this script - # see inc.writeFolderConfig.sh for details - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " - calling inc.writeFolderConfig.sh -c=createDefaultFolderConf -d=\$FOLDER" >> $PATHDATA/../logs/debug.log; fi - . $PATHDATA/inc.writeFolderConfig.sh -c=createDefaultFolderConf -d="$FOLDER" - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " - back from inc.writeFolderConfig.sh" >> $PATHDATA/../logs/debug.log; fi -fi - -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " content of $AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then cat "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " Now doing what COMMAND wants: $COMMAND" >> $PATHDATA/../logs/debug.log; fi - -case "$COMMAND" in - -savepos) - # Get folder name of currently played audio - FOLDER=$(cat $PATHDATA/../settings/Latest_Folder_Played) - # Read the current config file (include will execute == read) - . "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "VAR FOLDER from settings/Latest_Folder_Played: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " savepos FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - # Check if "folder.conf" exists - if [ $RESUME == "ON" ] || [ $SINGLE == "ON" ]; - then - # Get the elapsed time of the currently played audio file from mpd - ELAPSED=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=elapsed: ).*') - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " savepos ELAPSED: $ELAPSED" >> $PATHDATA/../logs/debug.log; fi - # mpd reports an elapsed time only if the audio is playing or is paused. Check if we got an elapsed time - if [ ! -z $ELAPSED ]; # Why does -n not work here? - then - #Get the filename of the currently played audio - CURRENTFILENAME=$(echo -e "currentsong\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=file: ).*') - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " savepos CURRENTFILENAME: $CURRENTFILENAME" >> $PATHDATA/../logs/debug.log; fi - # "Stopped" for signaling -c=resume that there was a stopping event - # (this is done to get a proper resume on the first track if the playlist has ended before) - - # set the vars we need to change - CURRENTFILENAME=$CURRENTFILENAME - ELAPSED=$ELAPSED - PLAYSTATUS="Stopped" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - fi - fi - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then cat "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi - ;; -resume) - # Read the current config file (include will execute == read) - # read vars from folder.conf - . "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " savepos FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " entering: resume with value $RESUME" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " entering: single with value $SINGLE" >> $PATHDATA/../logs/debug.log; fi - # Check if RESUME is switched on - if [ $RESUME == "ON" ] || [ $SINGLE == "ON" ]; - then - # will generate variables: - #CURRENTFILENAME - #ELAPSED - #PLAYSTATUS - - # Check if we got a "savepos" command after the last "resume". Otherwise we assume that the playlist was played until the end. - # In this case, start the playlist from beginning - if [ $PLAYSTATUS == "Stopped" ] - then - # Get the playlist position of the file from mpd - # Alternative approach: "mpc searchplay xx && mpc seek yy" - PLAYLISTPOS=$(echo -e playlistfind filename \"$CURRENTFILENAME\"\\nclose | nc -w 1 localhost 6600 | grep -o -P '(?<=Pos: ).*') - - # If the file is found, it is played from ELAPSED, otherwise start playlist from beginning. If we got a playlist position - # play from that position, not the saved one. - if [ ! -z $PLAYLISTPOS ] && [ -z $VALUE ] ; - then - # doesnt work correctly - # echo -e seek $PLAYLISTPOS $ELAPSED \\nclose | nc -w 1 localhost 6600 - # workaround, see https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/878#issuecomment-672283454 - echo -e "play $PLAYLISTPOS" | nc -w 1 localhost 6600 - echo -e seekcur $ELAPSED \\nclose | nc -w 1 localhost 6600 - else - echo -e "play $VALUE" | nc -w 1 localhost 6600 - fi - # If the playlist ends without any stop/shutdown/new swipe (you've listened to all of the tracks), - # there's no savepos event and we would resume at the last position anywhere in the playlist. - # To catch these, we signal it to the next "resume" call via writing it to folder.conf that - # we still assume that the audio is playing. - # be anything here, as we won't use the information if "Playing" is found by "resume". - - # set the vars we need to change - PLAYSTATUS="Playing" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - - else - # We assume that the playlist ran to the end the last time and start from the beginning. - # Or: playlist is playing and we've got a play from playlist position command. - echo -e "play $VALUE" | nc -w 1 localhost 6600 - fi - else - # if no last played data exists (resume play disabled), we play the playlist from the beginning or the given playlist position - echo -e "play $VALUE" | nc -w 1 localhost 6600 - fi - ;; -enableresume) - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " entering: enableresume" >> $PATHDATA/../logs/debug.log; fi - # set the vars we need to change - RESUME="ON" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - ;; -disableresume) - if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo " entering: disableresume" >> $PATHDATA/../logs/debug.log; fi - # set the vars we need to change - RESUME="OFF" - # now calling a script which will only replace these new vars in folder.conf - - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - ;; -*) - echo "Command unknown", $COMMAND - ;; -esac - -if [ "${DEBUG_resume_play_sh}" == "TRUE" ]; then echo "#END####### SCRIPT resume_play.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi diff --git a/scripts/rfid_trigger_play.sh b/scripts/rfid_trigger_play.sh deleted file mode 100755 index e96902eb7..000000000 --- a/scripts/rfid_trigger_play.sh +++ /dev/null @@ -1,499 +0,0 @@ -#!/bin/bash - -# Reads the card ID or the folder name with audio files -# from the command line (see Usage). -# Then attempts to get the folder name from the card ID -# or play audio folder content directly -# -# Usage for card ID -# ./rfid_trigger_play.sh -i=1234567890 -# or -# ./rfid_trigger_play.sh --cardid=1234567890 -# -# For folder names: -# ./rfid_trigger_play.sh -d='foldername' -# or -# ./rfid_trigger_play.sh --dir='foldername' -# -# or for recursive play of sudfolders -# ./rfid_trigger_play.sh -d='foldername' -v=recursive - -# ADD / EDIT RFID CARDS TO CONTROL THE PHONIEBOX -# All controls are assigned to RFID cards in this -# file: -# settings/rfid_trigger_play.conf -# Please consult this file for more information. -# Do NOT edit anything in this file. - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "########### SCRIPT rfid_trigger_play.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi - -# create the configuration file from sample - if it does not exist -if [ ! -f $PATHDATA/../settings/rfid_trigger_play.conf ]; then - cp $PATHDATA/../settings/rfid_trigger_play.conf.sample $PATHDATA/../settings/rfid_trigger_play.conf - # change the read/write so that later this might also be editable through the web app - sudo chown -R pi:www-data $PATHDATA/../settings/rfid_trigger_play.conf - sudo chmod -R 775 $PATHDATA/../settings/rfid_trigger_play.conf -fi - -########################################################### -# Read global configuration file (and create is not exists) -# create the global configuration file from single files - if it does not exist -if [ ! -f $PATHDATA/../settings/global.conf ]; then - . inc.writeGlobalConfig.sh -fi -. $PATHDATA/../settings/global.conf -########################################################### - -# Read configuration file -. $PATHDATA/../settings/rfid_trigger_play.conf - -# Get args from command line (see Usage above) -# see following file for details: -. $PATHDATA/inc.readArgsFromCommandLine.sh - -################################################################## -# Check if we got the card ID or the audio folder from the prompt. -# Sloppy error check, because we assume the best. -if [ "$CARDID" ]; then - # we got the card ID - # If you want to see the CARDID printed, uncomment the following line - # echo CARDID = $CARDID - - # Add info into the log, making it easer to monitor cards - echo "Card ID '$CARDID' was used at '$NOW'." > $PATHDATA/../shared/latestID.txt - echo "$CARDID" > $PATHDATA/../settings/Latest_RFID - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "Card ID '$CARDID' was used" >> $PATHDATA/../logs/debug.log; fi - - # If the input is of 'special' use, don't treat it like a trigger to play audio. - # Special uses are for example volume changes, skipping, muting sound. - - case $CARDID in - $CMDSHUFFLE) - # toggles shuffle mode (random on/off) - $PATHDATA/playout_controls.sh -c=playershuffle - ;; - $CMDMAXVOL30) - # limit volume to 30% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=30 - ;; - $CMDMAXVOL50) - # limit volume to 50% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=50 - ;; - $CMDMAXVOL75) - # limit volume to 75% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=75 - ;; - $CMDMAXVOL80) - # limit volume to 80% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=80 - ;; - $CMDMAXVOL85) - # limit volume to 85% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=85 - ;; - $CMDMAXVOL90) - # limit volume to 90% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=90 - ;; - $CMDMAXVOL95) - # limit volume to 95% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=95 - ;; - $CMDMAXVOL100) - # limit volume to 100% - $PATHDATA/playout_controls.sh -c=setmaxvolume -v=100 - ;; - $CMDMUTE) - # amixer sset 'PCM' 0% - $PATHDATA/playout_controls.sh -c=mute - ;; - $CMDVOL30) - # amixer sset 'PCM' 30% - $PATHDATA/playout_controls.sh -c=setvolume -v=30 - ;; - $CMDVOL50) - # amixer sset 'PCM' 50% - $PATHDATA/playout_controls.sh -c=setvolume -v=50 - ;; - $CMDVOL75) - # amixer sset 'PCM' 75% - $PATHDATA/playout_controls.sh -c=setvolume -v=75 - ;; - $CMDVOL80) - # amixer sset 'PCM' 80% - $PATHDATA/playout_controls.sh -c=setvolume -v=80 - ;; - $CMDVOL85) - # amixer sset 'PCM' 85% - $PATHDATA/playout_controls.sh -c=setvolume -v=85 - ;; - $CMDVOL90) - # amixer sset 'PCM' 90% - $PATHDATA/playout_controls.sh -c=setvolume -v=90 - ;; - $CMDVOL95) - # amixer sset 'PCM' 95% - $PATHDATA/playout_controls.sh -c=setvolume -v=95 - ;; - $CMDVOL100) - # amixer sset 'PCM' 100% - $PATHDATA/playout_controls.sh -c=setvolume -v=100 - ;; - $CMDVOLUP) - # increase volume by x% set in Audio_Volume_Change_Step - $PATHDATA/playout_controls.sh -c=volumeup - ;; - $CMDVOLDOWN) - # decrease volume by x% set in Audio_Volume_Change_Step - $PATHDATA/playout_controls.sh -c=volumedown - ;; - $CMDSTOP) - # kill all running audio players - $PATHDATA/playout_controls.sh -c=playerstop - ;; - $CMDSHUTDOWN) - # shutdown the RPi nicely - # sudo halt - $PATHDATA/playout_controls.sh -c=shutdown - ;; - $CMDREBOOT) - # shutdown the RPi nicely - # sudo reboot - $PATHDATA/playout_controls.sh -c=reboot - ;; - $CMDNEXT) - # play next track in playlist - $PATHDATA/playout_controls.sh -c=playernext - ;; - $CMDPREV) - # play previous track in playlist - # echo "prev" | nc.openbsd -w 1 localhost 4212 - sudo $PATHDATA/playout_controls.sh -c=playerprev - #/usr/bin/sudo /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=playerprev - ;; - $CMDREWIND) - # play the first track in playlist - sudo $PATHDATA/playout_controls.sh -c=playerrewind - ;; - $CMDSEEKFORW) - # jump 15 seconds ahead - $PATHDATA/playout_controls.sh -c=playerseek -v=+15 - ;; - $CMDSEEKBACK) - # jump 15 seconds back - $PATHDATA/playout_controls.sh -c=playerseek -v=-15 - ;; - $CMDPAUSE) - # pause current track - # echo "pause" | nc.openbsd -w 1 localhost 4212 - $PATHDATA/playout_controls.sh -c=playerpause - ;; - $CMDPLAY) - # play / resume current track - # echo "play" | nc.openbsd -w 1 localhost 4212 - $PATHDATA/playout_controls.sh -c=playerplay - ;; - $STOPAFTER5) - # stop player after -v minutes - $PATHDATA/playout_controls.sh -c=playerstopafter -v=5 - ;; - $STOPAFTER15) - # stop player after -v minutes - $PATHDATA/playout_controls.sh -c=playerstopafter -v=15 - ;; - $STOPAFTER30) - # stop player after -v minutes - $PATHDATA/playout_controls.sh -c=playerstopafter -v=30 - ;; - $STOPAFTER60) - # stop player after -v minutes - $PATHDATA/playout_controls.sh -c=playerstopafter -v=60 - ;; - $SHUTDOWNAFTER5) - # shutdown after -v minutes - $PATHDATA/playout_controls.sh -c=shutdownafter -v=5 - ;; - $SHUTDOWNAFTER15) - # shutdown after -v minutes - $PATHDATA/playout_controls.sh -c=shutdownafter -v=15 - ;; - $SHUTDOWNAFTER30) - # shutdown after -v minutes - $PATHDATA/playout_controls.sh -c=shutdownafter -v=30 - ;; - $SHUTDOWNAFTER60) - # shutdown after -v minutes - $PATHDATA/playout_controls.sh -c=shutdownafter -v=60 - ;; - $SHUTDOWNVOLUMEREDUCTION10) - # reduce volume until shutdown in -v minutes - $PATHDATA/playout_controls.sh -c=shutdownvolumereduction -v=10 - ;; - $SHUTDOWNVOLUMEREDUCTION15) - # reduce volume until shutdown in -v minutes - $PATHDATA/playout_controls.sh -c=shutdownvolumereduction -v=15 - ;; - $SHUTDOWNVOLUMEREDUCTION30) - # reduce volume until shutdown in -v minutes - $PATHDATA/playout_controls.sh -c=shutdownvolumereduction -v=30 - ;; - $SHUTDOWNVOLUMEREDUCTION60) - # reduce volume until shutdown in -v minutes - $PATHDATA/playout_controls.sh -c=shutdownvolumereduction -v=60 - ;; - $ENABLEWIFI) - $PATHDATA/playout_controls.sh -c=enablewifi - ;; - $DISABLEWIFI) - $PATHDATA/playout_controls.sh -c=disablewifi - ;; - $TOGGLEWIFI) - $PATHDATA/playout_controls.sh -c=togglewifi - ;; - $CMDPLAYCUSTOMPLS) - $PATHDATA/playout_controls.sh -c=playlistaddplay -v="PhonieCustomPLS" -d="PhonieCustomPLS" - ;; - $RECORDSTART600) - #start recorder for -v seconds - $PATHDATA/playout_controls.sh -c=recordstart -v=600 - ;; - $RECORDSTART60) - #start recorder for -v seconds - $PATHDATA/playout_controls.sh -c=recordstart -v=60 - ;; - $RECORDSTART10) - #start recorder for -v seconds - $PATHDATA/playout_controls.sh -c=recordstart -v=10 - ;; - $RECORDSTOP) - $PATHDATA/playout_controls.sh -c=recordstop - ;; - $RECORDPLAYBACKLATEST) - $PATHDATA/playout_controls.sh -c=recordplaylatest - ;; - $CMDREADWIFIIP) - $PATHDATA/playout_controls.sh -c=readwifiipoverspeaker - ;; - *) - - # We checked if the card was a special command, seems it wasn't. - # Now we expect it to be a trigger for one or more audio file(s). - # Let's look at the ID, write a bit of log information and then try to play audio. - - # Look for human readable shortcut in folder 'shortcuts' - # check if CARDID has a text file by the same name - which would contain the human readable folder name - if [ -f $PATHDATA/../shared/shortcuts/$CARDID ] - then - # Read human readable shortcut from file - FOLDER=`cat $PATHDATA/../shared/shortcuts/$CARDID` - # Add info into the log, making it easer to monitor cards - echo "This ID has been used before." >> $PATHDATA/../shared/latestID.txt - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "This ID has been used before." >> $PATHDATA/../logs/debug.log; fi - else - # Human readable shortcut does not exists, so create one with the content $CARDID - # this file can later be edited manually over the samba network - echo "$CARDID" > $PATHDATA/../shared/shortcuts/$CARDID - FOLDER=$CARDID - # Add info into the log, making it easer to monitor cards - echo "This ID was used for the first time." >> $PATHDATA/../shared/latestID.txt - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "This ID was used for the first time." >> $PATHDATA/../logs/debug.log; fi - fi - # Add info into the log, making it easer to monitor cards - echo "The shortcut points to audiofolder '$FOLDER'." >> $PATHDATA/../shared/latestID.txt - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "The shortcut points to audiofolder '$FOLDER'." >> $PATHDATA/../logs/debug.log; fi - ;; - esac -fi - -############################################################## -# We should now have a folder name with the audio files. -# Either from prompt of from the card ID processing above -# Sloppy error check, because we assume the best. - -if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "# Attempting to play: $AUDIOFOLDERSPATH/$FOLDER" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "# Type of play \$VALUE: $VALUE" >> $PATHDATA/../logs/debug.log; fi - -# check if -# - $FOLDER is not empty (! -z "$FOLDER") -# - AND (-a) -# - $FOLDER is set (! -z ${FOLDER+x}) -# - AND (-a) -# - and points to existing directory (-d "${AUDIOFOLDERSPATH}/${FOLDER}") -if [ ! -z "$FOLDER" -a ! -z ${FOLDER+x} -a -d "${AUDIOFOLDERSPATH}/${FOLDER}" ]; then - - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "\$FOLDER set, not empty and dir exists: ${AUDIOFOLDERSPATH}/${FOLDER}" >> $PATHDATA/../logs/debug.log; fi - - # if we play a folder the first time, add some sensible information to the folder.conf - if [ ! -f "${AUDIOFOLDERSPATH}/${FOLDER}/folder.conf" ]; then - # now we create a default folder.conf file by calling this script - # with the command param createDefaultFolderConf - # (see script for details) - # the $FOLDER would not need to be passed on, because it is already set in this script - # see inc.writeFolderConfig.sh for details - . $PATHDATA/inc.writeFolderConfig.sh -c=createDefaultFolderConf -d="${FOLDER}" - fi - - # get the name of the last folder played. As mpd doesn't store the name of the last - # playlist, we have to keep track of it via the Latest_Folder_Played file - LASTFOLDER=$(cat $PATHDATA/../settings/Latest_Folder_Played) - LASTPLAYLIST=$(cat $PATHDATA/../settings/Latest_Playlist_Played) - # this might need to go? resume not working... echo ${FOLDER} > $PATHDATA/../settings/Latest_Folder_Played - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Var \$LASTFOLDER: $LASTFOLDER" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Var \$LASTPLAYLIST: $LASTPLAYLIST" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "Checking 'recursive' list? VAR \$VALUE: $VALUE" >> $PATHDATA/../logs/debug.log; fi - - if [ "$VALUE" == "recursive" ]; then - # set path to playlist - # replace subfolder slashes with " % " - PLAYLISTPATH="${PLAYLISTSFOLDERPATH}/${FOLDER//\//\ %\ }-%RCRSV%.m3u" - PLAYLISTNAME="${FOLDER//\//\ %\ }-%RCRSV%" - $PATHDATA/playlist_recursive_by_folder.php --folder "${FOLDER}" --list 'recursive' > "${PLAYLISTPATH}" - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "recursive? YES" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "$PATHDATA/playlist_recursive_by_folder.php --folder \"${FOLDER}\" --list 'recursive' > \"${PLAYLISTPATH}\"" >> $PATHDATA/../logs/debug.log; fi - else - # set path to playlist - # replace subfolder slashes with " % " - PLAYLISTPATH="${PLAYLISTSFOLDERPATH}/${FOLDER//\//\ %\ }.m3u" - PLAYLISTNAME="${FOLDER//\//\ %\ }" - $PATHDATA/playlist_recursive_by_folder.php --folder "${FOLDER}" > "${PLAYLISTPATH}" - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "recursive? NO" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "$PATHDATA/playlist_recursive_by_folder.php --folder \"${FOLDER}\" > \"${PLAYLISTPATH}\"" >> $PATHDATA/../logs/debug.log; fi - fi - - # Second Swipe value - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Var \$SECONDSWIPE: ${SECONDSWIPE}" >> $PATHDATA/../logs/debug.log; fi - # Playlist name - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Var \$PLAYLISTNAME: ${PLAYLISTNAME}" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Var \$LASTPLAYLIST: ${LASTPLAYLIST}" >> $PATHDATA/../logs/debug.log; fi - - # Setting a VAR to start "play playlist from start" - # This will be changed in the following checks "if this is the second swipe" - PLAYPLAYLIST=yes - - # Check if the second swipe happened - # - The same playlist is cued up ("$LASTPLAYLIST" == "$PLAYLISTNAME") - if [ "$LASTPLAYLIST" == "$PLAYLISTNAME" ] - then - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Second Swipe DID happen: \$LASTPLAYLIST == \$PLAYLISTNAME" >> $PATHDATA/../logs/debug.log; fi - - # check if - # - $SECONDSWIPE is set to toggle pause/play ("$SECONDSWIPE" == "PAUSE") - # - AND (-a) - # - check the length of the playlist, if =0 then it was cleared before, a state, which should only - # be possible after a reboot ($PLLENGTH -gt 0) - PLLENGTH=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=playlistlength: ).*') - if [ $PLLENGTH -eq 0 ] - then - # after a reboot we want to play the playlist once no matter what the setting is - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Take second wipe as first after fresh boot" >> $PATHDATA/../logs/debug.log; fi - elif [ "$SECONDSWIPE" == "PAUSE" -a $PLLENGTH -gt 0 ] - then - # The following involves NOT playing the playlist, so we set: - PLAYPLAYLIST=no - - STATE=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=state: ).*') - if [ $STATE == "play" ] - then - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " MPD playing, pausing the player" >> $PATHDATA/../logs/debug.log; fi - sudo $PATHDATA/playout_controls.sh -c=playerpause &>/dev/null - else - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "MPD not playing, start playing" >> $PATHDATA/../logs/debug.log; fi - sudo $PATHDATA/playout_controls.sh -c=playerplay &>/dev/null - fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Completed: toggle pause/play" >> $PATHDATA/../logs/debug.log; fi - elif [ "$SECONDSWIPE" == "PLAY" -a $PLLENGTH -gt 0 ] - then - # The following involves NOT playing the playlist, so we set: - PLAYPLAYLIST=no - sudo $PATHDATA/playout_controls.sh -c=playerplay &>/dev/null - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Completed: Resume playback" >> $PATHDATA/../logs/debug.log; fi - elif [ "$SECONDSWIPE" == "NOAUDIOPLAY" ] - then - # The following involves NOT playing the playlist, so we set: - PLAYPLAYLIST=no - # following needs testing (see https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/914) - # Special case for NOAUDIOPLAY because once the playlist has finished, - # it needs to be noted by the system that the second swipe is like a *first* swipe. - currentSong=`mpc current` - if [[ -z "$currentSong" ]]; then - #end of playlist (EOPL) reached. Ignore last played playlist - PLAYPLAYLIST=yes - fi - - # "$SECONDSWIPE" == "NOAUDIOPLAY" - # "$LASTPLAYLIST" == "$PLAYLISTNAME" => same playlist triggered again - # => do nothing - # echo "do nothing" > /dev/null 2>&1 - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Completed: do nothing" >> $PATHDATA/../logs/debug.log; fi - elif [ "$SECONDSWIPE" == "SKIPNEXT" ] - then - # We will not play the playlist but skip to the next track: - PLAYPLAYLIST=skipnext - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Completed: skip next track" >> $PATHDATA/../logs/debug.log; fi - fi - fi - # now we check if we are still on for playing what we got passed on: - if [ "$PLAYPLAYLIST" == "yes" ] - then - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "We must play the playlist no matter what: \$PLAYPLAYLIST == yes" >> $PATHDATA/../logs/debug.log; fi - - # Above we already checked if the folder exists -d "$AUDIOFOLDERSPATH/$FOLDER" - # - # the process is as such - because of the recursive play option: - # - each folder can be played. - # - a single folder will create a playlist with the same name as the folder - # - because folders can live inside other folders, the relative path might contain - # slashes (e.g. audiobooks/Moby Dick/) - # - because slashes can not be in the playlist name, slashes are replaced with " % " - # - the "recursive" option means that the content of the folder AND all subfolders - # is being played - # - in this case, the playlist is related to the same folder name, which means we need - # to make a different name for "recursive" playout - # - a recursive playlist has the suffix " %RCRSV%" - keeping it cryptic to avoid clashes - # with a possible "real" name for a folder - # - with this new logic, there are no more SPECIALFORMAT playlists. Live streams and podcasts - # are now all unfolded into the playlist - # - creating the playlist is now done in the php script with parameters: - # $PATHDATA/playlist_recursive_by_folder.php --folder "${FOLDER}" --list 'recursive' - - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " VAR PLAYLISTPATH: $PLAYLISTPATH" >> $PATHDATA/../logs/debug.log; fi - - # save position of current playing list "stop" - $PATHDATA/playout_controls.sh -c=playerstop - # play playlist - # the variable passed on to play is the playlist name -v (NOT the folder name) - # because (see above) a folder can be played recursively (including subfolders) or flat (only containing files) - # load new playlist and play - $PATHDATA/playout_controls.sh -c=playlistaddplay -v="${PLAYLISTNAME}" -d="${FOLDER}" - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Command: $PATHDATA/playout_controls.sh -c=playlistaddplay -v=\"${PLAYLISTNAME}\" -d=\"${FOLDER}\"" >> $PATHDATA/../logs/debug.log; fi - # save latest playlist not to file - sudo echo ${PLAYLISTNAME} > $PATHDATA/../settings/Latest_Playlist_Played - sudo chown pi:www-data $PATHDATA/../settings/Latest_Playlist_Played - sudo chmod 777 $PATHDATA/../settings/Latest_Playlist_Played - fi - if [ "$PLAYPLAYLIST" == "skipnext" ] - then - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "Skip to the next track in the playlist: \$PLAYPLAYLIST == skipnext" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " VAR PLAYLISTPATH: $PLAYLISTPATH" >> $PATHDATA/../logs/debug.log; fi - - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo " Command: $PATHDATA/playout_controls.sh -c=playernext" >> $PATHDATA/../logs/debug.log; fi - $PATHDATA/playout_controls.sh -c=playernext - fi -else - if [ "${DEBUG_rfid_trigger_play_sh}" == "TRUE" ]; then echo "Path not found $AUDIOFOLDERSPATH/$FOLDER" >> $PATHDATA/../logs/debug.log; fi -fi diff --git a/scripts/shuffle_play.sh b/scripts/shuffle_play.sh deleted file mode 100755 index c0b80d067..000000000 --- a/scripts/shuffle_play.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash - -# This script saves or restores the SHUFFLE status in a playlist (=folder) and enables/disables shuffle mode according to the folder.conf of the current folder/playlist -# Usage: -# Enable shuffle for folder: ./shuffle_play-sh -c=enableshuffle -v=foldername_in_audiofolders -# Disable resume for folder: ./shuffle_play-sh -c=disableshuffle -v=foldername_in_audiofolders -# -# TODO: When to call this script? -# Call this script with "playlistaddplay" (playout_controls.sh) everytime - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -# Read the args passed on by the command line -# see following file for details: -. $PATHDATA/inc.readArgsFromCommandLine.sh - -# path to audio folders -AUDIOFOLDERSPATH=`cat $PATHDATA/../settings/Audio_Folders_Path` - -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "## SCRIPT shuffle_play.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "VAR AUDIOFOLDERSPATH: $AUDIOFOLDERSPATH" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "VAR COMMAND: $COMMAND" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "VAR VALUE: $VALUE" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - -# Get folder name of currently played audio by extracting the playlist name -# ONLY if none was passed on. The "pass on" is needed to save position -# when starting a new playlist while an old is playing. In this case -# mpc lsplaylists will get confused because it has more than one. -# check if $FOLDER is empty / unset -if [ -z "$FOLDER" ] -then - # this is old: FOLDER=$(mpc lsplaylists) - # actually, this should be the latest folder: - FOLDER=$(cat $PATHDATA/../settings/Latest_Folder_Played) -fi - -# Some error checking: if folder.conf does not exist, create default -if [ ! -e "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" ] -then - # now we create a default folder.conf file by calling this script - # with the command param createDefaultFolderConf - # (see script for details) - # the $FOLDER would not need to be passed on, because it is already set in this script - # see inc.writeFolderConfig.sh for details - if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " - calling inc.writeFolderConfig.sh -c=createDefaultFolderConf -d=\$FOLDER" >> $PATHDATA/../logs/debug.log; fi - . $PATHDATA/inc.writeFolderConfig.sh -c=createDefaultFolderConf -d="$FOLDER" - if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " - back from inc.writeFolderConfig.sh" >> $PATHDATA/../logs/debug.log; fi -fi -# Read the current config file (include will execute == read) -. "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "# found content in folder.conf:" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then cat "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi - -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " Now doing what COMMAND wants: $COMMAND" >> $PATHDATA/../logs/debug.log; fi - -case "$COMMAND" in - -shuffle_check) - #Check if SHUFFLE is switched on. As this is called for each playlist change, it will overwrite temporary shuffle mode - if [ $SHUFFLE == "ON" ]; - then - if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " entering: shuffle_check with value $SHUFFLE" >> $PATHDATA/../logs/debug.log; fi - mpc shuffle - else - if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " entering: shuffle_check with value $SHUFFLE" >> $PATHDATA/../logs/debug.log; fi - mpc random off - fi - ;; -enableshuffle) - if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " entering: enableshuffle" >> $PATHDATA/../logs/debug.log; fi - # set the vars we need to change - SHUFFLE="ON" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - ;; -disableshuffle) - if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo " entering: disableshuffle" >> $PATHDATA/../logs/debug.log; fi - # set the vars we need to change - SHUFFLE="OFF" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - ;; - - -*) - echo "Command unknown" - ;; -esac - -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then echo "# new content in folder.conf:" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_shuffle_play_sh}" == "TRUE" ]; then cat "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi diff --git a/scripts/single_play.sh b/scripts/single_play.sh deleted file mode 100755 index be5cfbd67..000000000 --- a/scripts/single_play.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -# This script saves or restores the SINGLE status in a playlist (=folder) and enables/disables -# single mode according to the folder.conf of the current folder/playlist -# Usage: -# Enable single mode for folder: ./single_play-sh -c=enablesingle -v=foldername_in_audiofolders -# Disable single mode for folder: ./single_play-sh -c=disablesingle -v=foldername_in_audiofolders -# -# TODO: When to call this script? -# Call this script with "playlistaddplay" (playout_controls.sh) everytime - -# Set the date and time of now -NOW=`date +%Y-%m-%d.%H:%M:%S` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -############################################################# -# $DEBUG TRUE|FALSE -# Read debug logging configuration file -. $PATHDATA/../settings/debugLogging.conf - -# Get args from command line (see Usage above) -# see following file for details: -. $PATHDATA/inc.readArgsFromCommandLine.sh - -# path to audio folders -AUDIOFOLDERSPATH=`cat $PATHDATA/../settings/Audio_Folders_Path` - -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo "## SCRIPT single_play.sh ($NOW) ##" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo "VAR AUDIOFOLDERSPATH: $AUDIOFOLDERSPATH" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo "VAR COMMAND: $COMMAND" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo "VAR VALUE: $VALUE" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo "VAR FOLDER: $FOLDER" >> $PATHDATA/../logs/debug.log; fi - -# Get folder name of currently played audio by extracting the playlist name -# ONLY if none was passed on. The "pass on" is needed to save position -# when starting a new playlist while an old is playing. In this case -# mpc lsplaylists will get confused because it has more than one. -# check if $FOLDER is empty / unset -if [ -z "$FOLDER" ] -then - # actually, this should be the latest folder: - FOLDER=$(cat $PATHDATA/../settings/Latest_Folder_Played) -fi - -# Some error checking: if folder.conf does not exist, create default -if [ ! -e "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" ] -then - # now we create a default folder.conf file by calling this script - # with the command param createDefaultFolderConf - # (see script for details) - # the $FOLDER would not need to be passed on, because it is already set in this script - # see inc.writeFolderConfig.sh for details - if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " - calling inc.writeFolderConfig.sh -c=createDefaultFolderConf -d=\$FOLDER" >> $PATHDATA/../logs/debug.log; fi - . $PATHDATA/inc.writeFolderConfig.sh -c=createDefaultFolderConf -d="$FOLDER" - if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " - back from inc.writeFolderConfig.sh" >> $PATHDATA/../logs/debug.log; fi -fi -# Read the current config file (include will execute == read) -. "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " content of $AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then cat "$AUDIOFOLDERSPATH/$FOLDER/folder.conf" >> $PATHDATA/../logs/debug.log; fi - -if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " Now doing what COMMAND wants: $COMMAND" >> $PATHDATA/../logs/debug.log; fi - -case "$COMMAND" in - -single_check) - #Check if SINGLE is switched on. As this is called for each playlist change, it will overwrite temporary shuffle mode - if [ $SINGLE == "ON" ] - then - if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " entering: single_check with value $SINGLE" >> $PATHDATA/../logs/debug.log; fi - mpc single on - else - if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " entering: single_check with value $SINGLE" >> $PATHDATA/../logs/debug.log; fi - mpc single off - fi - ;; -singleenable) - if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " entering: singleenable" >> $PATHDATA/../logs/debug.log; fi - # set the vars we need to change - SINGLE="ON" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - ;; -singledisable) - if [ "${DEBUG_single_play_sh}" == "TRUE" ]; then echo " entering: singledisable" >> $PATHDATA/../logs/debug.log; fi - # set the vars we need to change - SINGLE="OFF" - # now calling a script which will only replace these new vars in folder.conf - # (see script for details) - . $PATHDATA/inc.writeFolderConfig.sh - ;; - - -*) - echo "Command unknown" - ;; -esac diff --git a/scripts/startup-scripts.sh b/scripts/startup-scripts.sh deleted file mode 100755 index 6de44f52b..000000000 --- a/scripts/startup-scripts.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -start_time=`date +%s` - -# The absolute path to the folder whjch contains all the scripts. -# Unless you are working with symlinks, leave the following line untouched. -PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -########################################################### -# Read global configuration file (and create is not exists) -# create the global configuration file from single files - if it does not exist -if [ ! -f $PATHDATA/../settings/global.conf ]; then - . /home/pi/RPi-Jukebox-RFID/scripts/inc.writeGlobalConfig.sh -fi -. $PATHDATA/../settings/global.conf -########################################################### -echo "Phoniebox is starting...$(expr `date +%s` - $start_time) s" - -#cat $PATHDATA/../settings/version-number - -#cat $PATHDATA/../settings/global.conf - -#echo "${AUDIOVOLSTARTUP} is the mpd startup volume $(expr `date +%s` - $start_time) s" - -#################################### -# make playists, files and folders -# and shortcuts -# readable and writable to all -#sudo chmod -R 777 ${AUDIOFOLDERSPATH} -#sudo chmod -R 777 ${PLAYLISTSFOLDERPATH} -#sudo chmod -R 777 $PATHDATA/../shared/shortcuts - - -#echo "before mpd status $(expr `date +%s` - $start_time) s" - -######################################### -# wait until mopidy/MPD server is running -STATUS=0 -while [ "$STATUS" != "ACTIVE" ]; do STATUS=$(echo -e status\\nclose | nc -w 0 localhost 6600 | grep 'OK MPD'| sed 's/^.*$/ACTIVE/'); done - - -#echo "before playout $(expr `date +%s` - $start_time) s" - -#################################### -# check if and set volume on startup -#/home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=setvolumetostartup - - -#echo "after vol $(expr `date +%s` - $start_time) s" - -#################### -# play startup sound -#mpgvolume=$((32768*${AUDIOVOLSTARTUP}/100)) -#echo "${mpgvolume} is the mpg123 startup volume" -#/usr/bin/mpg123 -f -${mpgvolume} /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 - -####################### -# re-scan music library -#mpc rescan - -####################### -# read out wifi config? -#if [ "${READWLANIPYN}" == "ON" ]; then -# /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=readwifiipoverspeaker -#fi diff --git a/scripts/startup_sound.sh b/scripts/startup_sound.sh deleted file mode 100755 index 3ca5da787..000000000 --- a/scripts/startup_sound.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -#sleep 1.5 - -#################### -# play startup sound -mpgvolume=$((32768*50/100)) -echo "${mpgvolume} is the mpg123 startup volume" -/usr/bin/mpg123 -f -${mpgvolume} /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 - -####################### diff --git a/scripts/test/mockedGPIO.py b/scripts/test/mockedGPIO.py deleted file mode 100644 index e4ff6a153..000000000 --- a/scripts/test/mockedGPIO.py +++ /dev/null @@ -1,17 +0,0 @@ -from mock import MagicMock, patch - -MockRPi = MagicMock() -modules = { - "RPi": MockRPi, - "RPi.GPIO": MockRPi.GPIO, -} - -MockRPi.GPIO.RISING = 31 -MockRPi.GPIO.FALLING = 32 -MockRPi.GPIO.BOTH = 33 -MockRPi.GPIO.HIGH = 1 -MockRPi.GPIO.LOW = 0 -patcher = patch.dict("sys.modules", modules) -patcher.start() -import RPi.GPIO -GPIO = RPi.GPIO diff --git a/scripts/test/test_TwoButtonControl.py b/scripts/test/test_TwoButtonControl.py deleted file mode 100644 index d59125cc6..000000000 --- a/scripts/test/test_TwoButtonControl.py +++ /dev/null @@ -1,157 +0,0 @@ -import mock -import pytest -from mock import MagicMock -from test.mockedGPIO import GPIO - - -import TwoButtonControl - - -@pytest.fixture -def btn1Mock(): - return mock.MagicMock() - - -@pytest.fixture -def btn2Mock(): - return mock.MagicMock() - - -@pytest.fixture -def functionCall1Mock(): - return mock.MagicMock() - - -@pytest.fixture -def functionCall2Mock(): - return mock.MagicMock() - - -@pytest.fixture -def functionCallBothPressedMock(): - return mock.MagicMock() - - -def test_functionCallTwoButtonsOnlyBtn1Pressed(btn1Mock, - btn2Mock, - functionCall1Mock, - functionCall2Mock, - functionCallBothPressedMock): - btn1Mock.is_pressed = True - btn2Mock.is_pressed = False - func = TwoButtonControl.functionCallTwoButtons(btn1Mock, - btn2Mock, - functionCall1Mock, - functionCall2Mock, - functionCallBothPressed=functionCallBothPressedMock) - func() - functionCall1Mock.assert_called_once_with() - functionCall2Mock.assert_not_called() - functionCallBothPressedMock.assert_not_called() - - -def test_functionCallTwoButtonsBothBtnsPressedFunctionCallBothPressedExists(btn1Mock, - btn2Mock, - functionCall1Mock, - functionCall2Mock, - functionCallBothPressedMock): - btn1Mock.is_pressed = True - btn2Mock.is_pressed = True - func = TwoButtonControl.functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, - functionCallBothPressed=functionCallBothPressedMock) - func() - functionCall1Mock.assert_not_called() - functionCall2Mock.assert_not_called() - functionCallBothPressedMock.assert_called_once_with() - - -def test_functionCallTwoButtonsBothBtnsPressedFunctionCallBothPressedIsNone(btn1Mock, - btn2Mock, - functionCall1Mock, - functionCall2Mock): - btn1Mock.is_pressed = True - btn2Mock.is_pressed = True - func = TwoButtonControl.functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, - functionCallBothPressed=None) - func() - functionCall1Mock.assert_not_called() - functionCall2Mock.assert_not_called() - - -mockedFunction1 = MagicMock() -mockedFunction2 = MagicMock() -mockedFunction3 = MagicMock() - - -@pytest.fixture -def two_button_control(): - mockedFunction1.reset_mock() - mockedFunction2.reset_mock() - mockedFunction3.reset_mock() - return TwoButtonControl.TwoButtonControl(bcmPin1=1, - bcmPin2=2, - functionCallBtn1=mockedFunction1, - functionCallBtn2=mockedFunction2, - functionCallTwoBtns=mockedFunction3, - pull_up=True, - hold_repeat=False, - hold_time=0.3, - name='TwoButtonControl') - - -class TestTwoButtonControl: - def test_init(self): - TwoButtonControl.TwoButtonControl(bcmPin1=1, - bcmPin2=2, - functionCallBtn1=mockedFunction1, - functionCallBtn2=mockedFunction2, - functionCallTwoBtns=mockedFunction3, - pull_up=True, - hold_repeat=False, - hold_time=0.3, - name='TwoButtonControl') - - def test_btn1_pressed(self, two_button_control): - pinA = two_button_control.bcmPin1 - pinB = two_button_control.bcmPin2 - - def func(pin): - values = {pinA: False, pinB: True} - if pin in values: - return values[pin] - else: - print('Cannot find pin {} in values: {}'.format(pin, values)) - return None - TwoButtonControl.GPIO.input.side_effect = func - two_button_control.action() - mockedFunction1.assert_called_once() - mockedFunction2.assert_not_called() - mockedFunction3.assert_not_called() - two_button_control.action() - assert mockedFunction1.call_count == 2 - - def test_btn2_pressed(self, two_button_control): - pinA = two_button_control.bcmPin1 - pinB = two_button_control.bcmPin2 - TwoButtonControl.GPIO.input.side_effect = lambda pin: {pinA: True, pinB: False}[pin] - two_button_control.action() - mockedFunction1.assert_not_called() - mockedFunction2.assert_called_once() - mockedFunction3.assert_not_called() - two_button_control.action() - assert mockedFunction2.call_count == 2 - - def test_btn1_and_btn2_pressed(self, two_button_control): - pinA = two_button_control.bcmPin1 - pinB = two_button_control.bcmPin2 - TwoButtonControl.GPIO.input.side_effect = lambda pin: {pinA: False, pinB: False}[pin] - two_button_control.action() - mockedFunction1.assert_not_called() - mockedFunction2.assert_not_called() - mockedFunction3.assert_called_once() - two_button_control.action() - assert mockedFunction3.call_count == 2 - - def test_repr(self, two_button_control): - expected = "" - assert repr(two_button_control) == expected diff --git a/scripts/test_zmq.py b/scripts/test_zmq.py deleted file mode 100644 index c79df0614..000000000 --- a/scripts/test_zmq.py +++ /dev/null @@ -1,160 +0,0 @@ -import nanotime -import zmq -import json -import time - -import alsaaudio -from mpd import MPDClient - -class player_control: - def __init__(self): - self.mpd_client = MPDClient() # create client object - self.mpd_client.timeout = 0.5 # network timeout in seconds (floats allowed), default: None - self.mpd_client.idletimeout = 0.5 # timeout for fetching the result of the idle command is handled seperately, default: None - #self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 - self.connect() - print("Connected to MPD Version: "+self.mpd_client.mpd_version) - - def connect(self): - self.mpd_client.connect("localhost", 6600) # connect to localhost:6600 - - def get_player_type_and_version(self, param): - return ({'tpye':'mpd','version':self.mpd_client.mpd_version}) - - def play(self, param): - try: - self.mpd_client.play() - except ConnectionError: - print ("MPD Connection Error, retry") - self.conncet() - self.mpd_client.play() - except Exception as e: - print(e) - song = self.mpd_client.currentsong() - return ({'song':song}) - - def get_current_song(self, param): - song = self.mpd_client.currentsong() - #resp = {'resp': self.mpd_client.currentsong()} - return song - -class volume_control_mpd: - def __init__(self): - print ("not yet implemented\n") - -class volume_control_alsa: - def __init__(self): - self.mixer = alsaaudio.Mixer('PCM', 0) - self.volume = 0 - #self.mixer.getvolume() - - def get(self, param): - return ({'volume':self.volume}) - - def set(self, param): - volume = param.get('volume') - if isinstance(volume, int): - if (volume < 0): volume = 0; - if (volume > 100): volume = 100; - self.volume = volume - self.mixer.setvolume(self.volume) - else: - volume = -1 - return ({'volume':volume}) - - def inc(self, param): - volume = self.volume +3 - if (volume > 100): volume = 100 - self.volume = volume - self.mixer.setvolume(self.volume) - return ({'volume':self.volume}) - - def dec(self, param): - volume = self.volume -3 - if (volume < 0): volume = 0 - self.volume = volume - self.mixer.setvolume(self.volume) - return ({'volume':self.volume}) - -class phoniebox_control: - - def __init__(self,objects): - self.objects = objects - self.context = None - - def connect(self,addr= None): - if addr == None: - addr = "tcp://127.0.0.1:5555" - self.context = zmq.Context() - self.socket = self.context.socket(zmq.REP) - self.socket.bind(addr) - self.socket.setsockopt(zmq.LINGER, 200) - - def run(self, obj, cmd, param): - run_obj = self.objects.get(obj) - - if (run_obj is not None): - run_func = getattr(run_obj,cmd,None) - if (run_func is not None): # is callable() ?? - resp = run_func(param) - print (resp) - else: - resp = {'resp': "no valid commad"} - print (resp) - else: - resp = {'resp': "no valid obj"} - print (resp) - return resp - - def process_queue(self): - #while True: - # Wait for next request from client - message = self.socket.recv() - nt = nanotime.now().nanoseconds() - - client_request=json.loads(message) - client_response = {} - - print (client_request) - - client_object = client_request.get('obj') - if (client_object != None): - client_command = client_request.get('cmd') - if (client_command != None): - client_param = client_request.get('param') - client_response['resp'] = self.run(client_object,client_command,client_param) - - client_tsp = client_request.get('tsp') - if (client_tsp != None): - client_response['total_processing_time'] = (nt - int(client_request['tsp'])) / 1000000 - print ("processing time: {:2.3f} ms".format(client_response['total_processing_time'])) - - print(client_response) - # Send reply back to client - self.socket.send_string(json.dumps(client_response)) - - return (1) - - -#def get(self): -# def func_not_found(): # just in case we dont have the function -# print 'No Function '+self.i+' Found!' -# func_name = 'function' + self.i -# func = getattr(self,func_name,func_not_found) -# func() # <-- this should work! - - -if __name__ == "__main__": - #initialize objcts - objects = {'volume':volume_control_alsa(), - 'player':player_control()} - - print ("Start Phonibox Control") - pc = phoniebox_control(objects) - pc.connect() - - print ("Start loop") - ret_ok = 1 - while (ret_ok): - ret_ok = pc.process_queue() - \ No newline at end of file diff --git a/scripts/userscripts/addhotspot.sh b/scripts/userscripts/addhotspot.sh deleted file mode 100644 index 038fd6957..000000000 --- a/scripts/userscripts/addhotspot.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# addhotspot.sh newssid newpassword -wpa_passphrase "$1" $2 >> /etc/wpa_supplicant/wpa_supplicant.conf - -# /etc/dhcpcd.conf -echo ssid $1 >> /etc/dhcpcd.conf - - - diff --git a/settings/phoniebox_cardid_database.json b/settings/phoniebox_cardid_database.json index d3bedefae..e8eeacedc 100644 --- a/settings/phoniebox_cardid_database.json +++ b/settings/phoniebox_cardid_database.json @@ -21,9 +21,11 @@ } }, "108,07437": { - "object": "", - "method": "", - "params": {} + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "digger barnes" + } }, "107,60360": { "object": "", From 6233a8f9d627ca54e1a9c971273172434f5f764b Mon Sep 17 00:00:00 2001 From: arne123 Date: Tue, 20 Apr 2021 23:45:59 +0200 Subject: [PATCH 018/606] renamed components to Phonibxox added brach description to README.md" --- {components => Phoniebox}/PhonieboxDaemon.py | 0 .../PhonieboxNvManager.py | 0 {components => Phoniebox}/PhonieboxSystem.py | 0 {components => Phoniebox}/PhonieboxVolume.py | 0 {components => Phoniebox}/__init__.py | 0 .../audio/PirateAudioHAT/README.md | 0 .../audio/PirateAudioHAT/requirements.txt | 0 .../PirateAudioHAT/setup_pirateAudioHAT.sh | 0 {components => Phoniebox}/cli_client/pbc.c | 0 .../controls/__init__.py | 0 .../controls/buttons_usb_encoder/README.md | 0 .../controls/buttons_usb_encoder/__init__.py | 0 .../buttons-usb-encoder.jpg | Bin .../buttons_usb_encoder.py | 0 .../io_buttons_usb_encoder.py | 0 .../map_buttons_usb_encoder.py | 0 ...oniebox-buttons-usb-encoder.service.sample | 0 .../register_buttons_usb_encoder.py | 0 .../setup-buttons-usb-encoder.sh | 0 .../displays/HD44780-i2c/README.md | 0 .../i2c-lcd.service.default.sample | 0 .../displays/HD44780-i2c/i2c_lcd.py | 0 .../displays/HD44780-i2c/i2c_lcd_driver.py | 0 .../dot-matrix-module-MAX7219/README.md | 0 .../dot-matrix-module-MAX7219/display.ino | 0 .../dot-matrix-module-MAX7219/still.jpg | Bin .../dot-matrix-module-MAX7219/ticker.gif | Bin .../gpio_control/GPIODevices/VolumeControl.py | 0 .../gpio_control/GPIODevices/__init__.py | 0 .../gpio_control/GPIODevices/led.py | 0 .../GPIODevices/rotary_encoder.py | 0 .../GPIODevices/shutdown_button.py | 0 .../gpio_control/GPIODevices/simple_button.py | 0 .../GPIODevices/two_button_control.py | 0 .../gpio_control/README.md | 0 .../gpio_control/__init__.py | 0 .../gpio_control/check_installation.sh | 0 .../gpio_setting_rotary_vol_prevnext.ini | 0 .../example_configs/gpio_settings.ini | 0 .../gpio_settings_rotary_and_led.ini | 0 .../example_configs/gpio_settings_test.ini | 0 .../gpio_control/function_calls.py | 0 .../gpio_control/gpio_control.py | 0 .../gpio_control/install.sh | 0 .../gpio_control/requirements.txt | 0 .../gpio_control/test/__init__.py | 0 .../gpio_control/test/conftest.py | 0 .../gpio_control/test/gpio_settings_test.ini | 0 .../gpio_control/test/test_RotaryEncoder.py | 0 .../gpio_control/test/test_SimpleButton.py | 0 .../test/test_TwoButtonControl.py | 0 .../gpio_control/test/test_gpio_control.py | 0 .../gpio_control/test/test_shutdown_button.py | 0 .../player/PhonieboxPlayerMPD.py | 0 {components => Phoniebox}/player/__init__.py | 0 .../rfid_reader/FakeRfidReader.py | 0 .../rfid_reader/PN532/README.md | 0 .../rfid_reader/PN532/requirements.txt | 0 .../rfid_reader/PN532/reset_pn532.sh | 0 .../rfid_reader/PN532/setup_pn532.sh | 0 .../rfid_reader/PhonieboxRfidReader.py | 0 .../rfid_reader/RC522/README.md | 0 .../rfid_reader/RC522/requirements.txt | 0 .../rfid_reader/RC522/setup_rc522.sh | 0 .../rfid_reader/RfidReader_PN532.py | 0 .../rfid_reader/RfidReader_RC522.py | 0 .../rfid_reader/RfidReader_RDM6300.py | 0 .../rfid_reader/__init__.py | 0 .../rpc/PhonieboxRpcClient.py | 0 .../rpc/PhonieboxRpcServer.py | 0 {components => Phoniebox}/rpc/__init__.py | 0 .../MQTT-protocol/README.md | 0 .../MQTT-protocol/daemon_mqtt_client.py | 0 ...mqtt-client.service.stretch-default.sample | 0 README.md | 274 ++++-------------- htdocs/api/player.php | 2 - requirements-GPIO.txt | 5 - requirements-gmusic.txt | 3 - requirements-spotify.txt | 3 - requirements.txt | 36 --- 80 files changed, 64 insertions(+), 259 deletions(-) rename {components => Phoniebox}/PhonieboxDaemon.py (100%) rename {components => Phoniebox}/PhonieboxNvManager.py (100%) rename {components => Phoniebox}/PhonieboxSystem.py (100%) rename {components => Phoniebox}/PhonieboxVolume.py (100%) rename {components => Phoniebox}/__init__.py (100%) rename {components => Phoniebox}/audio/PirateAudioHAT/README.md (100%) rename {components => Phoniebox}/audio/PirateAudioHAT/requirements.txt (100%) rename {components => Phoniebox}/audio/PirateAudioHAT/setup_pirateAudioHAT.sh (100%) rename {components => Phoniebox}/cli_client/pbc.c (100%) rename {components => Phoniebox}/controls/__init__.py (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/README.md (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/__init__.py (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/buttons-usb-encoder.jpg (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/buttons_usb_encoder.py (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/io_buttons_usb_encoder.py (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/map_buttons_usb_encoder.py (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/register_buttons_usb_encoder.py (100%) rename {components => Phoniebox}/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh (100%) rename {components => Phoniebox}/displays/HD44780-i2c/README.md (100%) rename {components => Phoniebox}/displays/HD44780-i2c/i2c-lcd.service.default.sample (100%) rename {components => Phoniebox}/displays/HD44780-i2c/i2c_lcd.py (100%) rename {components => Phoniebox}/displays/HD44780-i2c/i2c_lcd_driver.py (100%) rename {components => Phoniebox}/displays/dot-matrix-module-MAX7219/README.md (100%) rename {components => Phoniebox}/displays/dot-matrix-module-MAX7219/display.ino (100%) rename {components => Phoniebox}/displays/dot-matrix-module-MAX7219/still.jpg (100%) rename {components => Phoniebox}/displays/dot-matrix-module-MAX7219/ticker.gif (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/VolumeControl.py (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/__init__.py (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/led.py (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/rotary_encoder.py (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/shutdown_button.py (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/simple_button.py (100%) rename {components => Phoniebox}/gpio_control/GPIODevices/two_button_control.py (100%) rename {components => Phoniebox}/gpio_control/README.md (100%) rename {components => Phoniebox}/gpio_control/__init__.py (100%) rename {components => Phoniebox}/gpio_control/check_installation.sh (100%) rename {components => Phoniebox}/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini (100%) rename {components => Phoniebox}/gpio_control/example_configs/gpio_settings.ini (100%) rename {components => Phoniebox}/gpio_control/example_configs/gpio_settings_rotary_and_led.ini (100%) rename {components => Phoniebox}/gpio_control/example_configs/gpio_settings_test.ini (100%) rename {components => Phoniebox}/gpio_control/function_calls.py (100%) rename {components => Phoniebox}/gpio_control/gpio_control.py (100%) rename {components => Phoniebox}/gpio_control/install.sh (100%) rename {components => Phoniebox}/gpio_control/requirements.txt (100%) rename {components => Phoniebox}/gpio_control/test/__init__.py (100%) rename {components => Phoniebox}/gpio_control/test/conftest.py (100%) rename {components => Phoniebox}/gpio_control/test/gpio_settings_test.ini (100%) rename {components => Phoniebox}/gpio_control/test/test_RotaryEncoder.py (100%) rename {components => Phoniebox}/gpio_control/test/test_SimpleButton.py (100%) rename {components => Phoniebox}/gpio_control/test/test_TwoButtonControl.py (100%) rename {components => Phoniebox}/gpio_control/test/test_gpio_control.py (100%) rename {components => Phoniebox}/gpio_control/test/test_shutdown_button.py (100%) rename {components => Phoniebox}/player/PhonieboxPlayerMPD.py (100%) rename {components => Phoniebox}/player/__init__.py (100%) rename {components => Phoniebox}/rfid_reader/FakeRfidReader.py (100%) rename {components => Phoniebox}/rfid_reader/PN532/README.md (100%) rename {components => Phoniebox}/rfid_reader/PN532/requirements.txt (100%) rename {components => Phoniebox}/rfid_reader/PN532/reset_pn532.sh (100%) rename {components => Phoniebox}/rfid_reader/PN532/setup_pn532.sh (100%) rename {components => Phoniebox}/rfid_reader/PhonieboxRfidReader.py (100%) rename {components => Phoniebox}/rfid_reader/RC522/README.md (100%) rename {components => Phoniebox}/rfid_reader/RC522/requirements.txt (100%) rename {components => Phoniebox}/rfid_reader/RC522/setup_rc522.sh (100%) rename {components => Phoniebox}/rfid_reader/RfidReader_PN532.py (100%) rename {components => Phoniebox}/rfid_reader/RfidReader_RC522.py (100%) rename {components => Phoniebox}/rfid_reader/RfidReader_RDM6300.py (100%) rename {components => Phoniebox}/rfid_reader/__init__.py (100%) rename {components => Phoniebox}/rpc/PhonieboxRpcClient.py (100%) rename {components => Phoniebox}/rpc/PhonieboxRpcServer.py (100%) rename {components => Phoniebox}/rpc/__init__.py (100%) rename {components => Phoniebox}/smart-home-automation/MQTT-protocol/README.md (100%) rename {components => Phoniebox}/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py (100%) rename {components => Phoniebox}/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample (100%) delete mode 100644 requirements-GPIO.txt delete mode 100644 requirements-gmusic.txt delete mode 100644 requirements-spotify.txt delete mode 100644 requirements.txt diff --git a/components/PhonieboxDaemon.py b/Phoniebox/PhonieboxDaemon.py similarity index 100% rename from components/PhonieboxDaemon.py rename to Phoniebox/PhonieboxDaemon.py diff --git a/components/PhonieboxNvManager.py b/Phoniebox/PhonieboxNvManager.py similarity index 100% rename from components/PhonieboxNvManager.py rename to Phoniebox/PhonieboxNvManager.py diff --git a/components/PhonieboxSystem.py b/Phoniebox/PhonieboxSystem.py similarity index 100% rename from components/PhonieboxSystem.py rename to Phoniebox/PhonieboxSystem.py diff --git a/components/PhonieboxVolume.py b/Phoniebox/PhonieboxVolume.py similarity index 100% rename from components/PhonieboxVolume.py rename to Phoniebox/PhonieboxVolume.py diff --git a/components/__init__.py b/Phoniebox/__init__.py similarity index 100% rename from components/__init__.py rename to Phoniebox/__init__.py diff --git a/components/audio/PirateAudioHAT/README.md b/Phoniebox/audio/PirateAudioHAT/README.md similarity index 100% rename from components/audio/PirateAudioHAT/README.md rename to Phoniebox/audio/PirateAudioHAT/README.md diff --git a/components/audio/PirateAudioHAT/requirements.txt b/Phoniebox/audio/PirateAudioHAT/requirements.txt similarity index 100% rename from components/audio/PirateAudioHAT/requirements.txt rename to Phoniebox/audio/PirateAudioHAT/requirements.txt diff --git a/components/audio/PirateAudioHAT/setup_pirateAudioHAT.sh b/Phoniebox/audio/PirateAudioHAT/setup_pirateAudioHAT.sh similarity index 100% rename from components/audio/PirateAudioHAT/setup_pirateAudioHAT.sh rename to Phoniebox/audio/PirateAudioHAT/setup_pirateAudioHAT.sh diff --git a/components/cli_client/pbc.c b/Phoniebox/cli_client/pbc.c similarity index 100% rename from components/cli_client/pbc.c rename to Phoniebox/cli_client/pbc.c diff --git a/components/controls/__init__.py b/Phoniebox/controls/__init__.py similarity index 100% rename from components/controls/__init__.py rename to Phoniebox/controls/__init__.py diff --git a/components/controls/buttons_usb_encoder/README.md b/Phoniebox/controls/buttons_usb_encoder/README.md similarity index 100% rename from components/controls/buttons_usb_encoder/README.md rename to Phoniebox/controls/buttons_usb_encoder/README.md diff --git a/components/controls/buttons_usb_encoder/__init__.py b/Phoniebox/controls/buttons_usb_encoder/__init__.py similarity index 100% rename from components/controls/buttons_usb_encoder/__init__.py rename to Phoniebox/controls/buttons_usb_encoder/__init__.py diff --git a/components/controls/buttons_usb_encoder/buttons-usb-encoder.jpg b/Phoniebox/controls/buttons_usb_encoder/buttons-usb-encoder.jpg similarity index 100% rename from components/controls/buttons_usb_encoder/buttons-usb-encoder.jpg rename to Phoniebox/controls/buttons_usb_encoder/buttons-usb-encoder.jpg diff --git a/components/controls/buttons_usb_encoder/buttons_usb_encoder.py b/Phoniebox/controls/buttons_usb_encoder/buttons_usb_encoder.py similarity index 100% rename from components/controls/buttons_usb_encoder/buttons_usb_encoder.py rename to Phoniebox/controls/buttons_usb_encoder/buttons_usb_encoder.py diff --git a/components/controls/buttons_usb_encoder/io_buttons_usb_encoder.py b/Phoniebox/controls/buttons_usb_encoder/io_buttons_usb_encoder.py similarity index 100% rename from components/controls/buttons_usb_encoder/io_buttons_usb_encoder.py rename to Phoniebox/controls/buttons_usb_encoder/io_buttons_usb_encoder.py diff --git a/components/controls/buttons_usb_encoder/map_buttons_usb_encoder.py b/Phoniebox/controls/buttons_usb_encoder/map_buttons_usb_encoder.py similarity index 100% rename from components/controls/buttons_usb_encoder/map_buttons_usb_encoder.py rename to Phoniebox/controls/buttons_usb_encoder/map_buttons_usb_encoder.py diff --git a/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample b/Phoniebox/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample similarity index 100% rename from components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample rename to Phoniebox/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample diff --git a/components/controls/buttons_usb_encoder/register_buttons_usb_encoder.py b/Phoniebox/controls/buttons_usb_encoder/register_buttons_usb_encoder.py similarity index 100% rename from components/controls/buttons_usb_encoder/register_buttons_usb_encoder.py rename to Phoniebox/controls/buttons_usb_encoder/register_buttons_usb_encoder.py diff --git a/components/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh b/Phoniebox/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh similarity index 100% rename from components/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh rename to Phoniebox/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh diff --git a/components/displays/HD44780-i2c/README.md b/Phoniebox/displays/HD44780-i2c/README.md similarity index 100% rename from components/displays/HD44780-i2c/README.md rename to Phoniebox/displays/HD44780-i2c/README.md diff --git a/components/displays/HD44780-i2c/i2c-lcd.service.default.sample b/Phoniebox/displays/HD44780-i2c/i2c-lcd.service.default.sample similarity index 100% rename from components/displays/HD44780-i2c/i2c-lcd.service.default.sample rename to Phoniebox/displays/HD44780-i2c/i2c-lcd.service.default.sample diff --git a/components/displays/HD44780-i2c/i2c_lcd.py b/Phoniebox/displays/HD44780-i2c/i2c_lcd.py similarity index 100% rename from components/displays/HD44780-i2c/i2c_lcd.py rename to Phoniebox/displays/HD44780-i2c/i2c_lcd.py diff --git a/components/displays/HD44780-i2c/i2c_lcd_driver.py b/Phoniebox/displays/HD44780-i2c/i2c_lcd_driver.py similarity index 100% rename from components/displays/HD44780-i2c/i2c_lcd_driver.py rename to Phoniebox/displays/HD44780-i2c/i2c_lcd_driver.py diff --git a/components/displays/dot-matrix-module-MAX7219/README.md b/Phoniebox/displays/dot-matrix-module-MAX7219/README.md similarity index 100% rename from components/displays/dot-matrix-module-MAX7219/README.md rename to Phoniebox/displays/dot-matrix-module-MAX7219/README.md diff --git a/components/displays/dot-matrix-module-MAX7219/display.ino b/Phoniebox/displays/dot-matrix-module-MAX7219/display.ino similarity index 100% rename from components/displays/dot-matrix-module-MAX7219/display.ino rename to Phoniebox/displays/dot-matrix-module-MAX7219/display.ino diff --git a/components/displays/dot-matrix-module-MAX7219/still.jpg b/Phoniebox/displays/dot-matrix-module-MAX7219/still.jpg similarity index 100% rename from components/displays/dot-matrix-module-MAX7219/still.jpg rename to Phoniebox/displays/dot-matrix-module-MAX7219/still.jpg diff --git a/components/displays/dot-matrix-module-MAX7219/ticker.gif b/Phoniebox/displays/dot-matrix-module-MAX7219/ticker.gif similarity index 100% rename from components/displays/dot-matrix-module-MAX7219/ticker.gif rename to Phoniebox/displays/dot-matrix-module-MAX7219/ticker.gif diff --git a/components/gpio_control/GPIODevices/VolumeControl.py b/Phoniebox/gpio_control/GPIODevices/VolumeControl.py similarity index 100% rename from components/gpio_control/GPIODevices/VolumeControl.py rename to Phoniebox/gpio_control/GPIODevices/VolumeControl.py diff --git a/components/gpio_control/GPIODevices/__init__.py b/Phoniebox/gpio_control/GPIODevices/__init__.py similarity index 100% rename from components/gpio_control/GPIODevices/__init__.py rename to Phoniebox/gpio_control/GPIODevices/__init__.py diff --git a/components/gpio_control/GPIODevices/led.py b/Phoniebox/gpio_control/GPIODevices/led.py similarity index 100% rename from components/gpio_control/GPIODevices/led.py rename to Phoniebox/gpio_control/GPIODevices/led.py diff --git a/components/gpio_control/GPIODevices/rotary_encoder.py b/Phoniebox/gpio_control/GPIODevices/rotary_encoder.py similarity index 100% rename from components/gpio_control/GPIODevices/rotary_encoder.py rename to Phoniebox/gpio_control/GPIODevices/rotary_encoder.py diff --git a/components/gpio_control/GPIODevices/shutdown_button.py b/Phoniebox/gpio_control/GPIODevices/shutdown_button.py similarity index 100% rename from components/gpio_control/GPIODevices/shutdown_button.py rename to Phoniebox/gpio_control/GPIODevices/shutdown_button.py diff --git a/components/gpio_control/GPIODevices/simple_button.py b/Phoniebox/gpio_control/GPIODevices/simple_button.py similarity index 100% rename from components/gpio_control/GPIODevices/simple_button.py rename to Phoniebox/gpio_control/GPIODevices/simple_button.py diff --git a/components/gpio_control/GPIODevices/two_button_control.py b/Phoniebox/gpio_control/GPIODevices/two_button_control.py similarity index 100% rename from components/gpio_control/GPIODevices/two_button_control.py rename to Phoniebox/gpio_control/GPIODevices/two_button_control.py diff --git a/components/gpio_control/README.md b/Phoniebox/gpio_control/README.md similarity index 100% rename from components/gpio_control/README.md rename to Phoniebox/gpio_control/README.md diff --git a/components/gpio_control/__init__.py b/Phoniebox/gpio_control/__init__.py similarity index 100% rename from components/gpio_control/__init__.py rename to Phoniebox/gpio_control/__init__.py diff --git a/components/gpio_control/check_installation.sh b/Phoniebox/gpio_control/check_installation.sh similarity index 100% rename from components/gpio_control/check_installation.sh rename to Phoniebox/gpio_control/check_installation.sh diff --git a/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini b/Phoniebox/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini similarity index 100% rename from components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini rename to Phoniebox/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini diff --git a/components/gpio_control/example_configs/gpio_settings.ini b/Phoniebox/gpio_control/example_configs/gpio_settings.ini similarity index 100% rename from components/gpio_control/example_configs/gpio_settings.ini rename to Phoniebox/gpio_control/example_configs/gpio_settings.ini diff --git a/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini b/Phoniebox/gpio_control/example_configs/gpio_settings_rotary_and_led.ini similarity index 100% rename from components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini rename to Phoniebox/gpio_control/example_configs/gpio_settings_rotary_and_led.ini diff --git a/components/gpio_control/example_configs/gpio_settings_test.ini b/Phoniebox/gpio_control/example_configs/gpio_settings_test.ini similarity index 100% rename from components/gpio_control/example_configs/gpio_settings_test.ini rename to Phoniebox/gpio_control/example_configs/gpio_settings_test.ini diff --git a/components/gpio_control/function_calls.py b/Phoniebox/gpio_control/function_calls.py similarity index 100% rename from components/gpio_control/function_calls.py rename to Phoniebox/gpio_control/function_calls.py diff --git a/components/gpio_control/gpio_control.py b/Phoniebox/gpio_control/gpio_control.py similarity index 100% rename from components/gpio_control/gpio_control.py rename to Phoniebox/gpio_control/gpio_control.py diff --git a/components/gpio_control/install.sh b/Phoniebox/gpio_control/install.sh similarity index 100% rename from components/gpio_control/install.sh rename to Phoniebox/gpio_control/install.sh diff --git a/components/gpio_control/requirements.txt b/Phoniebox/gpio_control/requirements.txt similarity index 100% rename from components/gpio_control/requirements.txt rename to Phoniebox/gpio_control/requirements.txt diff --git a/components/gpio_control/test/__init__.py b/Phoniebox/gpio_control/test/__init__.py similarity index 100% rename from components/gpio_control/test/__init__.py rename to Phoniebox/gpio_control/test/__init__.py diff --git a/components/gpio_control/test/conftest.py b/Phoniebox/gpio_control/test/conftest.py similarity index 100% rename from components/gpio_control/test/conftest.py rename to Phoniebox/gpio_control/test/conftest.py diff --git a/components/gpio_control/test/gpio_settings_test.ini b/Phoniebox/gpio_control/test/gpio_settings_test.ini similarity index 100% rename from components/gpio_control/test/gpio_settings_test.ini rename to Phoniebox/gpio_control/test/gpio_settings_test.ini diff --git a/components/gpio_control/test/test_RotaryEncoder.py b/Phoniebox/gpio_control/test/test_RotaryEncoder.py similarity index 100% rename from components/gpio_control/test/test_RotaryEncoder.py rename to Phoniebox/gpio_control/test/test_RotaryEncoder.py diff --git a/components/gpio_control/test/test_SimpleButton.py b/Phoniebox/gpio_control/test/test_SimpleButton.py similarity index 100% rename from components/gpio_control/test/test_SimpleButton.py rename to Phoniebox/gpio_control/test/test_SimpleButton.py diff --git a/components/gpio_control/test/test_TwoButtonControl.py b/Phoniebox/gpio_control/test/test_TwoButtonControl.py similarity index 100% rename from components/gpio_control/test/test_TwoButtonControl.py rename to Phoniebox/gpio_control/test/test_TwoButtonControl.py diff --git a/components/gpio_control/test/test_gpio_control.py b/Phoniebox/gpio_control/test/test_gpio_control.py similarity index 100% rename from components/gpio_control/test/test_gpio_control.py rename to Phoniebox/gpio_control/test/test_gpio_control.py diff --git a/components/gpio_control/test/test_shutdown_button.py b/Phoniebox/gpio_control/test/test_shutdown_button.py similarity index 100% rename from components/gpio_control/test/test_shutdown_button.py rename to Phoniebox/gpio_control/test/test_shutdown_button.py diff --git a/components/player/PhonieboxPlayerMPD.py b/Phoniebox/player/PhonieboxPlayerMPD.py similarity index 100% rename from components/player/PhonieboxPlayerMPD.py rename to Phoniebox/player/PhonieboxPlayerMPD.py diff --git a/components/player/__init__.py b/Phoniebox/player/__init__.py similarity index 100% rename from components/player/__init__.py rename to Phoniebox/player/__init__.py diff --git a/components/rfid_reader/FakeRfidReader.py b/Phoniebox/rfid_reader/FakeRfidReader.py similarity index 100% rename from components/rfid_reader/FakeRfidReader.py rename to Phoniebox/rfid_reader/FakeRfidReader.py diff --git a/components/rfid_reader/PN532/README.md b/Phoniebox/rfid_reader/PN532/README.md similarity index 100% rename from components/rfid_reader/PN532/README.md rename to Phoniebox/rfid_reader/PN532/README.md diff --git a/components/rfid_reader/PN532/requirements.txt b/Phoniebox/rfid_reader/PN532/requirements.txt similarity index 100% rename from components/rfid_reader/PN532/requirements.txt rename to Phoniebox/rfid_reader/PN532/requirements.txt diff --git a/components/rfid_reader/PN532/reset_pn532.sh b/Phoniebox/rfid_reader/PN532/reset_pn532.sh similarity index 100% rename from components/rfid_reader/PN532/reset_pn532.sh rename to Phoniebox/rfid_reader/PN532/reset_pn532.sh diff --git a/components/rfid_reader/PN532/setup_pn532.sh b/Phoniebox/rfid_reader/PN532/setup_pn532.sh similarity index 100% rename from components/rfid_reader/PN532/setup_pn532.sh rename to Phoniebox/rfid_reader/PN532/setup_pn532.sh diff --git a/components/rfid_reader/PhonieboxRfidReader.py b/Phoniebox/rfid_reader/PhonieboxRfidReader.py similarity index 100% rename from components/rfid_reader/PhonieboxRfidReader.py rename to Phoniebox/rfid_reader/PhonieboxRfidReader.py diff --git a/components/rfid_reader/RC522/README.md b/Phoniebox/rfid_reader/RC522/README.md similarity index 100% rename from components/rfid_reader/RC522/README.md rename to Phoniebox/rfid_reader/RC522/README.md diff --git a/components/rfid_reader/RC522/requirements.txt b/Phoniebox/rfid_reader/RC522/requirements.txt similarity index 100% rename from components/rfid_reader/RC522/requirements.txt rename to Phoniebox/rfid_reader/RC522/requirements.txt diff --git a/components/rfid_reader/RC522/setup_rc522.sh b/Phoniebox/rfid_reader/RC522/setup_rc522.sh similarity index 100% rename from components/rfid_reader/RC522/setup_rc522.sh rename to Phoniebox/rfid_reader/RC522/setup_rc522.sh diff --git a/components/rfid_reader/RfidReader_PN532.py b/Phoniebox/rfid_reader/RfidReader_PN532.py similarity index 100% rename from components/rfid_reader/RfidReader_PN532.py rename to Phoniebox/rfid_reader/RfidReader_PN532.py diff --git a/components/rfid_reader/RfidReader_RC522.py b/Phoniebox/rfid_reader/RfidReader_RC522.py similarity index 100% rename from components/rfid_reader/RfidReader_RC522.py rename to Phoniebox/rfid_reader/RfidReader_RC522.py diff --git a/components/rfid_reader/RfidReader_RDM6300.py b/Phoniebox/rfid_reader/RfidReader_RDM6300.py similarity index 100% rename from components/rfid_reader/RfidReader_RDM6300.py rename to Phoniebox/rfid_reader/RfidReader_RDM6300.py diff --git a/components/rfid_reader/__init__.py b/Phoniebox/rfid_reader/__init__.py similarity index 100% rename from components/rfid_reader/__init__.py rename to Phoniebox/rfid_reader/__init__.py diff --git a/components/rpc/PhonieboxRpcClient.py b/Phoniebox/rpc/PhonieboxRpcClient.py similarity index 100% rename from components/rpc/PhonieboxRpcClient.py rename to Phoniebox/rpc/PhonieboxRpcClient.py diff --git a/components/rpc/PhonieboxRpcServer.py b/Phoniebox/rpc/PhonieboxRpcServer.py similarity index 100% rename from components/rpc/PhonieboxRpcServer.py rename to Phoniebox/rpc/PhonieboxRpcServer.py diff --git a/components/rpc/__init__.py b/Phoniebox/rpc/__init__.py similarity index 100% rename from components/rpc/__init__.py rename to Phoniebox/rpc/__init__.py diff --git a/components/smart-home-automation/MQTT-protocol/README.md b/Phoniebox/smart-home-automation/MQTT-protocol/README.md similarity index 100% rename from components/smart-home-automation/MQTT-protocol/README.md rename to Phoniebox/smart-home-automation/MQTT-protocol/README.md diff --git a/components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py b/Phoniebox/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py similarity index 100% rename from components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py rename to Phoniebox/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py diff --git a/components/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample b/Phoniebox/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample similarity index 100% rename from components/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample rename to Phoniebox/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample diff --git a/README.md b/README.md index 62d9797e9..84beac8f7 100755 --- a/README.md +++ b/README.md @@ -1,259 +1,113 @@ -![GitHub last commit (branch)](https://img.shields.io/github/last-commit/MiczFlor/RPi-Jukebox-RFID/develop) +This Branch is an attempt to realize elements from the discussion which took place in https://github.com/MiczFlor/RPi-Jukebox-RFID/discussions/1329 -![Python Tests](https://github.com/MiczFlor/RPi-Jukebox-RFID/workflows/Python%20package/badge.svg) ![Install Script Tests](https://github.com/MiczFlor/RPi-Jukebox-RFID/workflows/Docker%20Test%20Installation/badge.svg) +This is the first attempt to a new structure, many things here are untested and error prone. This is still in the phase of orientation and proof of concept with trails and experiments in many directions which all have to be understood as base for discussion. -[![Gitter chat](https://badges.gitter.im/phoniebox/gitter.png)](https://gitter.im/phoniebox) +####These are the Fundamental Design Goals: -# Phoniebox: the RPi-Jukebox-RFID +- better maintainability +- clear strategy on architecture +- higher performance especially on lower end Hardware -A contactless jukebox for the Raspberry Pi, playing audio files, playlists, podcasts, web streams and spotify triggered by RFID cards. All plug and play via USB, no soldering iron needed. Update: if you must, it now also features a howto for adding GPIO buttons controls. +####To achieve this, the current direction is: -## The Phoniebox Calendar 2021 is here!!! +- avoid shell script invocation during runtime +- establish a socket based API +- re-implement the core functionality in python -Another bunch of wonderful designs! 2021 is the third Phoniebox calendar. If you are interested, you can see the [2019 and 2020 calendars in the docs folder](https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/docs). Download [the printable PDF of 2021 here](https://mi.cz/static/2021-Phoniebox-Calendar.pdf). +###What has been realized so far: -![The 2021 Phoniebox Calendar](docs/2021-Phoniebox-Calendar.jpg "The 2021 Phoniebox Calendar") +Running Player Functionality (Landing Page of WebUI) as a Python based rewrite of the Backend based on a socked API realized with ZMQ (Zero message Queue). -The year 2020 also has a clear *:star: community hero :star:*: @s-martin has been doing outstanding work for the Phoniebox community:sparkles:. Thanks to you and may 2021 be a wonderful year for you. +The work has taken place in the Components Folder, which has been renamed to Phoniebox since most of the existing Python code was located there. The Folder is structured as a Python Package, including all former components, mainly for the Reason of faster development right now. -## Important updates / news +###Architecture +The Fundamental Architecture looks like: -* **Discussions forums** we use Github's Discussions feature for a more forum style. Please ask general questions in [Discussions](https://github.com/MiczFlor/RPi-Jukebox-RFID/discussions), bugs and enhancements should still be in [Issues](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). +[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbldbV0VCIFVJXSAtLS0-fFpNUXwgWihwaG9uaWVib3ggcnBjIHNlcnZlcilcbkdbR1BJT10gLS0tPnxaTVF8IFpcblFbTVFUVF0gLS0tPnxaTVF8IFpcblJbUkZJRF0gLS0tPnxaTVF8IFpcblogLS0-IFBbUExBWUVSIE1QRCBDbGFzc10gLS0-IHB5dGhvbi1tcGQyLS0-IE1bTVBEXVxuXG5aIC0tPiBWW1ZPTFVNRSBDbGFzc10gLS0-IHB5YWxzYWF1ZGlvIC0tPiBBW0FMU0FdXG5aIC0tPiBTW1NZU1RFTV0gLS0-IGV4ZWMgLS0-IHN5c3RlbWRcblMgLS0-IENPTkZJRyAtLT4gZmlsZXN5c3RlbVxuUyAtLT4gZVtleGVjXSAtLT4gd2hhdGV2ZXJcblNoZWxsIC0tPiBCW0NMSV0gLS0-IHxaTVF8IFoiLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbldbV0VCIFVJXSAtLS0-fFpNUXwgWihwaG9uaWVib3ggcnBjIHNlcnZlcilcbkdbR1BJT10gLS0tPnxaTVF8IFpcblFbTVFUVF0gLS0tPnxaTVF8IFpcblJbUkZJRF0gLS0tPnxaTVF8IFpcblogLS0-IFBbUExBWUVSIE1QRCBDbGFzc10gLS0-IHB5dGhvbi1tcGQyLS0-IE1bTVBEXVxuXG5aIC0tPiBWW1ZPTFVNRSBDbGFzc10gLS0-IHB5YWxzYWF1ZGlvIC0tPiBBW0FMU0FdXG5aIC0tPiBTW1NZU1RFTV0gLS0-IGV4ZWMgLS0-IHN5c3RlbWRcblMgLS0-IENPTkZJRyAtLT4gZmlsZXN5c3RlbVxuUyAtLT4gZVtleGVjXSAtLT4gd2hhdGV2ZXJcblNoZWxsIC0tPiBCW0NMSV0gLS0-IHxaTVF8IFoiLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ) -* **Gitter Community** we got ourselves a gitter community; chat us up at https://gitter.im/phoniebox -* **Phoniebox [2.2](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/4?closed=1) released (2020-11-23)** +##PhonieboxDaemon: -The [2.2](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/4?closed=1) release was pushed through the doors with many contributors (some of which in alphabetical order): @andreasbrett @BerniPi @juhrmann @Luegengladiator @MarkusProchaska @MarlonKrug @patrickweigelt @princemaxwell @RalfAlbers @s-martin @themorlan @veloxidSchweiz @xn--nding-jua. [List of all contributors](https://github.com/MiczFlor/RPi-Jukebox-RFID/graphs/contributors) -## What's new in version 2.2? +###PhonieboxDaemon.py +This is the Entry point of this Implementation -* :fire: **Fixed location of gpio_settings.ini** for [GPIO control](components/gpio_control/README.md) -* Added support for files with embedded chapters metada (like m4a) enhancement -* Added customizable poweroff command bash enhancement -* Finally fixed resume function... -* Lots of fixed bugs and minor improvements... - * Status LED, Rotary Button, Volume Up/Down, custom music directory for +Spotify, Startup sound volume +This Daemon is so far: -**What's still hot?** -* The constantly improved **one-line install script** handles both **Classic** and **+Spotify** when [setting up your Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command) - * integrated improved [GPIO control](components/gpio_control/README.md) - * integrated selection of RFID readers and uses [multiple readers](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/1012#issue-434052529) simultaneously - * features *non-interactive* installs based on a config file -* **[WiFi management](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#wifi-settings)** - * RFID cards to **toggle Wifi** (or switch it on/off) - * Read out the Wifi IP address (if you are connecting to a new network and don't know where to point your browser) - * **Hotspot** Phoniebox: [ad-hoc hotspot](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/967) if no known network found (IP: 10.0.0.5 SSID: phoniebox Password: PlayItLoud) -* **Touchscreen** LCD display Player (file: `index-lcd.php`in web app) -* Integrate your [Phoniebox in your Smart Home](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Smart-Home-remote-control-with-MQTT). -* Smoother [Web App running on ajax](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/623). -* New [search form for local files](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/710) -* Control the debug logs in the web app (individual scripts switched on/off, empty log file). -* Set [maximum volume with RFID](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/633) cards. -* Control via [**wifi web app**](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#webapp) from your phone, tablet or PC. You can play, upload, move files, assign new RFID cards, control playout, settings, etc. -* [**RFID** control](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#phoniebox-controls-using-rfid-cards) for playout and controlling your Phoniebox. -* [Playout **Resume**](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#manage-playout-behaviour) switch for audio books, allowing you to jump straight back to where you were (unless you fell asleep...). -* Playout **Shuffle** switch to mix up your playlists. -* Download from **YouTube** directly to your Phoniebox. -* Support for **[Spotify](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Spotify-FAQ)** and **[Google Play Music](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Enable-Google-Play-Music-GMusic)** integration. -* **Podcasts!** More for myself than anybody else, I guess, I added the [podcast feature for Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#podcasts) (2018-05-09) -* [Buttons](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons) and [knobs / dials](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Audio-RotaryKnobVolume) to control your **Phoniebox via GPIO**. - -### Quick install - -[One line install script](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command) for Raspbian `buster` available. +- Playing Startup sound +- Reading Status and Database Files +- Instantiating Phoniebox Objects (Volume, Player, System Control) +- Instantiating and Starting RFID Reader as thread +- \##Instantiating and Starting GPIO Control +- Running the RPC Server (API) -* **MUST READ for users of [Phoniebox +Spotify Edition](docs/SPOTIFY-INTEGRATION.md)** -* This install script combines the two versions *Classic* and *+ Spotify*. -* *Phoniebox Classic* supports local audio, web radio, podcasts, YouTube (download and convert), GPIO and/or RFID +_Next Steps here:_ -Documentation can be found in the [GitHub wiki for Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki). Please try to add content in the wiki regarding special hardware, software tweaks and the like. +- [ ] reading of Config File in order to allow distributed development +- [ ] Refactoring as class in order to allow the exit functions to take over more tasks +- [ ] Add Logger ? +- [ ] Add Command Line Interface to pass config etc. (e.g to be started by systemd) -## The 2020 Phoniebox Calendar is out! +###rpc/PhoniboxRpcServer.py -Celebrating all the great designs of 2019, I put together a calendar for 2020, see picture above. If you want to be featured on next years calendar, please make sure to add your Phoniebox pics to the [design thread here on github](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/639). +This Server is ZMQ based API, which takes a dictionary of control objects (classes) as argument. +Each Method of the passed classes will be made available to the API -The PDF is about 6MB and will print well on A2 paper size, but it should also look good on larger poster sizes. Thanks to all the contributors, designers and makers. Have a good start into 2020 and keep up the good work! -![The 2020 Phoniebox Calendar](docs/2020-Phoniebox-Calendar.jpg "The 2020 Phoniebox Calendar") +###rpc/PhoniboxRpcClient.py -* [Download the 2020 Phoniebox Calendar PDF here](https://drive.google.com/file/d/1krb8G8Td1Vrf3sYWl44nZyuoJ0DIC5vX/view?usp=sharing) -* In case you missed it, [download the 2019 Phoniebox Calendar PDF here](https://drive.google.com/file/d/1NKlertLP0nIKOsHrcqu5pxe6NZU3SfS9/view?usp=sharing) +Module which is intended to be included by other python Control Class in order to access the API ---- - - +###PhoniboxNVManager.py -If you like your Phoniebox, consider to [buy me a coffee](https://www.buymeacoffee.com/MiczFlor) -or donate via [PayPal](https://www.paypal.com) to micz.flor@web.de using the *friends* option. +This is an Non Volatile Memory Manager. The attempt here is to reduce File IO writes, by keeping runtime Information in the RAM (a dictionary) and store them to Disk on Exit. ---- +###PhoniboxVolume.py -Prototype of the RFID jukebox +ALSA Volume Control, utilizing pyalsaaudio +hard-coded ALSA Volume Control -*See the Phoniebox code in action, watch this video and read the blog post from [iphone-ticker.de](https://www.iphone-ticker.de/wochenend-projekt-kontaktlose-musikbox-fuer-kinder-123063/)* +_Next Steps here:_ -**We love Tech** published a video screencast on *how to build your Phoniebox* (in German), you can find all the steps and see the final product here: +- [ ] allow Card and Interface configuration -| | | | -| --- | --- | --- | -|
Installation und Hardware
|
Web App and Audio / Spotify
|
The finished Phoniebox in action
| +###player/PhoniboxPlayerMPD.py -A new video screencast about +Core Player Function, Controls MPD (via python-mpd2) +Thsi si where most of the former Rsume Play and Playout Controls.sh went into -**What makes this Phoniebox easy to install and use:** +_Next Steps here:_ -* Runs on all Raspberry Pi models (1, 2 and 3) and [Raspberry Zero](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/15). (jump to the [install instructions](#install)) -* Just plug and play using USB, no soldering iron needed. -* Once the Phoniebox is up and running, add music from any computer on your home network. -* Register new RFID cards easily without having to connect to the RPi. -* Play single or multiple files, podcasts or web streams. -* Volume control is also done with RFID cards or key fobs. -* Connect to your Phoniebox via your wifi network or run the Phoniebox like an access point and connect directly without a router. -* **Bonus:** control the Phoniebox from your phone or computer via a web app. +- [ ] Implement Resume Functionality +- [ ] Reduce / Organize Json Output of playerstatus to what is really needed by the WebUI +- [ ] Switch mpd control to asyncio in order to be independent of WebUI Polling for actual mpd status -![The web app allows you to change the volume level, list and play audio files and folders, stop the player and shut down the RPi gracefully.](docs/img/web-app-iphone-screens.jpg "The web app allows you to change the volume level, list and play audio files and folders, stop the player and shut down the RPi gracefully.") +##cli_client +command line access to the Daemon API, +This should be a very lightweight and fast interface-tool with nearly no further dependencies. -The **web app** runs on any device and is mobile optimised. It provides: +_Next Steps here:_ -* An audio player to pause, resume, shuffle, loop, stop and skip to previous and next track. -* Sub folder support: manage your collection in sub folders. Phoniebox has two play buttons: only this folder and eeeeverything in this folder. -* Manage files and folders via the web app. -* Register new RFID cards, manage Phoniebox settings, display system info and edit the wifi connection. -* Covers displayed in the web app (files called `cover.jpg`). +- [ ] Define and Implement Output Format which can be easily treated in a shell -## Phoniebox Gallery -| | | | | | | -| --- | --- | --- | --- | --- | --- | -| ![Caption](docs/img/gallery/Steph-20171215_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/Elsa-20171210_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/Geliras-20171228-Jukebox-01-h90.jpg "Caption") | ![Caption](docs/img/gallery/UlliH-20171210_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/KingKahn-20180101-Jukebox-01-h90.jpg "Caption") | ![Caption](docs/img/gallery/hailogugo-20171222-h90-01.jpg "Caption") | -**See more innovation, upcycling and creativity in the [Phoniebox Gallery](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/GALLERY) or visit and share the project's homepage at [phoniebox.de](http://phoniebox.de/). There is also an [english Phoniebox page](http://phoniebox.de/index.php?l=en).** +##WebUI -## Installation +Even there are many changes in the WebUI it has been keept as it was as much as possible. +The changes are mainly in order to interface with the new ZMQ aproach -* Installation instructions for Raspbian (https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch). -* You can also use the [headless installation over ssh](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#ssh-install) straight from a fresh SD card. -* For a quick install procedure, take a look at the [bash one line install script for stretch and buster](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command). This should get you started quickly. -* If you choose the step by step installation, you need to walk through the configuration steps for [Stretch](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/CONFIGURE-stretch). -* Once everything has been installed and configured, [read the manual](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL) to change settings, register RFID cards, add audio. +###htdocs/api/PhonieboxRpcClient.php +this is the PHP Version of the RpcClient to allow fast and lightweight API access -Adding push buttons to control volume, skipping tracks, pause, play: read the [GPIO buttons installation guide](docs/GPIO-BUTTONS.md). +_Next Steps here:_ -### Components +- [ ] get all configuration needed from the Daemon via the API instead of setting files +- [ ] reduce the still 109 exec calls .... +- [ ] evaluate jszmq or zeromq.js as option to directly interface the API via js (https://github.com/zeromq) -Special hardware is now organised in the folder [`components`](https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/master/components). If you have new hardware attached to your Phoniebox, please add to this library! It currently contains soundcards, displays, GPIO controls, RFID reader, smarthome integration. -## Manual -In the [Manual](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL) you will learn: -* [How to connect to the Phoniebox from any computer to add and edit audio files.](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#connect) -* [How to register new RFID cards, assign them a *human readable* shortcut and add audio files for each card.](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#registercards) -* [How to add webradio stations and other streams to the playout files](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#webstreams) - [and even mix web based and local files.](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#mixwebstreams) -* [Adding Podcasts the your Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#podcasts) -* [How to control the Phoniebox through the web app.](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#webapp) -* [How to assign cards specific tasks such as changing the volume level or shutting down the Phoniebox.](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#cardcontrol) -## Contributing improvements -Read the [CONTRIBUTING.md](CONTRIBUTING.md) file for [more infos on how to contribute code](CONTRIBUTING.md). - -## Reporting bugs - -To make maintenance easier for everyone, please run the following script -and post the results when reporting a bug. -(Note: the results contain some personal information like IP or SSID. -You might want to erase some of it before sharing with the bug report.) -~~~ -/home/pi/RPi-Jukebox-RFID/scripts/helperscripts/Analytics_AfterInstallScript.sh -~~~ -Just copy this line and paste it into your terminal on the pi. - -If you find something that doesn't work. And you tried and tried again, but it still doesn't work, please report your issue in the ["issues" section](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). Make sure to include information about the system and hardware you are using, like: - -*Raspberry ZERO, OS Jessie, Card reader lists as (insert here) when running scripts/RegisterDevice.py, installed Phoniebox version 0.9.3 (or: using latest master branch).* - -## Troubleshooting - -There is a growing section of [troubleshooting](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#faq) including: - -* I want to improve the onboard audio quality -* I am moving, how do I get the Phoniebox into my new WiFi network? -* The RFID Reader doesn't seem to work. -* Changing the volume does not work, but the playout works. -* Script `daemon_rfid_reader.py` only works via SSH not by RFID cards. -* Script daemon is closing down unexpectedly. -* Everything seems to work, but I hear nothing when swiping a card. -* I would like to use two cards / IDs to do the same thing. - -## Acknowledgments - -There are many, many, many inspiring suggestions and solutions on the web to bring together the idea of a jukebox with RFID cards. I want to mention a few of these that have inspired me. - -* Thanks to all the [contributors](https://github.com/MiczFlor/RPi-Jukebox-RFID/graphs/contributors). Not only for the good code review and feature suggestions, but also for the good spirit I get each time a new Phoniebox comes to this world :) -* Thanks to Andreas aka [hailogugo](https://github.com/hailogugo) for writing and testing the script for the [GPIO buttons as controllers for the jukebox](docs/GPIO-BUTTONS.md). -* [Francisco Sahli's Music Cards: RFID cards + Spotify + Raspberry Pi](https://fsahli.wordpress.com/2015/11/02/music-cards-rfid-cards-spotify-raspberry-pi/) written in python, playing songs from Spotify. The code [music-cards](https://github.com/fsahli/music-cards) is on GitHub. -* [Jeremy Lightsmith's rpi-jukebox](https://github.com/jeremylightsmith/rpi-jukebox) written in Python, using the mpg123 player -* [Marco Wiedemeyer's Raspberry Pi Jukebox für Kinder (German)](https://blog.mwiedemeyer.de/post/Raspberry-Pi-Jukebox-fur-Kinder/) written in mono, using the MPD player -* [Marcus Nasarek's Kindgerechter Audioplayer mit dem Raspberry Pi (German)](http://www.raspberry-pi-geek.de/Magazin/2014/03/Kindgerechter-Audioplayer-mit-dem-Raspberry-Pi) triggered by QR codes via a camera instead of RFID cards, written in bash and using the xmms2 media player -* [Huy Do's jukebox4kids / Jukebox für Kinder](http://www.forum-raspberrypi.de/Thread-projekt-jukebox4kids-jukebox-fuer-kinder) written in Python, [the code is on github](https://github.com/hdo/jukebox4kids) -* [Willem van der Jagt's How I built an audio book reader for my nearly blind grandfather](http://willemvanderjagt.com/2014/08/16/audio-book-reader/) written in python and using the MDP player. - -I also want to link to two proprietary and commercial projects, because they were an inspiration in the early days of the Phoniebox. Since the first release, the Phoniebox code has shown the power of open source development. Today, Phoniebox might be the most versatile project of its kind. - -* [tonies® - das neue Audiosystem für mehr Hör-Spiel-Spaß im Kinderzimmer. (German)](https://tonies.de/) You buy a plastic figure which then triggers the audiofile - which is served over the web. -* [Hörbert - a MP3 player controlled by buttons](https://hoerbert.com) In Germany this has already become a *classic*. They also started selling a DIY kit. - ---- - - - -If you like your Phoniebox, consider to [buy me a coffee](https://www.buymeacoffee.com/MiczFlor) - ---- - -## Shopping list - -Here is a list of equipment needed. You can find a lot second hand online (save money and the planet). The links below lead to amazon, not at all because I want to support them, but because their PartnerNet program helps to support the Phoniebox maintenance (a little bit...). **Note: depending on individual projects, the hardware requirements vary.** - -### Raspberry Pi - -* [Raspberry Pi 4 Modell B](https://amzn.to/2Yuei04) -* [Raspberry Pi 3 Model B](https://amzn.to/3fqp8ef) -* [Raspberry Pi Zero WH](https://amzn.to/3fkfKc5) -* Note: You might be surprised how easy and affordable you can get an RPi second hand. Think about the planet before you buy a new one. - -### RFID Reader and cards / fobs - -* RFID Card Reader (USB): [Neuftech USB RFID Reader ID](https://amzn.to/2RrqScm) using 125 kHz - make sure to buy compatible cards, RFID stickers or key fobs working with the same frequency as the reader. **Important notice:** the hardware of the reader that I had linked here for a long times seems to have changed and suddenly created problems with the Phoniebox installation. The reader listed now has worked and was recommended by two Phoniebox makers (2018 Oct 4). I can not guarantee that this will not change and invite you to give [RFID Reader feedback in this thread](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/231). - * RFID cards: [125 KHz EM4100](https://amzn.to/37pjy9q) make sure the frequency matches the RFID card reader !!! - * RFID fobs / key rings: [EM4100 RFID-Transponder-Schlüsselring, 125 KHz](https://amzn.to/3hsuvLO) make sure the frequency matches the RFID card reader !!! - -* RFID Kit RC522: [RC522 Reader, Chip, Card for Raspberry Pi 13.56MHz] (https://amzn.to/2C7YZCZ) - * RFID sticker / tags: [MIFARE RFID NFC Tags](https://amzn.to/30GfLDg) untested by me personally, but reported to work with work with RC522 and PN532 card readers. - -### Speakers / amps - -* [USB Stereo Speaker Set (6 Watt, 3,5mm jack, USB-powered) black](http://amzn.to/2kXrard) | This USB powered speaker set sounds good for its size, is good value for money and keeps this RPi project clean and without the need of a soldering iron :) -* [USB A Male to Female Extenstion Cable with Switch On/Off](http://amzn.to/2hHrvkG) | I placed this USB extension between the USB power adapter and the Phoniebox. This will allow you to switch the Phoniebox on and off easily. -* [USB 2.0 Hub 4-port bus powered USB Adapter](http://amzn.to/2kXeErv) | Depending on your setup, you will need none, one or two of these. If you are using the external USB powered speakers, you need one to make sure the speakers get enough power. If you want to use the additional USB soundcard and have an older RPi, you might need a second one to make sure you can connect enough devices with the RPi. - -### Arcade Buttons - -* [USB Interface for Arcade buttons](https://amzn.to/3nRAtIS) if you insist on not soldering hardware. (23rd Nov 2020: GPIO control script not yet part of the repo) -* Arcade Buttons / Sensors (one of these might suit you) - * [Arcade Buttons / Schalter in various colours](https://amzn.to/2QMxe9r) if you want buttons for the GPIO control. - * [Arcade Buttons wit LED and custom icons](https://amzn.to/2MWQ6hq) as used by [@splitti](https://splittscheid.de/selfmade-phoniebox/#3C). - * [Set: Arcade Buttons / Tasten / Schalter ](https://amzn.to/2T81JTZ) GPIO Extension Board Starter Kit including cables and breadboard. - * [Touch Sensor / Kapazitive Touch Tasten ](https://amzn.to/2Vc4ntx) these are not buttons to press but to touch as GPIO controls. - -### Special hardware - -These are links to additional items, which will add an individual flavour to your Phoniebox setup. Consult the issue threads to see if your idea has been realised already. - -* [Ground Loop Isolator / Entstörfilter Audio](https://amzn.to/2Kseo0L) this seems to [get rid off crackles in the audio out (a typical RPi problem)](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/341) -* [Mechanical audio switch](https://amzn.to/35oOSCS) if you want to connect differen audio devices, this is the easiest way (in connection with the *Ground Loop Isolator* you will get good results) -* [Rotary Encoder / Drehregler / Dial](https://amzn.to/2J34guF) for volume control. Read here for more information on how to [integrate the rotary dial](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/267) -* [HiFiBerry DAC+ Soundcard](https://amzn.to/2J36cU9) Read here for more information on how to [HifiBerry Soundcard integration](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#hifiberry-dac-soundcard-details) -* [HDMI zu HDMI + Optisches SPDIF mit 3,5-mm-Stereo-HDMI Audio-Extractor | HDMI zu SPDIF Konverter](https://amzn.to/2N8KP8C) If you plan to use video, this might be the better solution than a USB soundcard or the hifiberry. If takes up some space, but will work with the HDMI audio out and split the signal to deliver audio through 3.5mm jack. diff --git a/htdocs/api/player.php b/htdocs/api/player.php index 691c94021..aadd3e348 100755 --- a/htdocs/api/player.php +++ b/htdocs/api/player.php @@ -33,7 +33,6 @@ * DEBUG_WebApp_API="TRUE" */ $debugLoggingConf = parse_ini_file("../../settings/debugLogging.conf"); -$globalConf = parse_ini_file("../../settings/global.conf"); if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { file_put_contents("../../logs/debug.log", "\n# WebApp API # " . __FILE__, FILE_APPEND | LOCK_EX); @@ -83,7 +82,6 @@ function handlePut() { function handleGet() { global $debugLoggingConf; - global $globalConf; $json_response = PhonieboxRpcEnquene(array('object'=>'player','method'=>'playerstatus','param'=>'')); $responseList = json_decode ( $json_response,true)['resp']; diff --git a/requirements-GPIO.txt b/requirements-GPIO.txt deleted file mode 100644 index 080f3e1de..000000000 --- a/requirements-GPIO.txt +++ /dev/null @@ -1,5 +0,0 @@ -# gipo_control related requirements -# You need to install these with `sudo python3 -m pip install --upgrade --force-reinstall -q -r requirements-GPIO.txt` - -python-mpd2 -mock diff --git a/requirements-gmusic.txt b/requirements-gmusic.txt deleted file mode 100644 index 39e13ce36..000000000 --- a/requirements-gmusic.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Google Music related requirements (in addition to requirements-spotify.txt) -# You need to install these with `sudo pip install --upgrade --force-reinstall -r requirements-gmusic.txt` -Mopidy-Gmusic diff --git a/requirements-spotify.txt b/requirements-spotify.txt deleted file mode 100644 index 83fa3206f..000000000 --- a/requirements-spotify.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Spotify related requirements - # You need to install these with `sudo pip install --upgrade --force-reinstall -r requirements-spotify.txt` - Mopidy-Iris==3.54.2 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dfd2c27b3..000000000 --- a/requirements.txt +++ /dev/null @@ -1,36 +0,0 @@ -# Library dependencies for the python code. You need to install these with -# `sudo python3 -m pip install --upgrade --force-reinstall -r requirements.txt` before you can run this. - -#### ESSENTIAL LIBRARIES FOR MAIN FUNCTIONALITY #### - -# related libraries. -evdev==0.7.0 -git+git://github.com/lthiery/SPI-Py.git#egg=spi-py -youtube_dl -pyserial -RPi.GPIO - -# Type checking for python -# typing - -#### TESTING-RELATED PACKAGES #### - -# Checks style, syntax, and other useful errors -# pylint==1.6.5 - -# We'll use pytest to run our tests; this isn't really necessary to run the code, but it is to run -# the tests. With this here, you can run the tests with `py.test` from the base directory. -pytest - -# Makes it so that pytest can handle the code structure we use, with src/main/python, and src/test. -pytest-pythonpath - -# Allows generation of coverage reports with pytest. -pytest-cov - -# Allows marking tests as flaky, to be rerun if they fail -# flaky - -# Allows codecov to generate coverage reports -coverage -# codecov From ff4c60110a5ea61cf06a767939e53defb28b9597 Mon Sep 17 00:00:00 2001 From: arne123 Date: Tue, 20 Apr 2021 23:50:08 +0200 Subject: [PATCH 019/606] fixed header in README.md --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 84beac8f7..9020e360f 100755 --- a/README.md +++ b/README.md @@ -2,34 +2,34 @@ This Branch is an attempt to realize elements from the discussion which took pla This is the first attempt to a new structure, many things here are untested and error prone. This is still in the phase of orientation and proof of concept with trails and experiments in many directions which all have to be understood as base for discussion. -####These are the Fundamental Design Goals: +#### These are the Fundamental Design Goals: - better maintainability - clear strategy on architecture - higher performance especially on lower end Hardware -####To achieve this, the current direction is: +#### To achieve this, the current direction is: - avoid shell script invocation during runtime - establish a socket based API - re-implement the core functionality in python -###What has been realized so far: +### What has been realized so far: Running Player Functionality (Landing Page of WebUI) as a Python based rewrite of the Backend based on a socked API realized with ZMQ (Zero message Queue). The work has taken place in the Components Folder, which has been renamed to Phoniebox since most of the existing Python code was located there. The Folder is structured as a Python Package, including all former components, mainly for the Reason of faster development right now. -###Architecture +### Architecture The Fundamental Architecture looks like: [![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbldbV0VCIFVJXSAtLS0-fFpNUXwgWihwaG9uaWVib3ggcnBjIHNlcnZlcilcbkdbR1BJT10gLS0tPnxaTVF8IFpcblFbTVFUVF0gLS0tPnxaTVF8IFpcblJbUkZJRF0gLS0tPnxaTVF8IFpcblogLS0-IFBbUExBWUVSIE1QRCBDbGFzc10gLS0-IHB5dGhvbi1tcGQyLS0-IE1bTVBEXVxuXG5aIC0tPiBWW1ZPTFVNRSBDbGFzc10gLS0-IHB5YWxzYWF1ZGlvIC0tPiBBW0FMU0FdXG5aIC0tPiBTW1NZU1RFTV0gLS0-IGV4ZWMgLS0-IHN5c3RlbWRcblMgLS0-IENPTkZJRyAtLT4gZmlsZXN5c3RlbVxuUyAtLT4gZVtleGVjXSAtLT4gd2hhdGV2ZXJcblNoZWxsIC0tPiBCW0NMSV0gLS0-IHxaTVF8IFoiLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbldbV0VCIFVJXSAtLS0-fFpNUXwgWihwaG9uaWVib3ggcnBjIHNlcnZlcilcbkdbR1BJT10gLS0tPnxaTVF8IFpcblFbTVFUVF0gLS0tPnxaTVF8IFpcblJbUkZJRF0gLS0tPnxaTVF8IFpcblogLS0-IFBbUExBWUVSIE1QRCBDbGFzc10gLS0-IHB5dGhvbi1tcGQyLS0-IE1bTVBEXVxuXG5aIC0tPiBWW1ZPTFVNRSBDbGFzc10gLS0-IHB5YWxzYWF1ZGlvIC0tPiBBW0FMU0FdXG5aIC0tPiBTW1NZU1RFTV0gLS0-IGV4ZWMgLS0-IHN5c3RlbWRcblMgLS0-IENPTkZJRyAtLT4gZmlsZXN5c3RlbVxuUyAtLT4gZVtleGVjXSAtLT4gd2hhdGV2ZXJcblNoZWxsIC0tPiBCW0NMSV0gLS0-IHxaTVF8IFoiLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ) -##PhonieboxDaemon: +## PhonieboxDaemon: -###PhonieboxDaemon.py +### PhonieboxDaemon.py This is the Entry point of this Implementation This Daemon is so far: @@ -48,21 +48,21 @@ _Next Steps here:_ - [ ] Add Logger ? - [ ] Add Command Line Interface to pass config etc. (e.g to be started by systemd) -###rpc/PhoniboxRpcServer.py +### rpc/PhoniboxRpcServer.py This Server is ZMQ based API, which takes a dictionary of control objects (classes) as argument. Each Method of the passed classes will be made available to the API -###rpc/PhoniboxRpcClient.py +### rpc/PhoniboxRpcClient.py Module which is intended to be included by other python Control Class in order to access the API -###PhoniboxNVManager.py +### PhoniboxNVManager.py This is an Non Volatile Memory Manager. The attempt here is to reduce File IO writes, by keeping runtime Information in the RAM (a dictionary) and store them to Disk on Exit. -###PhoniboxVolume.py +### PhoniboxVolume.py ALSA Volume Control, utilizing pyalsaaudio hard-coded ALSA Volume Control @@ -71,7 +71,7 @@ _Next Steps here:_ - [ ] allow Card and Interface configuration -###player/PhoniboxPlayerMPD.py +### player/PhoniboxPlayerMPD.py Core Player Function, Controls MPD (via python-mpd2) Thsi si where most of the former Rsume Play and Playout Controls.sh went into @@ -82,7 +82,7 @@ _Next Steps here:_ - [ ] Reduce / Organize Json Output of playerstatus to what is really needed by the WebUI - [ ] Switch mpd control to asyncio in order to be independent of WebUI Polling for actual mpd status -##cli_client +## cli_client command line access to the Daemon API, This should be a very lightweight and fast interface-tool with nearly no further dependencies. @@ -92,12 +92,12 @@ _Next Steps here:_ -##WebUI +## WebUI Even there are many changes in the WebUI it has been keept as it was as much as possible. The changes are mainly in order to interface with the new ZMQ aproach -###htdocs/api/PhonieboxRpcClient.php +### htdocs/api/PhonieboxRpcClient.php this is the PHP Version of the RpcClient to allow fast and lightweight API access _Next Steps here:_ From ec97338b588a3b41ee6206e81cf3ac9796940eeb Mon Sep 17 00:00:00 2001 From: arne123 Date: Wed, 21 Apr 2021 00:03:10 +0200 Subject: [PATCH 020/606] removed ci --- ci/Dockerfile.buster.amd64 | 30 ----------- ci/Dockerfile.buster.armv7 | 30 ----------- ci/Dockerfile.buster.test_install.amd64 | 33 ------------ ci/Dockerfile.stretch.amd64 | 30 ----------- ci/Dockerfile.stretch.armv7 | 30 ----------- ci/README.md | 69 ------------------------- 6 files changed, 222 deletions(-) delete mode 100644 ci/Dockerfile.buster.amd64 delete mode 100644 ci/Dockerfile.buster.armv7 delete mode 100644 ci/Dockerfile.buster.test_install.amd64 delete mode 100644 ci/Dockerfile.stretch.amd64 delete mode 100644 ci/Dockerfile.stretch.armv7 delete mode 100644 ci/README.md diff --git a/ci/Dockerfile.buster.amd64 b/ci/Dockerfile.buster.amd64 deleted file mode 100644 index 7aafa047e..000000000 --- a/ci/Dockerfile.buster.amd64 +++ /dev/null @@ -1,30 +0,0 @@ -FROM debian:buster - -COPY . /code -WORKDIR /code - -RUN groupadd --gid 1000 pi ;\ - useradd -u 1000 -g 1000 -G sudo -d /home/pi -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' pi ;\ - chown -R 1000:1000 /code /home/pi ;\ - chmod +x /code/scripts/installscripts/buster-install-default.sh - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y install curl gnupg sudo nano;\ - echo 'deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi' > /etc/apt/sources.list.d/raspi.list ;\ - echo 'deb http://archive.raspberrypi.org/debian/ buster main' >> /etc/apt/sources.list.d/raspi.list ;\ - curl http://raspbian.raspberrypi.org/raspbian.public.key | apt-key add - ;\ - curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - ;\ - echo 'pi ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pi ;\ - apt-get clean ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y dist-upgrade --auto-remove --purge ;\ - apt-get -y install wget build-essential git iw locales wpasupplicant;\ - apt-get clean ;\ - touch /boot/cmdline.txt /etc/sysctl.conf ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -USER pi diff --git a/ci/Dockerfile.buster.armv7 b/ci/Dockerfile.buster.armv7 deleted file mode 100644 index 2041697be..000000000 --- a/ci/Dockerfile.buster.armv7 +++ /dev/null @@ -1,30 +0,0 @@ -FROM arm32v7/debian:buster-slim - -COPY . /code -WORKDIR /code - -RUN groupadd --gid 1000 pi ;\ - useradd -u 1000 -g 1000 -G sudo -d /home/pi -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' pi ;\ - chown -R 1000:1000 /code /home/pi ;\ - chmod +x /code/scripts/installscripts/buster-install-default.sh - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y install curl gnupg sudo nano;\ - echo 'deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi' >> /etc/apt/sources.list.d/raspi.list ;\ - echo 'deb http://archive.raspberrypi.org/debian/ buster main' > /etc/apt/sources.list.d/raspi.list ;\ - curl http://raspbian.raspberrypi.org/raspbian.public.key | apt-key add - ;\ - curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - ;\ - echo 'pi ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pi ;\ - apt-get clean ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y dist-upgrade --auto-remove --purge ;\ - apt-get -y install wget build-essential git iw locales wpasupplicant ;\ - apt-get clean ;\ - touch /boot/cmdline.txt /etc/sysctl.conf ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -USER pi diff --git a/ci/Dockerfile.buster.test_install.amd64 b/ci/Dockerfile.buster.test_install.amd64 deleted file mode 100644 index 54b78b78c..000000000 --- a/ci/Dockerfile.buster.test_install.amd64 +++ /dev/null @@ -1,33 +0,0 @@ -FROM debian:buster - -COPY . /code -WORKDIR /code - -RUN groupadd --gid 1000 pi ;\ - useradd -u 1000 -g 1000 -G sudo -d /home/pi -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' pi ;\ - chown -R 1000:1000 /code /home/pi ;\ - chmod +x /code/scripts/installscripts/buster-install-default.sh ;\ - chmod +x /code/scripts/installscripts/tests/run_installation_tests.sh ;\ - chmod +x /code/scripts/installscripts/tests/run_installation_tests2.sh ;\ - chmod +x /code/scripts/installscripts/tests/run_installation_tests3.sh - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y install curl gnupg sudo nano systemd apt-utils;\ - echo 'deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi' > /etc/apt/sources.list.d/raspi.list ;\ - echo 'deb http://archive.raspberrypi.org/debian/ buster main' >> /etc/apt/sources.list.d/raspi.list ;\ - curl http://raspbian.raspberrypi.org/raspbian.public.key | apt-key add - ;\ - curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - ;\ - echo 'pi ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pi ;\ - apt-get clean ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y dist-upgrade --auto-remove --purge ;\ - apt-get -y install wget build-essential git iw locales wpasupplicant;\ - apt-get clean ;\ - touch /boot/cmdlinetxt ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -USER pi diff --git a/ci/Dockerfile.stretch.amd64 b/ci/Dockerfile.stretch.amd64 deleted file mode 100644 index 86fc73858..000000000 --- a/ci/Dockerfile.stretch.amd64 +++ /dev/null @@ -1,30 +0,0 @@ -FROM debian:stretch - -COPY . /code -WORKDIR /code - -RUN groupadd --gid 1000 pi ;\ - useradd -u 1000 -g 1000 -G sudo -d /home/pi -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' pi ;\ - chown -R 1000:1000 /code /home/pi ;\ - chmod +x /code/scripts/installscripts/stretch-install-default.sh - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y install curl gnupg sudo nano;\ - echo 'deb http://raspbian.raspberrypi.org/raspbian/ stretch main contrib non-free rpi' > /etc/apt/sources.list.d/raspi.list ;\ - echo 'deb http://archive.raspberrypi.org/debian/ stretch main' >> /etc/apt/sources.list.d/raspi.list ;\ - curl http://raspbian.raspberrypi.org/raspbian.public.key | apt-key add - ;\ - curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - ;\ - echo 'pi ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pi ;\ - apt-get clean ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y dist-upgrade --auto-remove --purge ;\ - apt-get -y install wget build-essential git iw locales wpasupplicant;\ - apt-get clean ;\ - touch /boot/cmdline.txt /etc/sysctl.conf ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -USER pi diff --git a/ci/Dockerfile.stretch.armv7 b/ci/Dockerfile.stretch.armv7 deleted file mode 100644 index e37eeae02..000000000 --- a/ci/Dockerfile.stretch.armv7 +++ /dev/null @@ -1,30 +0,0 @@ -FROM arm32v7/debian:stretch-slim - -COPY . /code -WORKDIR /code - -RUN groupadd --gid 1000 pi ;\ - useradd -u 1000 -g 1000 -G sudo -d /home/pi -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' pi ;\ - chown -R 1000:1000 /code /home/pi ;\ - chmod +x /code/scripts/installscripts/stretch-install-default.sh - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y install curl gnupg sudo nano;\ - echo 'deb http://raspbian.raspberrypi.org/raspbian/ stretch main contrib non-free rpi' >> /etc/apt/sources.list.d/raspi.list ;\ - echo 'deb http://archive.raspberrypi.org/debian/ stretch main' > /etc/apt/sources.list.d/raspi.list ;\ - curl http://raspbian.raspberrypi.org/raspbian.public.key | apt-key add - ;\ - curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - ;\ - echo 'pi ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pi ;\ - apt-get clean ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -RUN export DEBIAN_FRONTEND=noninteractive ;\ - apt-get update ;\ - apt-get -y dist-upgrade --auto-remove --purge ;\ - apt-get -y install wget build-essential git iw locales wpasupplicant ;\ - apt-get clean ;\ - touch /boot/cmdline.txt /etc/sysctl.conf ;\ - rm -rf /var/cache/apt/* /var/lib/apt/lists/* - -USER pi diff --git a/ci/README.md b/ci/README.md deleted file mode 100644 index 2cabfd10b..000000000 --- a/ci/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Docker Test-Environment - -Having to re-flash the sd card of their raspberry pi did annoy ZyanKLee that much he created -this little set of tools to allow testing without flashing. - -This is a work in progress so expect things to fail or being flaky. - -## Howto - -* First you need a raspberry pi with some decent performance (RPi 3 or 4 would be recommended) -* Flash its sd card with **raspbian buster lite** -* use raspi-config to resize the filesystem to the whole sd card (menu: 7 -> A1) -* install some tools and reboot: -``` - sudo apt-get update - sudo apt-get -y dist-upgrade - sudo apt-get -y install docker.io git - sudo gpasswd -a pi docker - sudo reboot -``` -* login to your RPi -* clone this repo and cd into its local clone: -``` - git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git - cd /home/pi/RPi-Jukebox-RFID/ -``` -* build the docker image: - * **on normal PCs:** - ``` - docker build -t rpi-jukebox-rfid-stretch:latest -f ci/Dockerfile.stretch.amd64 . - docker build -t rpi-jukebox-rfid-buster:latest -f ci/Dockerfile.buster.amd64 . - ``` - - * **on a raspberry pi:** - ``` - docker build -t rpi-jukebox-rfid-stretch:latest -f ci/Dockerfile.stretch.armv7 . - docker build -t rpi-jukebox-rfid-buster:latest -f ci/Dockerfile.buster.armv7 . - ``` -* get something to drink or eat -* run the freshly built docker image and start testing. For example: - ``` - docker run --rm -ti rpi-jukebox-rfid-buster:latest /bin/bash - cd /home/pi/ - cp /code/scripts/installscripts/buster-install-default.sh /home/pi/ - bash buster-install-default.sh - ``` - - NOTE: Get familiar with docker and its flags - `--rm` for example will remove the - container after you log out of it and all changes will be lost. - - -### mount hosts code as volume - -The created image now contains all the code in the directory `/code` - if you do not want to -rebuild the image after each code-change you can 'mount' the RPi's code version into the -container: - -``` - git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git - cd /home/pi/RPi-Jukebox-RFID/ - docker build -t rpi-jukebox-rfid-buster:latest -f ci/Dockerfile . - docker run --rm -ti -w /code -v $PWD:/code rpi-jukebox-rfid-buster:latest /bin/bash - - cd /home/pi/ - cp /code/scripts/installscripts/buster-install-default.sh /home/pi/ - bash buster-install-default.sh -``` - -In that way every change to the code in the container will be available on the RPi as well as vice versa. From 02e2da1b3b2c7b9356074e48b33e8900e88d25b3 Mon Sep 17 00:00:00 2001 From: arne123 Date: Wed, 21 Apr 2021 22:16:53 +0200 Subject: [PATCH 021/606] added comment about delets --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9020e360f..812e3fe73 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This Branch is an attempt to realize elements from the discussion which took pla This is the first attempt to a new structure, many things here are untested and error prone. This is still in the phase of orientation and proof of concept with trails and experiments in many directions which all have to be understood as base for discussion. +In order to Focus on the actual topics many non needed/active items in this branch have been deleted. This doesn't mean existing parts are aboselte, things will be integrated step by step. + #### These are the Fundamental Design Goals: - better maintainability From a804bf4cacee766bd55b4f311847c37de84cf1c7 Mon Sep 17 00:00:00 2001 From: arne123 Date: Wed, 21 Apr 2021 23:54:20 +0200 Subject: [PATCH 022/606] added python requirements --- Phoniebox/requirements.txt | 14 ++++++++++++++ htdocs/api/PhonieboxRpcClient.php | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 Phoniebox/requirements.txt diff --git a/Phoniebox/requirements.txt b/Phoniebox/requirements.txt new file mode 100644 index 000000000..5ee6de9d9 --- /dev/null +++ b/Phoniebox/requirements.txt @@ -0,0 +1,14 @@ +pyalsaaudio==0.9.0 +mock==4.0.3 +py532lib==2018.6.14.dev1 +pytest==4.6.9 +nanotime==0.5.2 +python_mpd2==3.0.4 +components==1.2.8 +evdev==1.4.0 +numpy==1.20.2 +paho_mqtt==1.5.1 +pyserial==3.5 +pyzmq==22.0.3 +RPi==0.0.1 +smbus==1.1.post2 diff --git a/htdocs/api/PhonieboxRpcClient.php b/htdocs/api/PhonieboxRpcClient.php index df114bded..f2f190bc0 100755 --- a/htdocs/api/PhonieboxRpcClient.php +++ b/htdocs/api/PhonieboxRpcClient.php @@ -1,5 +1,10 @@ Date: Wed, 28 Apr 2021 23:50:50 +0200 Subject: [PATCH 023/606] added architecture image --- docs/architecture.drawio | 1 + docs/architecture.svg | 3 ++ docs/music_player_status.json.example | 38 ++++++++++++++ docs/phoniebox_cardid_database.json.example | 55 +++++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 docs/architecture.drawio create mode 100644 docs/architecture.svg create mode 100644 docs/music_player_status.json.example create mode 100644 docs/phoniebox_cardid_database.json.example diff --git a/docs/architecture.drawio b/docs/architecture.drawio new file mode 100644 index 000000000..c6b4efabe --- /dev/null +++ b/docs/architecture.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/architecture.svg b/docs/architecture.svg new file mode 100644 index 000000000..b77858a12 --- /dev/null +++ b/docs/architecture.svg @@ -0,0 +1,3 @@ + + +
PhonieBox Daemon
PhonieBox Daemon
WebUI
[lighttpd]
WebUI...
FileSystem
FileSystem
GPIO
[thread]
GPIO...
WebUI
Control
WebUI...
PhonieBoxRpc Server
PhonieBoxRpc Server
PhonieBoxPlayerMPD
PhonieBoxPlayerMPD
python_mpd2
python_mpd2
phoniebox.config
INI file Format
[GENERAL]
..
[RFID]
..
[GPIO]
..
phoniebox.configINI file Fo...
cardid_database.json
json Format
{
"104,49914": {
"object": "player",
"method": "playlistaddplay",
"params": {
"folder": "xxx"
}
},
cardid_database.jsonjson Fo...
musicplayer_status.json
json Format
{
"player_status": {
    "last_played_folder": "....",
    "CURRENTSONGPOS": "18",
    "CURRENTFILENAME": "....."
  },
  .......
musicplayer_status.jsonjson...
MQTT
[thread]
MQTT...
SHELL
[C]
SHELL...
PhonieBoxVolumeALSA
PhonieBoxVolumeALSA
pyalsaaudio
pyalsaaudio
PhonieBoxConfig
PhonieBoxConfig
mpd
mpd
alsa
alsa
audio library
folder for audio files 

audio libraryfolder for aud...
Outputs
Outputs
ZMQ REQ (InProc)
ZMQ REQ (InProc)
Input
Input
PhonieBoxNvManager
PhonieBoxNvManager
WebUI
Display
WebUI...
PhonieBoxPub Server
PhonieBoxPub Server
RFID
[thread]
RFID...
ZMQ REQ (Sock)
ZMQ REQ (Sock)
ZMQ REQ (InProc)
ZMQ REQ (InProc)
ZMQ Pub (Sock)
ZMQ Pub (Sock)
ZMQ Pub (InProc)
ZMQ Pub (InProc)
ZMQ REQ (Sock)
ZMQ REQ (Sock)
ZMQ Pub (InProc)
ZMQ Pub (InProc)
ZMQ REQ (InProc)
ZMQ REQ (InProc)
legend
actuel module in development
external dependency
lower priority
File, frequent changes
File, rarely changed
legendactuel module in develop...
Future1 PhonieBox Architecture
Future1 PhonieBox Architecture
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/music_player_status.json.example b/docs/music_player_status.json.example new file mode 100644 index 000000000..8e5b72ad8 --- /dev/null +++ b/docs/music_player_status.json.example @@ -0,0 +1,38 @@ +{ + "player_status": { + "last_played_folder": "b/bb", + "CURRENTSONGPOS": "18", + "CURRENTFILENAME": "bbb.mp3" + }, + "audio_folder_status": { + "a/aa": { + "CURRENTFILENAME": "aaa.mp3", + "ELAPSED": "114.455", + "PLAYSTATUS": "Stopped", + "RESUME": "OFF", + "SHUFFLE": "OFF", + "LOOP": "OFF", + "SINGLE": "OFF", + "CURRENTSONGPOS": "2" + }, + "b/bb": { + "CURRENTFILENAME": "bbb.mp3", + "ELAPSED": "200.556", + "PLAYSTATUS": "Stopped", + "RESUME": "OFF", + "SHUFFLE": "OFF", + "LOOP": "OFF", + "SINGLE": "OFF", + "CURRENTSONGPOS": "4" + }, + "c/cc/cccc": { + "CURRENTFILENAME": "ccccc.mp3", + "ELAPSED": 0, + "PLAYSTATUS": "Stopped", + "RESUME": "OFF", + "SHUFFLE": "OFF", + "LOOP": "OFF", + "SINGLE": "OFF" + } + } +} \ No newline at end of file diff --git a/docs/phoniebox_cardid_database.json.example b/docs/phoniebox_cardid_database.json.example new file mode 100644 index 000000000..69e1ad68f --- /dev/null +++ b/docs/phoniebox_cardid_database.json.example @@ -0,0 +1,55 @@ +{ + "104,49914": { + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "music 1" + } + }, + "103,12632": { + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "music 2" + } + }, + "104,29698": { + "object": "player", + "method": "playlistaddplay", + "params": { + "folder": "music 3" + } + }, + "108,07437": { + "object": "player", + "method": "playlistaddplay", + "params": { + "url": "url 1" + } + }, + "107,60360": { + "object": "system", + "method": "shutdown", + "params": {} + }, + "106,64513": { + "object": "", + "method": "", + "params": {} + }, + "104,14891": { + "object": "", + "method": "", + "params": {} + }, + "103,24033": { + "object": "", + "method": "", + "params": {} + }, + "104,32860": { + "object": "", + "method": "", + "params": {} + } +} \ No newline at end of file From 4bf61651ce185d2064e7cae47d200b21601ba4ed Mon Sep 17 00:00:00 2001 From: arne123 Date: Sun, 2 May 2021 00:31:18 +0200 Subject: [PATCH 024/606] allowed single process/zmq_context communication (inproc) --- Phoniebox/PhonieboxDaemon.py | 5 +++-- Phoniebox/rfid_reader/PhonieboxRfidReader.py | 4 ++-- Phoniebox/rpc/PhonieboxRpcClient.py | 23 +++++++++++++------- Phoniebox/rpc/PhonieboxRpcServer.py | 12 ++++++---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Phoniebox/PhonieboxDaemon.py b/Phoniebox/PhonieboxDaemon.py index 3b857563d..bac324e4d 100755 --- a/Phoniebox/PhonieboxDaemon.py +++ b/Phoniebox/PhonieboxDaemon.py @@ -15,6 +15,7 @@ #from gpio_control import gpio_control g_nvm = None +g_zmq_context = None def signal_handler(signal, frame): """ catches signal and triggers the graceful exit """ @@ -76,10 +77,10 @@ def exit_gracefully(esignal, frame): print ("Init Phonibox RPC Server ") rpcs = PhonieboxRpcServer(objects) if rpcs != None: - rpcs.connect() + g_zmq_context = rpcs.connect() #rfid_reader = RFID_Reader("RDM6300",{'numberformat':'card_id_float'}) - rfid_reader = RFID_Reader("Fake") + rfid_reader = RFID_Reader("Fake",zmq_context=g_zmq_context) if rfid_reader is not None: rfid_reader.set_cardid_db(cardid_database) rfid_reader.reader.set_card_ids(list(cardid_database)) #just for Fake Reader to be aware of card numbers diff --git a/Phoniebox/rfid_reader/PhonieboxRfidReader.py b/Phoniebox/rfid_reader/PhonieboxRfidReader.py index 01059d0f2..8cf357e18 100644 --- a/Phoniebox/rfid_reader/PhonieboxRfidReader.py +++ b/Phoniebox/rfid_reader/PhonieboxRfidReader.py @@ -50,7 +50,7 @@ def readCard(self): class RFID_Reader(object): - def __init__(self,device_name,param=None): + def __init__(self,device_name,param=None,zmq_context=None): if device_name == 'MFRC522': from . import RfidReader_RC522 @@ -72,7 +72,7 @@ def __init__(self,device_name,param=None): sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name) self.PhonieboxRpc = PhonieboxRpcClient() - self.PhonieboxRpc.connect() + self.PhonieboxRpc.connect(zmq_context=zmq_context) self._keep_running = True self.cardnotification = None self.valid_cardnotification = None diff --git a/Phoniebox/rpc/PhonieboxRpcClient.py b/Phoniebox/rpc/PhonieboxRpcClient.py index f64c08336..f3d166db0 100644 --- a/Phoniebox/rpc/PhonieboxRpcClient.py +++ b/Phoniebox/rpc/PhonieboxRpcClient.py @@ -4,17 +4,23 @@ class PhonieboxRpcClient: def __init__(self): - #self.objects = objects self.context = None - def connect(self,addr= None): - if addr == None: - addr = "tcp://127.0.0.1:5555" - self.context = zmq.Context() + def connect(self,addr=None,zmq_context=None): + if zmq_context != None: + self.context = zmq_context + local_addr = "inproc://PhonieboxRpcServer" + else: + self.context = zmq.Context() + local_addr = "tcp://127.0.0.1:5555" + + if addr != None: + local_addr = addr + self.queue = self.context.socket(zmq.REQ) self.queue.setsockopt(zmq.RCVTIMEO,200) self.queue.setsockopt(zmq.LINGER, 200) - self.queue.connect(addr) + self.queue.connect(local_addr) def enqueue(self, request): #todo check reqest @@ -25,8 +31,9 @@ def enqueue(self, request): try: server_response = self.queue.recv() - except: - print ("somethng went wrong") + except Exception as e: + print ("somethng went wrong:") + print (e) server_response = None return server_response diff --git a/Phoniebox/rpc/PhonieboxRpcServer.py b/Phoniebox/rpc/PhonieboxRpcServer.py index 627a520c4..ddede246c 100644 --- a/Phoniebox/rpc/PhonieboxRpcServer.py +++ b/Phoniebox/rpc/PhonieboxRpcServer.py @@ -13,13 +13,17 @@ def __init__(self,objects): self.context = None self._keep_running = True - def connect(self,addr= None): - if addr == None: - addr = "tcp://127.0.0.1:5555" + def connect(self,addrs= None): + if addrs == None: + addrs = ["tcp://127.0.0.1:5555","inproc://PhonieboxRpcServer"] self.context = zmq.Context() self.socket = self.context.socket(zmq.REP) - self.socket.bind(addr) + for addr in addrs: + self.socket.bind(addr) self.socket.setsockopt(zmq.LINGER, 200) + print("server zmq context") + print(self.context) + return self.context def execute(self, obj, cmd, param): call_obj = self.objects.get(obj) From c07e88b2b6fd495cf4aa72885af8c7e638cdea15 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sun, 2 May 2021 23:40:30 +0200 Subject: [PATCH 025/606] Added initial parsing of config File --- Phoniebox/PhonieboxDaemon.py | 46 +++++++++++++++++------------ Phoniebox/rpc/PhonieboxRpcServer.py | 2 -- README.md | 6 ++-- docs/phoniebox.conf.example | 17 +++++++++++ 4 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 docs/phoniebox.conf.example diff --git a/Phoniebox/PhonieboxDaemon.py b/Phoniebox/PhonieboxDaemon.py index bac324e4d..72b3812ea 100755 --- a/Phoniebox/PhonieboxDaemon.py +++ b/Phoniebox/PhonieboxDaemon.py @@ -4,6 +4,8 @@ import threading import sys, os.path import signal +import argparse +import configparser from time import sleep, time import PhonieboxVolume @@ -35,26 +37,33 @@ def exit_gracefully(esignal, frame): print ("Exiting") sys.exit(0) +def dump_config_options(phoniebox_config,filename): + print ("\nDumping configig option from File:"+ filename) + for section in phoniebox_config.sections(): + print ("["+section+"]") + options = phoniebox_config.options(section) + for option in options: + print(option+" = "+phoniebox_config.get(section, option)) + print ("\n") + if __name__ == "__main__": # get absolute path of this script dir_path = os.path.dirname(os.path.realpath(__file__)) - defaultconfigFilePath = os.path.join(dir_path, 'phoniebox.conf') - # if called directly, launch Phoniebox.py as rfid-reader daemon - # treat the first argument as defaultconfigFilePath if given - if len(sys.argv) <= 1: - configFilePath = defaultconfigFilePath - else: - configFilePath = sys.argv[1] + defaultconfigFilePath = os.path.join(dir_path, '../settings/phoniebox.conf') - #parse config - #gpio_config = configparser.ConfigParser(inline_comment_prefixes=";") - #gpio_config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') - #gpio_config.read(config_path) + argparser = argparse.ArgumentParser(description='The PhonieboxDaemon') + argparser.add_argument('configuration_file', type=argparse.FileType('r'),nargs='?',default=defaultconfigFilePath) + argparser.add_argument('--verbose', '-v', action='count', default=0) + args = argparser.parse_args() - #read config to dictionary? - phoniebox_config = {} - phoniebox_config['audiofolders_path'] = "../shared/" + phoniebox_config = configparser.ConfigParser(inline_comment_prefixes=";") + phoniebox_config.read(args.configuration_file.name) + + print ("Starting the "+ phoniebox_config.get('SYSTEM', 'BOX_NAME') +" Daemon") + + if args.verbose: + dump_config_options(phoniebox_config,args.configuration_file.name) # Play Startup Sound volume_control = PhonieboxVolume.volume_control_alsa(listcards=False) @@ -64,10 +73,10 @@ def exit_gracefully(esignal, frame): g_nvm = nv_manager() #phoniebox music player status - music_player_status = g_nvm.load("../shared/music_player_status.json") + music_player_status = g_nvm.load(phoniebox_config.get('PLAYER', 'MUSIC_PLAYER_STATUS')) #card id database - cardid_database = g_nvm.load("../settings/phoniebox_cardid_database.json") + cardid_database = g_nvm.load(phoniebox_config.get('RFID', 'CARDID_DATABASE')) #initialize Phonibox objcts objects = {'volume':volume_control, @@ -89,11 +98,10 @@ def exit_gracefully(esignal, frame): rfid_thread = None ##initialize gpio - #gpio_config = configparser.ConfigParser(inline_comment_prefixes=";") gpio_config = None if gpio_config is not None: - gpio_config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') - gpio_config.read(config_path) + gpio_config = configparser.ConfigParser(inline_comment_prefixes=";") + gpio_config.read(phoniebox_config.get('GPIO', 'GPIO_CONFIG')) phoniebox_function_calls = function_calls.phoniebox_function_calls() gpio_controler = gpio_control(phoniebox_function_calls) diff --git a/Phoniebox/rpc/PhonieboxRpcServer.py b/Phoniebox/rpc/PhonieboxRpcServer.py index ddede246c..0797d8974 100644 --- a/Phoniebox/rpc/PhonieboxRpcServer.py +++ b/Phoniebox/rpc/PhonieboxRpcServer.py @@ -21,8 +21,6 @@ def connect(self,addrs= None): for addr in addrs: self.socket.bind(addr) self.socket.setsockopt(zmq.LINGER, 200) - print("server zmq context") - print(self.context) return self.context def execute(self, obj, cmd, param): diff --git a/README.md b/README.md index 812e3fe73..4e1e6a52d 100755 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The work has taken place in the Components Folder, which has been renamed to Pho ### Architecture The Fundamental Architecture looks like: -[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbldbV0VCIFVJXSAtLS0-fFpNUXwgWihwaG9uaWVib3ggcnBjIHNlcnZlcilcbkdbR1BJT10gLS0tPnxaTVF8IFpcblFbTVFUVF0gLS0tPnxaTVF8IFpcblJbUkZJRF0gLS0tPnxaTVF8IFpcblogLS0-IFBbUExBWUVSIE1QRCBDbGFzc10gLS0-IHB5dGhvbi1tcGQyLS0-IE1bTVBEXVxuXG5aIC0tPiBWW1ZPTFVNRSBDbGFzc10gLS0-IHB5YWxzYWF1ZGlvIC0tPiBBW0FMU0FdXG5aIC0tPiBTW1NZU1RFTV0gLS0-IGV4ZWMgLS0-IHN5c3RlbWRcblMgLS0-IENPTkZJRyAtLT4gZmlsZXN5c3RlbVxuUyAtLT4gZVtleGVjXSAtLT4gd2hhdGV2ZXJcblNoZWxsIC0tPiBCW0NMSV0gLS0-IHxaTVF8IFoiLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbldbV0VCIFVJXSAtLS0-fFpNUXwgWihwaG9uaWVib3ggcnBjIHNlcnZlcilcbkdbR1BJT10gLS0tPnxaTVF8IFpcblFbTVFUVF0gLS0tPnxaTVF8IFpcblJbUkZJRF0gLS0tPnxaTVF8IFpcblogLS0-IFBbUExBWUVSIE1QRCBDbGFzc10gLS0-IHB5dGhvbi1tcGQyLS0-IE1bTVBEXVxuXG5aIC0tPiBWW1ZPTFVNRSBDbGFzc10gLS0-IHB5YWxzYWF1ZGlvIC0tPiBBW0FMU0FdXG5aIC0tPiBTW1NZU1RFTV0gLS0-IGV4ZWMgLS0-IHN5c3RlbWRcblMgLS0-IENPTkZJRyAtLT4gZmlsZXN5c3RlbVxuUyAtLT4gZVtleGVjXSAtLT4gd2hhdGV2ZXJcblNoZWxsIC0tPiBCW0NMSV0gLS0-IHxaTVF8IFoiLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ) + ## PhonieboxDaemon: @@ -45,10 +45,10 @@ This Daemon is so far: _Next Steps here:_ -- [ ] reading of Config File in order to allow distributed development +- [X] reading of Config File in order to allow distributed development - [ ] Refactoring as class in order to allow the exit functions to take over more tasks - [ ] Add Logger ? -- [ ] Add Command Line Interface to pass config etc. (e.g to be started by systemd) +- [X] Add Command Line Interface to pass config etc. (e.g to be started by systemd) ### rpc/PhoniboxRpcServer.py diff --git a/docs/phoniebox.conf.example b/docs/phoniebox.conf.example new file mode 100644 index 000000000..cbe31a740 --- /dev/null +++ b/docs/phoniebox.conf.example @@ -0,0 +1,17 @@ +[SYSTEM] +BOX_NAME: PhonieBox + +[PLAYER] +AUDIOFOLDERSPATH:"/home/pi/RPi-Jukebox-RFID/shared/" +PLAYLISTSFOLDERPATH:"/home/pi/RPi-Jukebox-RFID/playlists" +MUSIC_PLAYER_STATUS: /home/pi/RPi-Jukebox-RFID/shared/music_player_status.json + +[RFID] +CARDID_DATABASE: /home/pi/RPi-Jukebox-RFID/settings/phoniebox_cardid_database.json + +[AUDIO] +AUDIOCARDNAME: +AUDIOIFACENAME: + +[GPIO] +GPIO_CONFIG: From 8b5c39ca1b753d3af8d1da51b62d104803448db7 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sat, 8 May 2021 00:06:48 +0200 Subject: [PATCH 026/606] added startup/shutdown wave files (#1401) --- shared/shutdownsound.wav | Bin 0 -> 81226 bytes shared/startupsound.wav | Bin 0 -> 259632 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 shared/shutdownsound.wav create mode 100644 shared/startupsound.wav diff --git a/shared/shutdownsound.wav b/shared/shutdownsound.wav new file mode 100644 index 0000000000000000000000000000000000000000..a51461ca3a92699b93acd6bccda9c444f1c23e03 GIT binary patch literal 81226 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%0AA7{kB5kMSjjaW41R$I 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

SqBOYlB_4{%1&V3M&Z*ug9minjg^ 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 aEI$TMpQ(Fs~Dv4_{1sCp`Ni{PdPIZjEE|25E z>c!3LiXRDI!j3$zj<Wr@9{MYg6$*EA(00<0M^^smY2#tN>-SUnOi2(>ovGg>pwqsDdcShTc!l4su1=FyZ_4oth9{k2Tf1D}A^cE4s*kGxaU{Aog5rx^CZd zWBFqLi)D{(iKS;_GsTj0Dk^r*&)1B-WAARF#=8}YNcBtZ7f-8xqYC}_>tQh!3|+ycPZq4Vzf(_c!(1` z#vUB!HeTX;eCKM{H?FU!f4`9#S((&qnW$HWy2^kG71qVLw}ory4~U+q3JUCG}Ly5&Z!RwJN$d zSRDN*m>BJaH7y?`CQRp+b-F^MycXtr*C%NfY{t?~wny8-ijV1omzw`Ni#_`}%%nQ% zh8mmw@sGmI;*KxkpHLruA^)*8oMUe+u-=Q*KDl$MB)aKCItdH4LCkfL-aHrl@QfWd z2$y9}as*bFvz-eWOuv_}xi1*)`B2PgqVN6FJWEmENo|LoRr$1DL_SSS&?UarU!N`; z*v$?rBZ5e>M!WPK(0v;|PAh+NBUXzpy-avi+y$ahlgK;as2ifF-{|!&mk8O|}oyPj~mWipaZmFP3gJ|SY2(pNtl9|J~ z@!aa4vxl$BC#n7o>&t=Gig%D-8bk3kk3#X+u#~e`Ohs=g+Nk33H*lbzt9mg`iX1W`Ox|?_UqJMYy^_RsyFY6uo zD!>1#8k5(YxE^BqL{__(?8so5?%^VaiBugQ$InnFToy^I0K(xX>mO1!O^X*Ud~eHtD4o8``cr7z|kJQ?XHqtGtW#g2VV z%+w#7I#`Xx5bW$AdD4O2Uw4_=ZgQxNtx#R~u>^mcUp6_NYT}5q^$#WLpU%$}uY6H| z%By$@md_r{t?s=9g+xu9R8y)(eY4dFR~JRJ6fwR?Co`~WQ zoYw`h(HR`pQBm=349z+_Wifj^&UBRS@*NFLEY7FjcS_o*-TD!)jBi!H`f;p1t!n}w zC&w$q{t64~i<(31o*_0j%o=-_)f^a>i1iX3y%2VEy%Fn1J=X_%eN)G;8F0ckQ0-p# z{jN-aez2;lzOyc=sUsSRK&RrK_lfB<(%m$c=NU@RyNXiq_h5>gIL#JbvbtI|+>Ys6 zIyFRFM%qNH=!{ap3=`@Gn)~X}UHZ>{p}PAc?BGaO57Vhy#S57}kyBs(+$I%die|$e zri-VKW->b}z32I9-%7?Gjn<1-igs5WK2|;JYWn>X`VVI|XZcArbMz(AMUn2&<9djt z51Pw1*2K-1icU7!>ub@dyQi@%WEH6IQ5^)5A-?}qNgj!RM|rV?C3;)sWG|JIZRjfNJ0I1| z5v_m?tH{RIq=0))e}L}PaO0h!PjQt8!$RzECEY=ur#I+}7n`PM?KhaQ{c2lp;p4Nj z_LLy-#~xYWJ7U5DGV`@~vKM6L24R#ZnJ6^F<3||Nxw0J}=s!A!dZ$nDDsAMmI`kEz zq)06iJ%uUxn%-=pPtaZk@8dkd1FZDvu#Hbp0`5q{gPhWF<_DN{8B{-=g?|%MKS1R| zM{Gc=a8B3|Z(j?deM&Zox?9p3aiH7b+5OB6m&RY@N5_byR`UP*?1w1E;NeJXc2c(jJK=4w|FOOC zKksFoIb*ut;|iAh{~}-gK(29weAjF4PIJFi1wt(Vp{5q=Uxj}6P*rSDy*bCLjHMdu zDT|Ilmn(cn_GbtVZGg(oUf7^M_a5`a2gV=u^(wyK#IEV)H%DS!r>TEisaA7ae2a7B zllz;=7{$^U+Uhc8ZDsvl5djX7?;7uOB>1KeVdUvf)-)$;vfVKb5B4SxJb-QOiF@rV zs%b3;)_`)Zsty8=^8MNQ{=4xfd6!4Za_{X-*66NsXEYl=Nf)a{Z0Slh z%NxzJ|4rxiqpm+ALsj(*iUz?edQi8G-XA<2jbr?7vZUwKdhDTjTFV;DmC+eNz0uVf zsmTgFVsE6tqhFM@-^n8`=OrhpzU_}W#zyLR(##Hg%(|y#DX;2k@*B0|dJ3Oq;Yr@< z0y}U%9IldPKp0~u(x}kR6Ym0RJ y9?#wY8fX}POJ#FLrFbTIt%|6%8#_EPQZ@V{ zQcZOE1Re7eVT#}}%CX17lCCO2S-twog-xi=+N-2#L)+XoXreyvdCHn@VFRkj+EkO3 z=v+&x*?UB^npNMaOlm|UH2?p=+2=5Q2O^bW&E}Zvm&Hm$aIKT4i;)RjnM%Z`-rUXS#``8orzn};T^f(6ukcZ7_kQ;ogt*U{BHtDWP};7LZAnD zlSF5^*^tL*sN+>v+wg)~`hRtQSrl>R+9OO`$l^>^+6YG;TDCX;#P0mQ+XD^$Kby${q z3oEjMjhzjHjgp}@yNFfJjEM~0slRDV^;~dz|6z;Hx=;IAj&1DRx9-yC_fSbr3~f=E zs+v6VGq7YE)%h<%(!JzA2B~muhluy}8OwF%+$-)|!NdeS|5!tXxfH}m_+%#^grky#M+$0|$aOYtpq#uoVZ zLLBcx7Iz-b_apJxEE<4mGyoI)jkmm`*Y%=!+23vB6`t{mRrtapUMr_}5#{6l#mSy< z4|m0fir@yZH2rzfS8<_jMHh{!-yV0L^SUol-u4W2r;cCoCE}g2aPSK}aUJ=gT)H>h zqR-ofFI_BN8(}tg3w6ZhMf}Oo$Um{M;qlmbU09}s>ti39^SvniJodH6EjX$J;ilLH zm347FX)~B~RG6~Aq6)8P;!VTOG|zhMhoe~84`fZKk#y>)p*u(~(d!H=v(bwDWkoXE zP1Wdzy1E;q?pdMYyw#JG$8|)pEwF=~Frj_qQAT1zr^*C>M(>lz0v}887!mzw0-gML zMZ*x@U@{*a+LG@l&;Iuwr zKVcca_Iooijl)b`?1IOyCF5B*IOtq_16NGqOJA~%l|<9Ya%ZPt!Zjj@2{bb=ItA5r z_bDV>61daXeC`ACz`wW$o78`=!^i-d;Cqe>~rA=9gN1tv$C|M?bk+pZ4W+o zg1_+@zq!MzU7%!BFNK>q44)l=DUR}m37>hCUi5dl#DhH29=z8tc=7EJ)b}zYi44YP zVwc5Y?CE+XPLUZOE!RF=TsIIV=@rkXekL`J>jv)Jd^E50lvjNMk6s4)EdXKUWA(GB z9?HzqXMzKgdH!TMRB;Y3FiSngGS?QW`6IB6X{DImETZO2qLKT>Wl?+crgQukW#k{S zga^%I_?1=rQN7m2aHUyRU#eMI=zPyHF?}MP>=4&$uIHTlI=)woEyzWMbe{@}Tlcyu zU&G(2j)gd=GUbVTv&&QLtPQ;OG4DSOB|=f3zL|_#PuBc{$WQW=hbU;z^40&yqo;`E z4$?=;1i9pL3MMp05OZ<)oRac6@B|w=7$@HcOV%Gh-#yT!CeR@OGtrdltd@$gO2Jru zyDv7P18%>b*uK0#mE~V{Bb;!*#!($*i)WC|pom?}<(# z$q{$}lU>Dco8hl7O(8ql^lO&qY z6u}DY*<3oy$>P%y7zI@>aOWy({E_VEM9+J8{@lH_?~jWd%3i(!=MS}0hsK9n=`O1I zYPgEpff*^CgG9egmf~FavYwKiF{CfC8eQWjA*#z*m7C6Y8o!(0PN{4Hbt`{qpjDUw zGpw^C4m#c1oyW6_=quq&o3n6z*j)4JWv?1QrX75qmm&87kp2i~d}8E&s>Jlcbmx59 z|9_l9EjQ6MTBc-RB!J*=`@P$&;x#tnPgwg;mi)APa#k+&v~zJP!m>awl;YIp5AMDmb`ucqaQa$mqJF`|KIG;g>LX@EyhON>!YTLidu_{V?3ACS2qsv-2+9 z&19PUabXJ2cdG+`PgK{2vMsagFw8T<^}LuUFU9jI^=->z!^7dRVPP|s$K_&Eb?aXe zrqSi}?%iEsGsacb*F22a`lPw44h+PNqkX(J+YRFVz#&1)(H*urB+yZI&m4U+F4&U#I@zvsXNi|y4lZ0&cVlV8}{ zBXG(oGhnX!%m4CAciE5-PPpxK{0-Nf_v|-c{qY{6xr4pG>+jtVtqKbADacig}-L?|Ec z@Xl$Rql|n>KDhWXJMSrcZAblPhKYn`ih|a{APEh_EoVC$>scnKE~0KDs_2b>cvs!( zC%A_#Q0E`eXBhb@$Qx|OT<(WCe+^!C4G(tWdp6pqX2kN?>-7!TDSN#eGX33-KESL0 zF5doCEWRz+=&wwPETNr!Pdqd@_>W$G2PN=yRbns8x>a@3Gei0}))!Np#nQlD4K(Ta+Fprz}8- zWxlM>+s}B*HE_cWIipeW&v~Ln^z#etrMa>(i(u;|`el5R>&>y=`%A4Jug#Q(cth96d&M zlrPUC2Ct-JQd4`WkLhNkJT9Q1Ss6^wVeKs)V28vn;qR~e1a~QHB9xOsw35fNI>Z+7 z_5AS*^!UeRg!kxmxK&O4H#DQm)yd#G4JKx|mFtJWmw1LM~@DG`t#nv*BJeHi0^8h-jfJKib&OmZurW%bP|q z_ZRrutsTTCD>fm1!OlL)6CI3g(o6Oj4f)?ZQ+ji`3aYoSgg<>QQbzXfsc^7a3UBL=^1i;e zbL6o;mI+=&pCrPe(RkQZ6ADXocpAbMO%?NfAzt}OW!GtZ&KOx}M!sGuyIP?grD z9yY6?Pt}kHrja=5DcZieyg*g>s4`s_pRZo*3afM#+Pp!>a}%o+$|;1to=U8g%3n$+ zs!InqW%m0I`a3zrX}LvIx$vb0Tsg&c55XSEIFA(A)H^b~7oE7n-q|)gah+&&8LszZ z`k)!=(ZKF34sRGoQ2Mkuh$3SfiEQpYt>+tnW0>=m4}YxtWW3pTPe7P3Be zvMYo-n2nuA{khB;-(s@Bel-wROj@}M5oDCj$)ZlH0G(37!2J(~>p;+r1*jXz$_k{G z`;6Fke~TKJS7aaE9|bcTD>t|+J`Du=qO473ALI)$4U zq01PdQ<%GhcHkaU`gX9f+f*y+q3UXY$!kJ&*NL6&8P1hgdS5t2A59#v*yfmG<3wpYUgjuEU~$i%$J*dd#%2NH&`0 zVo*+b8R0tACyiv=T0lYVnMLe52E@@ zr(k)aG*9=%pK?7clDNaqAI2;#PfNX=h=J zjd=Hu|p-OVwBgi(i`V@nK&tqJrd6D#x;rM>W{wS!_V(upO+@&&-`Pc2(g%HIy`KEs%hlhgwEc+jnuW_A+QuFjVY7i=l-BL zW50Tey|BqH@y8}gzAxoX=Cd4Q@h1IXn09PX4OpiXKbR@<4gB*d9oZuLXQnuEqW`~P zH@)V+mM~@=U7(AJjWMYBdplV_i$1SM~Lxj?jIe_qXiU z_goA7dzJ6~0Qu?f5T1elZp%MMaajpXT`nG7kJL| z(8gs<{SByDCW*gjOD(N(jD31o+*lM|tV{)2)wyZFFF)&kwHGR$HeZ^q%IK2Sxq zlmcd@yZgPY@sA?5pCI}j@+#Y%s7(;aYFXv4sU8-4zDSq6>EehP>UPJuCbCV#pvE`k zc6!R5yksrgiE#BjP>VAqQj=$^9O*0$Zo-o5-fBH-@ewt#WzX^o%`s?QuxYPa(?Rgj zTk^tFc!4>*!D3(knj(C)m~b;|zsa>lJh9Q=Sm*tJA-?#8HfNf@J5J3(e{tjs{6rJa zD|wZ?{+-&ZpJnTJ$>)8l^5s4C1ij<~TDh~8F`UKhlq~Q@8cf7pHBF)X_CMins`lM> z$8XN!Z+6Nt-7k;Hrd`6UT(^hx+I6S<;fn^j)9>&nqpk9IsQfKDgW>M->pWstpQ;tB zRnsngOx__UpA=DDaZzUZr|?D0XgihPja7uyk91>Wo2vkSGR$dOdj@K?l)7@<;>~Y3 zbD}u-+CYbpC)`$%)TQG-b)V_go@Z9q`H-HYxp;v5I$h*a?V3N{A4}04if)bXe}=NLVvt&YlLxUF z$rz1%a!y6DJ(YO~>>aq_og!W__>n=^I)I>McPfSVDAFmcr#xeM5@}&qJ(DFkTHXST*r0 zPf}!8&fH1?t~`_-5|YXt|jfZ#enY@OmI)m)Zf z&x+%xinCvjIvI~ZA`iQAtGv(Ybfl+iON+~ii;M1x{;%?U7h(T1>hkvA>j4wMF%RPX z3W;oWL|`S~;IF#FE-hfWS~84}xgYsN7@1gzfETzKF4d{_GZ^JFS%=TCl1m~xq1GMN zZhJVxdX1wPd`q^mKlNY_O8bP0vjwYMgMPe<-e-@BB_0+>r3+m!2nCMqa!wTJll-(kYdINfz(zE4Cd zGvr3zcKUllqqGC!vj*b)x{%isFu>!`)}ubjBi^B$d00rLQ9*fK z5>2S-U!qDLD1Q3@_xgz(%8&fb5w!z1MM#;jmxY7oYE*`b*=Arnf3PFZ!JTP!%P2#E z*8&#m@BDs9QL<7W>t7=ssFLf;imN-2KYA4ZR2S}S!v1%`NxdvT-`8*Sg_!!Ngm@K7 z>P*GmGT0|xPiXPxK{pe`>?YF(T_E6ABGS6}&uZB0(pZZ}efqqZh8!})>1ZwPw}V1w z>;{{6SqFtfVO`x6>N{Je&as6HL?h#6hTp)_bzvu)V38|@{X{9RM>2{1|BUsc{OCym z@Rmh@e#29QyZ27ECux=cOf2At;>KJ~C&p_7D2WE{QH|74g>@;P_qa0-ao zt3WYL+?f~H$X>x%^TH-XhT}X(cpSnr_J`JA3tl&OraSN13hVZi3hN3GypHDV=l!yL zCLyXn`iT;4rIY`WzE_EAh<={qX(P|Z|8UO!axQgT#0!^qo*x%;mvD~D#LKgyH38>1s~d*hPf98WgvcZpm=>iQ1hN@__d%0ZF4!1@#C`8B~1;;%SPnXo8m!V6<6$TC|GZqJ_<^%NRcvjfGcDw~p)l zmn!Li9#Ff&%dObWCjp!MbExw$_X;!qpNpL!G`@4;zbNu~p zb=BA(zZLu)KPz9bQC{KmptzG$6^B|+t;n;H(m`u1XKQ)YcF@RkqW6Y;YZW-P7>tq; z7QHR2{5w{0z5MztY~lzQu?sy&eTtzn*rS~BP*=6zV|UceUJl2{&W10>j)e7Nr^3>) z6Le#Ls;s#chIj9a-@02ce)De0czCy&xiBx7COJC(aO`9A6V|E4IADigkI#=~pphy< z2UQzlc#h}liRT_HCo|rfPR0RE$05GQlT2mRM!UnKV5mOO*NgNe&#MfmZD&6LVdsJf z(%Y5SXcmvLBHM9sD{&hO`KqaQ+gQ&>@ft&XZz#_(g!bVL-S*zGLnk=%v&EjDvVt3& z`NI^kSH(S!KDe;Wr^|JhCV>Yv=T+Ewd_F3-6 z0(B}Yg9j;#P1a#U*9Nt8Txf*7>>_^b1{V!x6-~Nj2Z}?&X{eA*cZOGcLRqh2CfeI0 zEp-!kDiVQy4?*QCMJW?xb6)0*D*qrooh5tHb=h&s)Dj0tfajZ6t_bVsv6S3S} z3YVEq-V|K)1nkZ@Ih(iS9!EQ6BmB+?oot7ROa_Te`iF;AA6~=?#hk{}x+vzxi#|+^ zQ7KYN7u@{ev+DcX(Ivd32WoFUzx&fJ3^4_Mpows=yL$Rs7Zu9wA_aJza=cG7JZmqt z3DaHc{H^n@%zS1|cebw;Sjcbh^_UhWsl;pc!}iaI&G$k1No-(g5kgCTXQ0V=Gj#k} zM!m8=I1||({EZR5sYgwKKaR-srj+T3%VdP?={5HC0)F%$_G3F{ZDr(yU9?fmGTZ(g zB}VLEhc$xqOYy@6?eBCno=G@@n_|GLn8u5=OqXez&WoR}(gWUt5mTrr%Z6W9uMZ`6 zX5)L%G4!-+2ig-uaH+#gA4t^ACwBNKJAJ4KIkEG5y1F|Bt<=>&%@$Ykz5LWdnfz>` zrvHR|=z84QC-R?T+`V3y>4sr8T^4TBFCV}pufe@7FeBRv$A;>{*faip?4|gRu3tPq zr6TEOY@B|2ld&lOC5;j1BxYUYDJ% z5$&y#ym`Dt^znG!Xl@$Ol;)8B9bOAg=ti(7+^Z-47M)+#1Y05V9VW%?2~}~1Z_AJN zkso~p+tC8@f0}nH8<>S1JR_IdlBanQ!tCYlzTsVuq7@t~i>(8Qm?^tEeuHN)S6NgR+LU-u-wQ z_N@mgH^S*Xi_y?cLp*s52ELBFF*}=W9m^_CvG1qRy?nriPt#j#lIxv&nw0`hV0N#Y zmi>u)&A(ykBYf>PJubd*R%c;J-f}v6;N07=KTq>p)u{!_;T+0~p(^TEQk~BKSy690 zEXfesfGHy7ud(s_M8%0|Nsro(&qH_bK;^69SsIUcHVXN=II~`|>(gb~*Q;ASj8nTQ zTc0+XMJ+{1bKBVbsQF9L_G(wV>5AOb4(+e&m+6$jAhG2@^{Zwh1+U2scVmGf zY^KW!f39X_6NSQV%7hD)3IB){QrXAZgHsf1m%|dlU#_b%%73{og?Yt%IfG+iO5GN( zi{Fnz;G4XL>Br$1@nMI1-P{(*^L`Ybr2gC>=6nYS*C{?fET?S|eV1r(R z|E9xy+hjVfio*-Yrq#z|zV4c?8fJZDO0X9m)qRgWZ7O42DafYA^&$2lH{_pDZVNlpRxY(9KEQP207;= zWb~$BLl@oCC-zf^GfR}eEllT}m!hF+ApUw?l=nXDy&kqWim^&bHJl&sUMDCKw3e~# zFONBrid{Db{kcm-=jc(x*V7ygjJ_3&iuMU6L|X@MN2{t?FA((81Ef>*kohyqbYy=U z-`Npw`nZl8Ss;dsP{ejvc2T?oyi`F|Y<^zjwy5ufcl4dObOE$B&i+wzrVHx~dvy|D z`>uTE_^@bX0^dDZ4s()EFg4UUA-oF*TytHNe?IK`RSxny4CViz?OFENDF1#rzF5bA z#4Mi=%(0sgo>TR+J{%H$M4LXy)go*YR-|#rL9c!%);K&Hdp6weakWlZi&QI4kG-r9 z(O^~G6ce$}RWO=J8T*%=eMtA<1}0he(YHv?Td&^DyP6_K-4S+;nBk_{W<9KR0U}5j z9FQx_B>t%`g6xBXpJi`ujeMf+bEOVGUk4A%7FXAAz9}_hhhVaMKS(~geeg<9(`@?u zo`pEa6a3j~EY(~nQKl#8N;y^s8YoYdP|)7ZLPe5}h9RYT+mzHJDdF?9JYhC`W>Na_ z$~fj0*sk8V&52g}OGs^}zCl;8UTFfeed*0=s-bztCr#3xL7^C`*SV+ za(GbY*eu0xMLY+zcAqTBP5HUMVS!7r@%lp~o==V6iOu%CC4Ocl9rRlE;3te#VxH(} zeRR)S;TsfZH}KZ~(Q+l~9RAi3;RtSckG#n@7_fQtJ5*|-kZp88M^r_fO?XmymKfby zdiGGJ%)aW1ZK&fNma^BfvXl40hIT{bs<`@kOs~DzP?fW~vrsA&!h^}_h%k<`N;lch z)F!qRVH+#suv!Lnq2@lU&|Dt$Cy4n^om?}s=%pe@M82nGT~0&Ge*`_$u=I}nEP7rW zyeQ++oK~fQ*~%sL&dFze?m$xKuqYcb!AtqYx3Nb(=`&i$s8`47mBqdmgqZW{ADvGg zG(VJFN?k)0yIfZgx{JY7czx8}4R!WMWA{FA?w31RdKBR2H_@_P!u;e5>SHjv$NS0_ zPZ#N|67}qpOZv->is3mk$Z}+gzUmsHWB=I5U4H&DHe_G$j!K-j^z$Ad#(xE-ZDm*1 zQJGLWatc@U9dA3$Cm2Y1-I%8>&r&8P|Aka{XMKu&x}p17s z{t3IQkqc7LY$mI7&(5air&cZy^a+ZXERoxJ%M+AV5uXY6zasuj^jrT(gg6*W&11{@ z49BeX^ty>Tj$Zc?N6BfGbQJyh?-V|?e- z{7lDv1ebmTubxuum5&dr=x?=!KWj&M{~`?@GOCPGY08=3)z$}F3o zNvx3`%1Q-wN%M&>&WbC(m!F>-SH*_KY)UO%+`6TTFSIYGd*!L#(Kvf>inBjXBsPtY zp9$A5#+iIWO|sc_#C6>{&mcN0Axdi!>8$#D5a#k@mD!tddl$poa8?$z=*983)kWhC zf(6?&@ORD=R=qfey$sy*82nQt!&AJCIp_rGJ_-5e60b+&rD#rbs;$jH2^8n$Zt!|nsq!!Kf|ou1#pC_!in-_M zAhIm?v{pyLa?oI7EOA@+Z*ZiTuA5Kk8{I+8?i(13ne;PXtK{70-k!&YrEvc;s;@5S zvOna7OY`D|u%5}mV^C6J9(5r(vaC84rKjrt4@$oV1)acU?`M0rsgn7D>SwKt-C8I8 zTPJxV6CT^;lkAXdN<9CL*WCc!{Lj8#>UTbJ2dCNR?_z<( z1om`3ds;pM7mJ&Lg0kV=vh!5BA3*VK%@1ma`F}1_o#kpR_IO21GFCjY*g4rIQa%p@ zW)jK3Q8=A^)GIly=>5)rlC}JchV?w{!@hXs;AeWFRr-*xP}My}Wy3hxkC)`b+uO6% zbQCBH7v%J_8E}wMzVlXiSpTF|V(598;bE?hu*8$HWqH+rrVwkKp#=JwI%uT|-+8j+ z6WGx=-R0h571INFzwxZ)Y?0#^vOgQdD+fd~e^3VgXEh$c3go7idc?b}ge|CniFpPy z(L_Y{oX^oh{;-8yO7qBP&iPt;oXzqqyTluNA>-fu^kH?Y`>;3%{Ps?Eew%%;j&)e! zTI!lA(`d$vzm*yzm=-UQ+Fvpq{oX64U^yNTwcPK@c&{5wB}n;6mbAWTt+B5@@9N;a zb%R#yeQK84{?=2Xxu$BspR?yWd4JtyYJ1D9zUeh}mh*}qi~VM~%oW5sP51jV@gdVO z)gO3;4_K;6lxGuU$Hzjwi7W(O+WI%dcvO~?%x{Wr#`ueoB>V1W*q%n!WDb2H&$v#r zMkjIf$F0Ok7~!ZK*%|T8X~f1+Hz%zq}q3^dhrL&^6tWUb|bUvXbv$B(=N=a0i~ z&W8Nv%49FWj(koBvXX-KTQU4*y_2?^Qnpt&zk_`J>Bvo4kiU4PKg3Tv&HmV+dSF&? z8-~42y>M52dY9k73;TsE-W~hmwtaKc-M=C)cM&6V+}fIAVz=a^YDgh`~;Chu;?|><3Ej!i(*Hzzc zt|JPr>fhC9NhS(VW^sv+$7ApJWA+FiYTuOBboL%yq@-|J(=20<4?=rrECS1Iv<_c6fx z=tQ;gq}O@Ot7Y~nQhTL;@Qr_ohR>=PIx8Z+5ULoJ1^Z9tA|gVA*V9$up_>QCF=|Kj zrrWKn^l$i;&A#`eJnnaV*Lt694c+h>-eDDs{|&3YhHv`DuKJes`_{^RYh~B5kn5?d z*E*G|Aw53iYe^!8n>0da!)5%@LbUhC|5*F}A9ZZR`dMR)ff+eo0>V>C3&~|?T^zcbJpTVrt_`6`R2xyT%~C|lD)rM z;`Wn1(NFHc_wK|O>H?SZ@r!9Rmht;b;Zn0fb*h=g>Wx!1{IYtYrqto(-S4b&kYRXJ z^<|PeuGDl#IpLbzVwzG|e%;_`k_NJyL&9gM=UV75*b>9k7Usd-$-({X@qE~Uf~B?9 zYDbCO#+tQd9XsLkx;f)LoU38%^cZ=~*>o?Tvk*Uu4^FU||F9m}%%v&rYG_Zsgu@wO zr+-9;vxddmWvBnigCqs(AiagMK_BYuGJ%?5IKMtfrE6c6i@ok$BZB@q54|quIGRFs z47J0DYBT0To@;2}ezZ?d(zM^AFUaUM$~t%TdD=uh;smeyiSKN6X3nsQ|2aoyDCq{Z zLMM?=WoACdp)9ApUE>wlF<4fwSg-WA*z-PhKY7iFD9^e#gN9Y_$;u}Bi#`YuM_6`M zB~IC0-C5HF3sjMhf|Xx`?;286R=^76!ie3^M_g6Sa|}DPouYCLt>L2hhgN(LbwC^T zsv4A)o2oJ{Iy#OuO~i6b)b~$NYt>gA*H%1NJ6=e1ms5p@w$#1 zclFe|ty5vdX-h#po{D}fz1=`j$QN!7bHq1=d12Xn6o5te$-+8rl<;#U?35BBw&HPm z0jIYRZ<`km%4(f6Sl!fCHsBLu*uy*g^Q|zSTzhqTht6!|+mST!1#&i9B6(z(ibW#)n%DCjJYW=d3Sgzb)EFct44h$~PC}rkoSKvH{Yh5y0zYRZ~whr@qN6C zuh(OHpXS}#z(<|@=0J$_O@3~g9kYPvUF9oV;FtX+GTtxQH1t z2_M>{3wV*ma-CmNQ?7vkH;U+fX5IFPnh#@ePRnJUVg0U}X?9hP^N!k^I8Hq!HB4I7 z3+Zt1nMHdKMlRto5(#jcpMj=s{$U2I1EZRV)>Vu!eXzpr1EH~dd@ks5kVFj9qK8-3>BnbFvTx8a@1 zyul=UXQn+g-G2XA4skAydm)|2Lb1^&^czdXJfAvUE7;KGeE2uCI$zNUucdhS4xhi- zjOQJ`w@;*fSakj;dwW|IVnXSiKrI!WFW6iL{}0&ychScIJA99??qJn^(jjaEWy%lM zZ>n3@(@U|qjZd-FdG?CM~L+VWpyu6}dvp@QB|5xrO4WI_@2i@8rf zVsgIqs{eC}Wdoe_cfIP%DrKw7>SlJL66bXbRpdeu?kL%#PVVW`@Ie`8AP>8gi7kri zopML7k*nb%^-*JW7U+qKZlk+EZMl#q^cH#4*YmN6i7Tfl;NdV?hlhfgVLqBd?Zsb z7Yh3XYFjFs_67U%mFQRrP!_=MAr$Hds%$g(-|1(D?^=!(O8!;?$~&DZUUb+fyQFITEDS0BX46n-m+#R zSj}PV=|Ifc>#|%uovc?_r%tlv?PMexV-%mpaaZJ#9;1%zeC=+3bAwG1l_1 zyvrU|Y&S2R$lYvpZIFjq&)R+E=a-Apm#~U+tl4yDeuBS0hQ%J{J@gZ^_7G2Y@sn-Y z@+Rv2>O#PY311~yiLB0d2J!E8fAx(0{FB`H7qn1w-Mb-}ftSP`I;rzPzo{}^=rj+r z+6|rN!f~~c;SyaXJ~jvNeHG}F&5IeQ|Ja1EY&hOpPYE9kKMd~=KhlfrOWE8tVSe*P zi-*Vbj{i>|%KO=XA|k``@gw1LcF~Lc=}@?48Y{N|VqM1;Z)Xn=({!B2R@}l82DBj= zXb!SQzSRYJE2ZQfYjcE$;O*?bmc6dud~Lh$ZQ?WaT^1v5@{az;w@!B6hS)J}y{qbC z{vslVRL=fY_I$s1W0N>zg;`38zmrP7?~0RsKot3{ll`%7Tf?DD z74l&nD5WH9n*mC_Evs}Q>?B@qCd2uZ9t2gxIbkJhTq&IB`6Lqo#`@lSa!=F!>>S@& z>8DoNBYVRe);)*5cu$HgUcypOb)wfg(SPC0Gx-EHVX(w3k||KY3cG$Ep5;&RQyQ9~ zJWfifpp(3NpGXZFle&0%-9ubw=?^B(E)~c9U?|yHA0d_fUdvSRu?2oh1%%At- z_OGnMXSmY&_pVejRVl?15BMu7f{)>!rMS&coy|4=y%vJlA%6VHwI8xL#2O#B(@wI& zC!C}c?DA2}*dh7&eRjoe$Z3c5+lb3qjoVpff6ZY##4B*dC7JzSt=PJIcRnZIkygIo z8gy|Qx3z;``d%&UeEp+7;+Nj!kA`DcI=R}2lIu~iSLRW5OoInYs~{;P0w@A?7Q(m` z)@`UTT&Sywh%ddLOLS__4yhEqR|TP#&)<2{>ehoJ8}iPr*zlI|novb`8ux?}pg6x- zf``l{HZSP)v#_=~Sl9=xQ^JcUL!FtuzhoXYnH|XD-=^L8nK|CWe0X@iclf#2Qw;?X z^u*&$V4FX&zFVoFF3KwC@$W3wWS!cwkUgD%aZW9TFaK4<32CB_Q_tWs#^$!np&77x zoct|rOBc&%MOi4N0q zr;GkOaA#0NKb4e}Mpr~IyLjMlRT9nTtv9+%?f{GeZ zh1G+co)!;214%UzLo`$2pNI)N@sd5PReyGR3_ZtGR&*g-nwa#v1rv0L_dbUmx(VS# zVYk%0OR~M0iTB9Nh9<+&X+5T8Ba>L6B$n_$=;sdWe+|QM4H~+{R_K8Y#V5n(>8xlv zae7+OV}?isw~>ZTPUxsh`8y3+(=II9DBfs+xM8y#&w0Kn4W#i{pz0?$E3cMH{cyhE zOS$1)K`R|S2AF(3!A$cdK_)$9Ge>vo8~eNGr+w|5NeO=jXY~d-5S$Cv1}Ei`56Crb zmCIkNj()xz?>K+8r@iz{Fo~)p;bSYX0m;tkWo!HkkN%k`WE#vo1mo}${;3uAs~)bb znzK<}eD|omUlgXvV_hE-gPE-?PCx28#AEHlQ0`F!eh5#wJ3N4~+~fb7!XsErb94QB zS$LYaJ7&VvA3WhP-PTU)GH}e46kQnnT#xX$xoQU~LJo#6_}$*&FJ@Nmk+a*@`F{M33SHrE|Scu|7asNmB{u1_SIS;s;=UE9Wtcc8VeI}Or(pr7*`#-=8 zJN^7F-f6F&IDlC_sNI4q{&u48`p&;t(z~o{ zDBh0B2Br#%1!;rIdcV|_BWw~BvTl!31DFyXRMiRL8IO%Uw)y}6b=3!>QWiDe`KVXA;iwN3L zIaOsdi^e06%h1CHyQ*qTIO=o2@Ive6t#6hWld+bx~a$ z9*O}(FBRxX>B&2Oc3i84v#t{KRwa5tHq)0~)~BF_6`=4n*1UiD;M6a085 z{F+>(D894zHH;0JY!}XRM%Ox>n{cj&oY7;}QcbjVE97LAh4iYyJdK>Kmi*z1PFPQ8 z>ka+m-jPk1?q3U>wEuaw&N^;|F81(P2l=d1u;v+cTNf!Sjy$7`Mt`yCKF?3R!J zj?Vw9NLNfrS7)YuFkEHj3r=Sf9=3{@F(12}!S7$kKAyz4CaTca%Hn;B8=Eh8^`88~ z`;hkdd-b;EV3fknmk!cw!k6sBHr8gp$o85TE$%Uw&r*_Kugw><7hSx{?u>%8ruaN_ zeZm#6kN1N~?`anfvKvQ=LdW9v$NGGe?6=o(gHnhDQu~be*|+JO!DJbq9MEbZ)p^C7q^i(MZRe`3 zbJYo=90XD7w*>VR4dyuqU#hMARy@BG7xt@lJ7z7AKfgbT<2_kK{xgZEc7u3ATdvNl3g{yolo@E z8Uf!7_0EQ{^n+-;hT0Xw#fKxrTx030CyJFnQ1dv8?)GCjiv_riB{GhSblF@i>p3ry zo>u*SKa)-@mJ%+?1Q(@OQI{Qqo*O?@2c4ayYGmJlb4t^<*FUdqneIu+v zJN>wtj$$Ui$aIEjs6Fpcjh+{4?$wulTbNzEkydPy3IdJA#)m1x(I!|9Gxv9(**`=4 z%wRt|+^HSU4!`HC3nA8(&i^(R_o)4GD-7VAT;7YRcuwqV{QrA!(N|XWkgBGb_g7TZ zSRY^fIv@CfbzNsyAGWJQdonxUQ!%Is(=@RjtyR@Q=g{j)XKWLF((fw5Pr;klf)%n* zTO!fu&ykeTQ?A53)JPDrmO(U6aDzJODox)Xtl!Z8}Mdg4cE0 z??G|V!&hEpGdr-ZErKUm)W?DqE6Fqu*TP-wTWWD9-@Ord{P;_b#}NuDO6z= zs~bNQjLdOw~=RZe9pwia+t|8P6y+5vdzq`!MvkHVWQ z^BwpqNe(|HeSj(eIkAa2nHe~+WzN%=Sg&uzh3iFzo1mHvGQr>D0@jKe*La_+@DpER z3szdO71r!aQRUY#?kewni)*Lv9f7T{Sj{;5n@0wvECwJk3og;6Z7N&xjorQ1F8_~( z%@lOjdG$4zVkGW)oQkR$6qgHWnwH>HzNT;bhQghOgDu_Y`8s~-D`)dF-=FE{-nGty zD2O`;qn)un{#I|ktBW{829`QvADh13wfJ6)`K*)GoNsO-7u?L6G-fTIh0z*9W=(Zf zXvmT_RJHc3?yC(jrHy5u8u0OT#TIq!#!9%&$8np5JvYBjMtrCHxe-42o_^&E*}l*H zH^=9mf)g7F@AtxGx3|Le#Rz3ZWjUex6qx5rnC#zRk9DpEY$Q!u_=d-x*qtueoK{Xq zb9szs@!2h4lU8AAcClf+H_SRB93*csS-y7(o?tT!b%_7t%|x2@pvbOH&O7e)980khoAVVvvl8p`C1!3p zWvQ;PEY1rm@}5_b_dJ$P{zRqpR4m4P+0{j2=hghyCb4jW2|g>%z6}GX4N67w294Oh z9yrqR_|X-zzgw-_Wz}>^CRpUBUaS;+p-KizLoqGW<(}?{m36bV92_yyocPsXY$-c?@0>)y8%}LvDHqepS zrr2?F0xp>z@Lz0dEUl?Q4~Ju8RluCk(HqSoEf*6W^DKoZ zzm&7mz+LLAqH(MY&a&VM=dBcV-b0ZyK_+%H!e0JsD&i%*u201aMo+3|IHbnwgud1% zT^CKtyBV()jfx4gL~2JLj?|4-)>FHQt6QW(^bJ~Gu|jmIT%svy(O<-jCnEpJ5nkml z4~i*%VLiW6GdYJ1eMhv?$!a$8Jii{Csi4_oJmp#_>wSEK4k_aPeY`lP$IjdgB~7;r z`|@@5sm>n7#5@pBC@)&5NoY(_Ue8qR+H$H@DK)D_UZQ2{7*) zbpE2^g>w+ip|BiuS^;{gLPJ+s{<1p#x>;~6_)->qWms3nQ5390!pX3?yjB~3r8DhE z58ikndpZ`MJ{@xXAC@x0dHl$K zEZ+UFOWxpfRhBS9@E*H3l1}$ExTX#4SrfCP(^HTIW=+!dIk$ksHb>7g&WwXr!jMI}|yLU7alBzli<(Qdi+k_Ug~9#$LI?qmb<}XYz#WD3)!n zv-vAqwhsHY8v31Uy4Cv>Rd2FLuc>(HL>JMJ4npSw7^fE&Z-6dG16?CzZbo~2kG-Dm zT`rRK`rP-|vg4awzsWrur^>pF?YragKDIG4uTX%+DB-H?irHVd$1)wm z`O%cBlzYiL=}bdr4i$c*F5Ouf_vIm&C5>(o|I%4p3QOM8`~2#JZj)=-jH%dw!T5pZ zayv%j*KikBW2YSBR-SnyEUmjTKiXS$=TsJF9khFbcXPvNxRtOLJ46?Qi?_Ce9{t#qm<&LOf%2 zezBJOUDbWAYj;$0Z|k_X)%Z_+ukQW3ntNZBvZD&Npo*RIq~EI|CWCHd`%a4@cX~9h z1U7n{7cGfPDD1uzx=2ZDm|i~ch~4h*VEzd>5@TskpCM6NHT6}c-9EW!+8viB*S z!TWUj%i`a8eXq3ZDf#tw{tfd7vm({3QXTPQJxbmt!Na^-J+XQBpe3)@UOit&{o*C_$)WHIG5kEN^mH7|SkH!s zvoJ5y!&f}MhEwSz8hAnE*VRvV41e23hB=GLLgWYrLk=TV z@Q;af4Bk*&^+NxGn)#RK6Wl zr}rZbfbKcLBHbWV&_WYO@Ss17-+y4SR`SaKW5E`|Nb}tJd92WE`|KkpZ8km594j+d zv_3~%I){E_mh8xET+u9%>n!hhjvYAD`cZd!Oo3TX8O#*9PNT4SKN3Jzcb(vKBFICs zVc+=+pJD+fu@QaQu~zpo{Kf3yd{~c6P;L_b;3kgs477R}uHPo_xJE4a39R~ov-Vaz zAAgiv-c}u=bN8gvSAvHte9zz2z|lU3qwR(fA7LFn@cc_2a2vMV-xk3o(d;MbX%Vmi zNi1Ya-XoD;Ze$0`6};_q;2+JO)>?$i3cd9ZvBecjaU<2j5V8{iqgu zN4y~=SaW`_E2T~*tGq=Ud|@5=(uzL+qhk7e6s6fA z=wvIO&d$i7KWzs6|1!!(=BBOAPQmji%}`Mq>&MMWdICdKN!?df6`++VP^#*g@r2#; zxO-TdFD%9bJ&I*7AkHr!FYpk}T2^`Z^qA_@*tyV|PUvU;#kBt8tp0~Xy(QvEQstFa zPsc2x$3l8mmoh1!sz|euim=x4v#}nc%MmUsOk?36%kj+bEOEn$|UuqhnRr>dJke>LP50D zwF++8Kr{0LTly1w`3pvyei)BZP5$C(+*)FOBqbkS<&3!RB97%>oJ&Nco-9JhDN=YW zcwaoBgN}(lErKPirD@*5XF=~^g-Mjl)JH8f*>6GchQ6?GV#$Z{CmK2?5)V9(Vo#Te(WBfncx);EP``}nXWSkR&3zh`JpiiiMpQNc$}hR28ED0`@% z>0rK6t9UJWgbMhrhg~^b$#iEa#rR2b53yJ)S4W*QyWX44kdc?l@An@I$Eb6ds(x|0 zx(D-s!o~U@ET?Dt41!#)qJ63UT1(PmfNHWFiJWC8tiWK_ zbP7cEiLCVxFwkKL?i%!#79!8bCzj_28}f1O@t?2bzecf@)5P)fO;cSS*-9n+8%%o8 z;}L6o1OoZhwH;TJs08{X=qr}*6dBG-bqH!(=StSSxc!mO3S^ZHOp7m18Tkg!`6*^> zrf7YN=i}J2QP9c=oZlcg4dcb4XTe?SRUqo?!rmp+1?^>B+E9M9!_c=8?{{#uwT|sQ zYX$SPfTkPZOKUpUkNbL2-%U&(O+(>+hra3xymFK+`^6pD0MC9VE}R3ijK`r2!i2vH zyF8CSX+$McSr(`qjz=sY;=k;UUW8Y!%YEGNwYzL+lCztJEy&6y6ckmwpJ4Nq3i zyDm&^k?4S#N*w>6wL2qgcmyA@O(tf&ck{VjFpnO5M*Ij5yfYlH3T(8UH9^esF3dDb zoU$0US_NZmwZrt14lmk80Y))Hq#&zY4PI)^=JsRVKcr~*8sm6~HM)bZ%f&WU!JM~t zY6kFYlkn$@F=DGCZ^(Z1#gnzioi~=zs6>(RuvN~;GTdURkGp=*ONnLYJx_`64)6i= zG;Cg38j3vZUKYC`oht<%G&S}ii49F_S3Jm*K13f^j9o1Qy*-5wZJ~F^i>{&Wa^a-r1Y{?1+pqBeRf@nT)a`L@5zPMrJBei3st%p7;0edH#6n z^>tnMbzk>2&htDz$LIJQ$9bHuByy8kYBQH$cOSyf4Q{u%-3(7R;w@`~k-9Jr!Tx&K zZP^)LX^OABU`JUEIf}}v?n}emV*FVVI&=}4jG{F0a+J#|@a{Pgf!>4|#0skPJ7#S$ zJ#!)JTNL}o{qwPP9*VRh=gPq=?t=;G8l41X8tl|t-adv&AANIPr zMe6zS+yXj=l=5CBWH8F%Fct8Z3T`W#naUoogxge$-^W|;i6(A^b>XRcIQ7Ff!p^$J z_4L?KxsM5Q6tg{d1*_UY+14u$7E~59H4_u{h98rupeudP13ctqYc{Q@y`U;4iX-H? z&O(wJvvjfr*>#(ETz38mRo3}s>hc6VDIz27JfB0iUPf(RA6z!y->X5}>Uxh>8m)z{ zxM~yK=2YO|rgejV>`6PRzdnVMxB5yXyq2={rYLy`|J;)8*3%*BDSI0Witn?zGP{y` zEKu`z3s&653I7xw+@e(96pP)JQA@#sqE0b6WI%I^B1($zD~eESL#yX$i7#5SE#Ylv z>$VdN=mVE}P%H+p_THk*zL2Y{+vp^_7V3YVhs!mrz0zJog%Y@&lD6vvA_6eje}fQMB>V)SO}X@vGDv9c3x#J22(%Da2Rk?MZ1K z(J1N+D;7Hm`>*1`H_cE|)|F12rU!%1eAs9HUIg=_@j2mBpD{*9`Gs#i?{g7qbUOWd z+R8h0lv#Y!XlOiue%aAJ>ITj|sfkaQ!YA{qu+L%V$Ah{wq_qz)mHr*c?U+xdqeB{J zN2at(KC>P2S!46*Vavio&ZH=zmsJgW2kWSSZh@n>6XW;gw+8ZAu)(!gEP2Lz-g7^j z8m=eJ_|tsobLP1%9U-a)>&YMYH5+}*PFJYg1-du29_zD=@=hTtipf4?|6E##eus7a zCbmB#UjK$p_9Z{G4XfI!SEF-4!c8!Dn@Dh{-+e;?|B+w1#wt_sOSxsks_L}YP8ZLS zB8GRw1G`w=NuTmQ1uC;xtrWihjJ#`WC^R4_6dM^-65BoP45(Um)<5s6Be(FZdd|AA z^J!g4YRm0az%i?d4@*;5DvHxf;X36gDtgcPIhp6(vX(E)Ed1cRf9>0T#y@Z5uh&7* z_t^R(b2x|3o6XlvmA9WsDVQXSH5J>@MHCb20J-~!Gc$ z=aHTYTZP}!raupxsI6!i?ods-J#3>gqL0UhslJ#Z=6_Fh#m92*N8szvurrAm@Ij2G zxEi)rRzCZQrT@j z`H}2y6VycA2nVZs915Rbi|@yMqkX?yDK($qx!cV2ZmZy6c)|C%VRfY8`<=glgV*OV z`>PwCYaF7}ATFzI%gkmf@l$Q!>Lgj9W%$J*Ui4R2DsjT&a<*mVaO=y}z8L%%>mjD- z5$xb2KeE$$w%BiipL-bjc79*o^VG%%kb}mPg^m+bP5c|~`@I?+imOWH%UH)%Q7nYC z#`~)#=*>#ILbNVeWoK3pbq?)9uk9RL0t+|WCAD8Y;*T)#H+<;6JWO)>-2>J_HdPnd z#P->+<8-cMBBPtu&qd$k8+yRU{O}w~{%gKl3wlN!HkAYaOMs(4!s1WuLzxd1qa0it z`c@s0Niq4l9HQhj@F##vxA2g^!mj!mHv(kI7>3#Rc8Z$sk+@NqG|8TFG{4tZB%Qy;iiP%f9rq`YUw z-sRmFc+MQxJcvD&?lu>yM;XYsMN(5S=vhAZOw4%}1~4E0ngeH-`~fp(KjRT1)mnBU14{Mr^PY!+5MIF`bC%I2G9e=KM8nfHeFHiqM#lfvnRc4sQ#>T;&bd`+7oPzpQNdT!EtAw zo{blfE2^AuBmRuMP;`cBlZ3$8d}&?j z&(J{|LAe%cLhVJPOz)&nzi%!k%3MX7RvQ{vJsMXPv4TpMAg>I^BNWuEwC{9~Iiu(? zCH9ksC8vVeDcz>Te3Rg$Nm#QysLq?E0ufp8lODL=IeqwRmi)2&!fIN|Tju&Tez=R= zKx5iyNwL7gDiZ#ML8ronw8sgsbU=Js*hUxjCbZ0F_0yqphfl}L$oLd?PGzyMnqCCe zbr*k52lqyP*Gl*HZhG$w!&hIUXD$!{EQ#+3H|xBxlaBc%t2_c-kBP%i$^e~F75M|? zw0nb!IbJS)t_=J_m4+)>;5z8K0cL&zV?P#gevGevB6swWX#Qhr&l)!Tkx#r9Qm*9} z*Sp=|=dBRjSsiiH`^uvK`{ z?)uB(z-x4`(C(|4{+wy_AxR&+q>tRo_HvHWGiMqVRwfGbjfIyTzLy z(nRNbx7VzxUe;C=&P{+@Z&40r;FWV_ zKj!$mx!4)iRlQdgS(fK$XU*)7Y{RyD$S?Kg>4u1AU$G*G+sX8b9t1jt@6CC3rfWd3(;l4F=4Q6eq@&DDck|WIKHZAuESJ|v@9~>q+TeVcIXI*{^e4`% zTurVA#7$%BzPi;d97W0-4nctvl(bN0S|2d#GNwW?QexWEYuBXq>=V#i(` zyO|5%2q_@ruk0!61EaemCNdRQc?XY)x{Q1xldvzoEBs1D#fkVBSLg7&nEj`CrSKAK zh7+zV@HCwcSgGBnlo3uF{vOW|URSv$#t84}Mv^>MFU%BcA3mYqwK_fTzR^xym8ber zyi4JZPPjiS#!nLTvqx|i6yingM1C{)+o=dCV~6R^zl+P$Bs8+;wri|HLciEE39mbs zdSa|Y!hGjMFNpP(NAI4n()j`FW33a`I`4E-tcv@E6E^Cw^HJ<3{&G?zzERd^Ee)WAD=$tFU*5dhwcxlb}Ycl2o#r6F|>c6Xf_*lHP>m~a)TZT`#^2rqy zkt-@19}t$YtF4^woz7WmXdBzQ7-I(R5(F6!?ozW*Oxd!jj;8tfK_tP_vCZBEDQp4DHB-!3r^HJp~y zHz+QS$ww1^Ob#oPSVY_`wz&wu4~iZ);)V-iPx7KAL}pQE{|2(ikpkL}4;|{7F790@ z`d)?mY@v>RCK~@rCOqofaTJR>gi9TQm;3zHZfO0n@3F!a)tyIt*80SF+rjl_7*~DX zxtf@-xc7Su4@>Fao5m(O>v+Fh+)DQNHsl)x9ecQ5a@BD?B^HkA(jM{q%+^e5&rRZr zVKhk~W&pX;V1${B_oHmN2tHUIBdw2%wqef$WdL6{*YnM~8a6il8NG59Tb(Yt9U5j) z)sa6|AGfI%R*Mx0pHJ+`noUHTF>D=6mw0v18@pvJn=9(=S~ApKJdAeXm&N9ugf=-a zw~X@lfs>%__*B1BcrMF+U+@{6OC?jk&Fy*kngmYf5trAcUv%PkCSj&aVvicz+$tE$ z;Pd50B2M`URG`T!RiTSLCnM4%=wLQGsg7!I=V1$8w5d4oS&B|&S1~;ubGlMuOMk~- zmvNj3SH}g*+#j#kZf`xxUx*J4%GueS+ivc3ddA$-@Bfdmqi#fO*mX3lgLg*fQn z>pZ4cdf@Cm-SY&go`ye@+7DVz43EKUg=DZSdaKQWVovj3~pcT6^G zoy}$){^bd?l^mb{B@AiFcg5#dbq)NPbsvqtf`4|WBex0d`4H9B38!O4B^AZ-dhC;SMM6j76_6aq4tNkb&s4APF>uTQY1GP;{>|UEk z@1CvJ=?x0?1dok|xI@J9uZZQlLFAtNV!K36x(gp&3C$*n7YB*bUy@0$CbLuwJIqA& zPVPPbveGW#p(kYazs7;Sz%O^>HG6TL{knR6rKiJjT^3I3vT#*)`F6Y)d+iOC`-paW zvf-Ec+Scs8DZ78hyI01H3t2Od(|%I>lyRA!U&Z%F@Z-I{v8qb(&sd*hAXT9suG}gS z!>hpO(l9#@bbr_wrS*+Bkw$pQu>GygTKe`<{ z!C5r@pp1WBYGpBXW!31(4QK{0(P4YjU|*4Oo20&M9xt*&yuS?+?dK&+_*bLqxp{&VB-Czojkw?Aza> z#HXh0WtA(>@B0;_nUv=#tLa!&UB#E)Ormo@R<|C_) zF^}?FkLs5Fc&voV#=;cb!eMp2CmX~Hg>9Un{)*d4ez#1wyq)~vSrw6YV=cnRoE%>! z7#p?@X6Qlf+)m$OrCz>!gZtr$AXEHzp5?x8kTzj({Go(7@tg@;<9QS8uuq7^o=A91 zTwg%V&Ny~73TlrGzR;y)3!nBLbbmYEN*wV5lz-NV8P%NEQ6BOa3CgNu zET-ehV`9KeV#Ac?`|t3)E=ONGC1abDO;$J;WSZL0F|_Y4Iv}*9-Bh7_7o*?kG0D6C zPjy4z@LS#AH{+b|y2i=s^~Fh>#oOVk9m0Y#c@Mi1%)Tv#7t9Nw(3e2g!*XDD# zxjm+S@Vq+0>tPwOTve4Jb_unG(Pk`?@DyiKMT~H2mSZ#DY#6nd2<$ zOCzY}Qq5^$T3b%`m_x--N_;0d?coMY{6oZk+OE&<T!`$mb5kGKY%cM?%n7 z)kZ{J^mLl%&vJ_c(yC3m7k(<*IUu$?4nu#XU|$!ZB)8f!=$rkxKE#D}q^=Yj7d{hv zHEc>5Y~}o_ZXWB!o(G8^hseMT!8eEFIQGMbQw1_Gyi5kSm z;b9Y0BD_JVe3O&dvPrMP zpTWFccWb77qK;aYidu`OY3iqr;+cWQbDUS00ari32{%IJZ{&i`;3jv(f9Zl+s;r8| zD>(tYW$>Wts?=~b!MRpWW}tU|09#kuQ#J%VbQXi_9^n>*O%@i7rmlOK#G@GZ^b9 z#qqWDM`^8xny#Fo9h^=o*c~2+CV%Twr6*`u+zDKDf_K9X!Q60IurPcxm=~@HR`4dv z!!Ltf;c318e+zUj3Sw~d5w-PIVQBmKlL_iv6IRB{CLD14d%QGmS}>s)me@G zBRXuRGQ5&nL3@t*nqTBr*)h)lxw^AedY|D6eA6b%Q`F;gIi>4e3fFw!ex`3fjklfz zOUJ9PA8X(BFnyVa=&Rn>S*M-#7jI(~H>7`8!Zq-+}*3PT-Q)@8^S}zJm&~@ZmRh8ZnKRiJDET~%ad2`xY z6f=s2PiN_?t(@I-^kaPP6?w?JR#ZCi`a@K~XvLStKC9A7p2Gs0VqGmsZMeL_{);}q*ByjEn71+dcM{;He|VRgE6 z165SbMLAv6sSOnAjJB)zO}zA-#EOtpJo5>i0lu~?<1qiZFFYmYyg<9Tl8CqaX?5SY zzn@NbM9g?tEPBk(XI&TcK>7n7-xR~&5AVx$MG93~@naSl@?0X|e0E7zh~?vNi|GAP zQZBhH&9xfsw*W9(Vk}KnpLSl?u)<71Bn_8EO`>M3Bn}gH1 z&57U1PLHN`mG9Nx1{C&S%@Z0^S!R7n?f{XVj z2UqVe4Sv7BH3;v28>9*^29IIi<-%A(qcCN{pfF>?EZ3GWbAt0;6OzYMB$SWe3Dm6^ zxpU(FqruPdT|svCSKV1I12D`vtY|+A`b}k$&TK((ob(w;+9gO546rM0lC1D_vHb#S z>SC6>SasE6>f5|vu=8(K^wMUAdbRGbw5fTkt#Y`qoYSMh{dfX(;TkV{oQ}Q^w^%QK z80}e^!aI+&cekI+z{~WmmR8yebgt+4wrWn>s3hZ7OgFA4owD&T9L*H#jSV)jXR|i! zED`?TtS~*I!;dM^OXI1+nd(P}#bcC{1UaVo{qp+V=k|Ci%Jelm&(4Io<@3uz%bH^P zRv2pU@HMsjBg1#X0n~v0`X2PR)39F{P*WemP1VcUpR|}3x?NS%5!D0N!WnRLK5x0k zJa58xwwv8A*lv_B_)ew9_bM)alrQ*+rTp%@-J%((C=rjYqjl{NpMFI{{00X-CVD*% zEsw~&9+7d~i!<)R9M`Id(6LY_u9Un~HnBrK5pij;c{S0%b86n2@>*@>1>3v22Mx{J z3)C_FFU>-$psnBQf2c~Ls<^%cg*A^TI)52oKEGYT7Jj^ zmqYS}SmF#dOXJ}EKJUc^m#uC0|gBtZH!(zWC{zXH?X*1J0itxWx?K)|O z!FTh@LldjC`eAJy_}~}iUY^H!tKnLeT%}YT6vcyzV{T7keZ^&lis89syklkTzovI+ zAaD7Sj5$pks?HbVkE67-=kcENQxW$02#ynANVnvIFVg5v!O?H!KE8&dU$WURX?F*8 zRQVQup0Nrqs=T-ZGnJ5IQVHrmQ}`t5W##lvph~q+dDRhqc9S*jBhNoT>^>+lHYrC% ztSN?X;I@@KT5n@ESX}>x+}3nBy4bf|g*mQu?GV#^VsF`Q-+H&#+>KT4@mf3N;I!ZDr7BgEMIWqo6 z(8rmot>UZYpLXJ^dxG-ugTYhr13~flH$f3U7l9T<;_LmqNRRF5L1i5>tN8l{`p7nt zqj&C={M8-bb6S1rpMm*AC4P{E$!Tg$0^r~-5A;i?UB)sFIsebktZg1588 z8&S349{%jKIr^98_9%U&l4>p(CBhvLu5>=d7tX}J8hbWO9oX#{ln-kJWijm1n0A%$ zwV+x!GiVkr3EJYRBb_cgJ>1As>7g%MX)WaG+sees zV!4ty4eAP|e1La{? zg)pNMP_nFid`;Y_q5G}vRvGB$QD$`kPr3^G+2^@u+`l18NvoG$Zk(+g?pB}X+fiL( zFZ$JZdDjWz);X|sfqd%%(b>BczuEFiGqKaxVAgOsn(ivt+lsXth=8l9*exN(&m)(e z9onU_e=WhN-cad4zc-R+z3Ms0bCE5b^DgJzo>KF8N_6}^G(RkZ^^JGjtLo%4*0fbV zX}#B470XKrEkuzl>)k6*5o$xu+A>Lv5%(%F zPte%&tFbA{j?0R3s$+Z4s{whDj@XU;4wAo`fECYUt5L_BT|D7YHhEqwc?atW;8<4s z;UD!^g~a0}ti}p<;#8Iat!Y)(_Ug~lHtXV(4OK1F^PHws{Knp)8J`hhVp}?2C*Qx9 z>{##E8Wj+$sDjbybX&#qd(RY^{T4=Uw zrY!v=QQbsyIF54i8XVOf+_zZGyUh1KQ$2r#zwPZcn_CC9c<*8~fQQsT#P~in0&)*S z=#z6`*k-SG40>OK)R}_%wC>7s!3AlWIpmpAIXf$W3V($YgUjLD;`>=*`&n{JZ^=1L za^l^X;A}WhEZ@oR8p}sjrUN_~IAx3`aNADV^AwGJl+E?>_NpVpajqWX{MN48E~k|l z*?>lqguhHi0Zy)hB9$wHRq!CqJEt6fJ~2lrF-H}v;(2+(wpdC}EOa~zi@Fr8rG4*% zqfz&mP&Hz9b*JU5kmkJS|I|~=ReQP<8#^hsPa;mw5p0wxTO7Vb4;>tg4krhLFzD{# zhe5k=E4=(NXz6N8_3aWK33@xzZE1w=T#k4^;_vs!5fp>mBHe zis3&U+|idgFV0gB`t_%?&B2g1Q{s+cEZ5js!vAGNj|JWF%a-E#I%=IN!Ow#3=Mpbu z6x*kOp|{P;HQe)@oWu{-_FmoBwp;frs67j1KJQ=j@JuYW{VMb7jWA^U$cO zTHOtC)OK{k0gz`1UlR4}iq4>Z1A~|d&BoX-Gb$#VP9g0s9*cI=G=!zqU{`6HSU%5w z1g55uEl&aWqCO`7P#q%`?5g{}u+b}C^>@nH4PNT5cM50+Y2|9Nz~?-;WI;@&s$5b{ zxZT1dou~`_G0a!J|6~X`M-G0W&#+8xeu-x+lUrY;YBb9Iyb0}J^+s5BQBT-ucoeGbgL0q9udK3DW;)eu3=3*+rt6uZ zqSk#T4Dzl%2B%q!Q_|VVLNyJa;EO!2+GKWG4F!|_44jR zRklT)g>u#_KA)9b z?S2kEJFi@(_)e_1+;3(X!~dCuXiw4$5WNN;RRWslgR|MqR3=wCKc|NB8ECBOXmB|w zxsMq4qBv$r^{3Tf<1^l&5&gP3Bx?iF+8W2sa(*3*L}xbE&iJ)8X3Z(r&2j&FxJx~? z+E4qp0!yqQ7AT5=miBlNGf;}+{S=>8&EMAY8qKLY?TzFA;OhkYFy6;wKQV8o*!B$` zBRfC;l=axcN*^wYT4;Ulc6*M#kr2Fy{neG-EGNd!s{%7UJ?w87c*a?NyRh$-IOVkX z=HP$vRY8~dI(emcRr1Zk#9s>v#Crz0opO^aUcqfHR{|z}PF2xdQN7R6=|) z=46f3s=pp^l0gocx`MH7s=_wI(=E=I-a;$=JRIP6!|a-!1Uo14Yctr=o4CqkwHM>? zj$u6a0N>8ZotVN&y%X-^I8j&0VzA|D{Hc|1)RpfascvwBm}-W2=^d--U0maRR=12= zzQlPni|N%10QUg|95g#d)*o}C+ZyB9K{F&J97o(}d z57wlkP-QXK*;r3*cvJ*Nm4QajsN`&fS?caAzW>T^55mfwqWNu*>wOG<9^4wu)_RNi zU%*Ezin??8>l8X5UgMd5!0bQwZ8xxr_pR#rW^FoanL>w~fLBd6!f#mFQ?2ZUDis$| zDL0t$9F8~8y3fYN?QTtjd)p~t2{7S!KbFeDUffVK*GIZTB!LGh#7bx7-4EFLl`XK_e&baZf2`zHYRU)F zFU)Bz?@-=Il=Uhl>~JjYw!GV?*-Qghdz^FthCA6iEcSQXT*t7jzgTt#KC3W2q%K{g zlbCFj+~FMmt~R%O-8+bzoTc3SOu@N~uUwN4{Ed$Do64pOvI{@j;de;Q;TI}GH>h~i zc`tEi!x-Isy1<6U@`x4vO&(F+1J=fUR`)0F8+F|}W){B?DSr-mB1GS6R6m0HABlQ5 z@cbWQ2p>}+Hru)IDRqCBeSM$n%@dto_qAHULlms2_iJ=}IPvGzez!6FMn%|X6ybeh zx~=w_Y_jWNt*YpE?b(}yT}-5ij8I$F4<>dJ)wP9>jbULkOe2c-UWA8@{S=)B(hkP< zgt1Z1=vDWo`+Q6J%1u_ox3KbxS4?J9^TCWN)?O>$YOs-=#B$$bKOe%)-SVLaag5`! zDG<}C4t&uSWAdvS=|5#^E~pCmiL!Wv%6|ZUZk7R9uG(m-e1e?|_H=#_)VI&Gd~8*a zTR*gPu|@dgyTRr7ltAZy+46yEOgjXt;thi(vg5O5#^*Wpb~;UZzO0WrOm@DW-7OSH zID;7nK4K1>pTI(z;xJJk#i}xXC3vv{EIv0iFT2XXtgJqxE0t=TGS1L*rX*IL0dos*j!X2-!!qMr z!u9rhE{o3;e@}1?3zw=5{Xiwya(zP9xK^tJ-E0r&M|RY1*QaTV-C^5xE&Nms=vF<^ zw#yi7F`t|0Dxa9W|GL(_g;{RIa8!50l;v_9i$oO*#B4Ks>M6d#1T5uManV>jZnzj} z3~w-ow;qo>F97QGVMpbPh zHmEM9FDM?*#s&i@`xneVC35{9mhQl`KK1#cp5!00rw`4|?SUf4VaRDJ z)HM;)T~`LU@{p^DZ&A_g*XG}wnbS5#|X#{{o7oF{#mCvL>2RIt`^!qYe$za%RAR6O=R9392>I$?h` z*i-?Qlp5=}t5V{0IF^l1plnT(MW0KXS!5^RN)=`wn(J-$Q0$TU`BDwsad_|pT=+?p z@C!ckI}US$731T4^9;WE1J@ot`=p<*!O>*a_(P(Bg0AxB?io1rf^Tm33H9YK@%Rnn z_^0azME--vxa;;Gyd#-8N@WDH*gf!sSt%lNsO0{$#w0q?^JR0_U4CbzeSV{j%^SvK zhPjIBE#6TDsy4_e8zo~{jBYX1xKm21Bm~{;x@c$}P9{5addZ>)j1U zP<|&Gvw3jvef=Unq9X2APpBu1imwE^R9gH$i}*UPtCVr8Zp<2E)a_lpWd%pE=htNh zqn!+Mjp<_7QnhEx;omBMzd{~xl~{ZkYg@_}E>85|M=<-NBJm?|@)QPt61)Ee&;Qfy z9Vnc}Yi5NL1t}Tj@U7=rS}WeE7fTz>2TsHq7ErU6T4igo_>UpcRx9x{(bP_B@pEfW zU6uP^(fWU2E!X+2TUKUT6`JXT$^6j>+_aaRZ3le6q3F4~9oJ>(mU-oJbJ~fgE)@U2 z%>r&<3DJpS=h@i_NcXMV-RAyNcD{-=FJm3ES@d+wVJv1gieDRG-uj61x`@--$RM|7vd5v}}XOl6?;2S0pwcjyfSB%gZR`E3tvz_OoCtJgX zXpMQ;N$O}GXv6=6`Q$M(VVw7Lb@|An7)o!zLDCB_O!8=Ri5<{rwqa_`a{J*=41e` z(UU&Zjkjp)8#FT-^^8K)U$V3jc#3O#*yz>naSo zC5ldFCqssyfH}=&ezV9YrM0I_&viQ*qN>_F!C9Gs?p4H0XM z5CcHeQ!Y1{D99rExS@Vbw@;}VwqHbjmX@E~zYM_eI_S*j*h|9t}~zgE{6shyv* ztz8gvvpo7o>}LtaIWPVd6$P`!H3vhs&Td=NyqfT-b?}h7?5h%$vId*1$i8Y?Sx?Ig z)%Q&r@qF#Qc6TgtXd=sg$Sx%QL=Dsq?EhPF&IQkjI!h<`H-k*oqp&BhZ(h>um-Wpn zv-+C8_0#-gT`FlUs1Q{s*Q63wr9o7LM`c{aJYK~01b_Od5sIuky;Wp?JM`FZ&7XxJ zf4iTaW#zMKs_<;hLlm{=)p}ZrsYm^+qWuTkdDLBEja@Ky7d>VRB;CXhuH+xz z6O~Mb2(ODk`g&%2zp3Y{$X4_CZFaVr#BXo%=T}71r{Mi}SX%VGRPsWaq-N&l#8Ljp zcyC~P8)2zaZp=V;`;01VY*d} zkbZjwGLOVohj`cFW^@E*I*OV$4w_DYxKnA=v&2Pf@*qYb*AFG)v53>SF?sR)h2IqGu7>9!Tlgr|qm$^BmV)+a4{C0WQHFi`k zut$1|eC%kq{qWSTs^VM82Q^h|4e^3!{oS*6gw^$`b#=FRfiJ6XH%vPj;kI%TPEhgA z@3HQwW^p1y5!)UOVa$|10(84t|;} zk-@i0WOW7k%c3mnDKT70?6VvWQdHbl0#@c$JO6}^E_!(c8O?Dj`^n&63iprBJ!!=>S$NWPjro@3T(lkm$ai*UEWPT~pCm^PsePn95eyGkVF?*CD#KmDf&>nI2YRA1ksq zbRT5B_E3q{gKpjq7BsbvH{&B+KB zHB!tl(yp&Ds)eGSGc&}E3+!rGZC*EsF?6aCS5B2}oi8iCP?gadbGwcI+-KgMJ4wsA z!+P(Ut*h441swD{tZOfO{*Y}h;n{Uq^3&Hew-f4Su3}bB|BypU0|x^Z`v>LdI&XX~ zjP7Tm*Vy0gu{&W(Dp8f3T*1tnq;Nh)0|aVrOO@h55wNth+WZ z+MH(78A1+YTNA|a^JzBgFwkw{jqk9e)BgR7C;gYLrC{G_?4fudn9q;up08#mDXw(S zs9weKFY?6)#qB%!=#_B$ZGL?W4DSI)oAY9obyO-s?aWA@iosQxM5BJpC>*!O&l`nH zxXcx?@-4V^Tg0vZ9-q{U($t5JGsriHD*m02L*dIVI?Kk}q~aX11kvh=@`{TDvl&n4*|1S_A&>XD z+}#%acUOtgKcJ>B#2ntkBHpD~F2g9+()Tv|B>TmEhsEzd%jw;4r7*9LxC)3as);n7 z!;{*v?Ux#MO)}@x6R+KNntba1UipU|Rz>7dw_skI#4l^Dn59_jJbv>{_%M52>h&W?YD*NeRyRjcF|o*^RiFy+v&`8QkZxnJR`#SNx!aN)UaIrzcqlDSotLg ze*)5*1jF@EQF7oJaV~EAEC6U-AA2te9i0^%QIUg_X%Tnyp-7`eLHpvTUp> z%ZNJ1J&mhYHU1S@S!p##dK_TIx3J@kL5vm#69Sn4d4sxvoua{J71FDn?!F}ca^OTb z8cmO2hM#9SpH&y3fEMbi6)g8R8}xGe40m;&C)`Z~i*Lc!>pbE`zVEczIbvNMGV7n4 z#r-0cT_Vd}toBogzFow#P0nzu%Bsy&`RH1wVrsQLzso(gz>cZ8BBROn1CP;tVIVZ< zA~Vv|>Zrlz7K7JWv8@!$LjLojj6bOf=X-%=DK5RC+x17KHOf+;HAG$0He4l?zN&n6!BlQSXJ)drId1app z%fshq$wg$I@~FhK%G|04hc0FwM*4jty(Mee9Td@xa!K!r;iJ=-Mv5uB8`Xy5kh0><9Im+7gGOQgW5K%k zefsM;*`Q;xVB0AFe6bu|ek$<8qOS)qx3m>SN z9`lR(?(~LWuNs*dvSrIun{0J`!6u_U3_sIBZus4Om>W~Anhatlqjtr`-XSZx2@B;f zt?-Ff`e-YDBk(Z>nO%~l9l`>4*lExKRWmJCm#Dpao;x(X^$$GkBss= zu<=c*=m1Q#qxjt}sGy|GcMdEe34QcV;tutff)niK3wE;~H`#92!KZ9#yPtR3DYT!h z9biX4I1&7;-3iz2UcVnt6+DPFu=m8@z0-in*O#>1}CT9dMAB?PTr^KZm%+*ys6%-Ij0D zJD2)-jZTd_=wiF=FgYUbKc?#aqUz0`*~}kM?oY4#7liu@dz2YtAw~G$sy;_uUb#Jl z>&kjY@_Cc_()loT6?U{;UUi>Kmrs$+TUO4SR>=&QKV8H)Q}k;u1;3P!Kgv&wiuA?8 ziBao8h;Y{Ro0ag7D>+2W#D;QELkqykLKMuW#BpVKnbP=RDG?BYTG%fu)YcLrO4wrxJqkf_jZMy}WY)0p91V5c46TBE7;^+QyibI30 z*1`z8#wNzQ!q;}edU@P0?D#w9#8~H`1TZTo4x3*P?R10FQ`BHAw*tSwiZ1ck_kE5> zg7x<5FIC|-SNFs>aL3;T@QW9I&#HE=*bg)RDJserI14u?zVq0{WT9nbp>6-oAl8Xd%; zP32GOLd&N`871Inar6GD8P0(VMSWw^@v2E!d`hb#4t3M;ddVpcS+UCOI86`A~fhMUhHq>47FY+dd750^gI6IeJa6+_~%EuENt<3Z$@tHMzVGm9dJ1D# zKe5Q4Fs)mnyMN6_8oKnu^yvIPNpyPIvqrEDj%F`0o>_=3MV+k57@sn@UM0v?5n@%d zwkuoNwV-VkYuye(-}q0+`!`?rr|YUwxNOWXxK8kRM-!v)mV8{aXQ3lh?1}}vZ0vd( zH>W`uuVTjIQ9mbm&R>Z-LmNoYMm^(;Ft9Pict-tLZB=gNw@;&*c9(+@0YF}rpx*fZ0&o{MTqwQ7F zG}n8!Bb#kycSi?TR8iBB-*Wo3-24t&=m+@!TfS3NSJmC?Hxdg}!X_T`eVwNR>)&M= z%gyjwvEK&S>aD!%PF{E~FM5Cl9;7-Pg~Z>p#nZUw846OQ9yzbtr+$!^e%SQ@w)v>J z&c?guq`N(a6Xc<|K2Dh}>|5kctO|Kstg~Dl&t|o3U#bK@F7IoO<@##yb9L~I=T+@B zUvWnKW!E|P<}i zq5E;wsOOCPNuT})?{2OK;HyIut8OY;&-JX;Cf0H{>$snlJWfPE zlP{L;XT&R9@e>&C2Us>xXbMhKSz)4^KHMj#-dt)(=7Iiy~fw!Okn)e^W<60 zNnUeW$Xu1hn4?`0HT+dgQE45~s#V~xr%_a=c>Fc*Jl-peW%IB5G!xkTbm;XKoP5_x zUxACPH-@`#kOS=GlyUscDBL!d=~&i7IC=?ISIJB?W|{5OHw<(;Rt?Z}jOslx{~8MR zj>J0G>Eh;PR#bF)%zmrvh2@Gv!6zz9V+mU92?H z`X0*Xzaf7#*)>g$b*A+`11FiO`@>9e@ieznWRxbmKf!7r=XQ*1lxvvhsj6ia>%G<* zYc_g(rMP>kYY7kb7UelQm3ox-80>XBy5HQRHLUY8UjH#aXLMhWFe5+CEDSR%gJdQ; z8P}JLX)T!Z6zqB&-_E3xW-hjsTIUbj#5v&qV`+rZk%Y;|SeV^WEYQ&72*%L*j1 zN|Rxq$uNPGRIgO-r=fJD@+zsU!DOBjP(kj7FNwEQl!aa8()-vQG6bg>Be(863*2+B zx{1BG=k~gp(Z6Rq2SF)Cs)ydz?hx+5Oa3?nAs{5$IO~ z%V_5F_kw~G{2c8r+`$(dPF&U3td_sTXL0K{PODFa4`eWsnfW)n9a#A1xZ}}8{qiaP zODAGkw!5x7=J%fKE^N5%Hu7=5^L0P_-8p{ndpLI3-|eOw@9;Vw(#%$hMwXa?x%}#E zYkn%^nu0@2F&mTF(+u|XCVQGIC-k-`cp)2FhS@C>*=(@xHsQj%Si$G&vJShx_1KRd zJHZ=X#F0n<{&iu&N}@9kJ)eL_fi(NS*u&cCTo2+@wu3!ywy^f z@2z8%)e#LffxC^NVH+sd#v1AZ5xYUYt~j>y^sMLFuBxs|@UNub6?B`|Z8leC&r!FI zhxLWY1AT%)RJ&m+iiT62MzYbd@>XN{YUk=%ixu_t%H@7qIfU!sP!-aH>{shT~p(2IBW-bkvU4O`mXzZV1C@seN@k=s+84 zVof!Y#jK8XK8)0%p z_$6=r0|dWhZ_#bLl>-))%@uXeSK(p&-ZFMm*!*;3HqltUZhS^#Gp`tnK5jeXcg+*W zv6Jy@?=M>LF)G9{(xO;aK9-utBWkZHZECk^Fb~4Tw0LM*EHK*Fm4s(Y;&X&Pv6HD` z@^(@7`uH7Jhzs6=>Hk{gH`(Yd@!vm?|2hr&E-rHmzqyZ1GD?y zS+S8EJX0==B{x=*9~KsNTg>C7%~p9)MP<*g1{Z7l+d9zVIVk*sv1%w^_o67gnYGv) z=C{-n;6(`D(zDTD_TR(@vNLY#Oq6q!%8C? z?J!wm3|6tql}2JEPA}Jvp{JLr&4?2?7mU}K0^7!0(IfE#T@3NXvc^Bkj@QHUWsQt| zW50O@n$|OC&(JVyd*vv*Uz-{bVRdabTZ`6Jo1$8qTAIPP`6 zHpid2HsSV5e6yM6dxW2(>h4A?rJ`?{*L$Y+cejkxDH+Q>yo`N2I9x$%|8d+jJ6$Wi zdfQ}}*IjsdQwGkNX5p`}?3}uj)3EJ?9>qsp-^rnWEg$f$Yp?kHD>3_Cu~~HOSF^H5 zecFDQ_@&NRU&eQO?q{B}-*wQ>U%QTlyPVUxSC8Y(@OCS#{80S9241exQDO;vevhU) zU)EQK?x5G!yv7CeNf9bsJ!rOA)Ryhr$k z5nCO{e%`b;W>^EWUuz!N<;1 zcQh9!E-;Eq#q5i*t5xu94ZPgI9=5^GUE-Cm#Arv2woVRw^4<7#Hgnf)Qrc)b@pzUX zu2WZ-{vGJYw}9vR1#q z73~C34RFr4I4n;u;|CX~QaoN|NyDv=zVxk^SzfeH=p`802-AKZPChO3Pz4*Wpk})` z^|6R5+q^379~Ij_q#8H9eHAHH;ySaKp8a!(6^Cc&KHr)519C_^+-`EK8y2j*6wcPm zaI)vV#wQPU^|uZ?^Xnb>_!sSlXv&Ay7tuGc7M>H4*B9;A6DvJWv1vjNZzu+7jX$>J zQ#*(tdr|89SrcQ#^AoYf+3@Loc(aza^C_(P9JcC3Yd&+rhR4LwdHBC4jmDFFQz>&+ z0#;NqLe*JMZT`4{%xfe5vZV;Hh2OU~zftX!lal0wE?ZYWsTPT9KF+D__)%TmDHSB= zDIZZa?jqo+2l5y zHrn6yBfGq+8Y*<94YueM{fT&Pzu4_7{X|dF!p_sT{-TB5(Qzsjg(@ReDkodXCu>?< zUcRENYIVv~J(+;UbkvvFT}L^;F7(x?>Tm$=+Y??6;~S%1V^K9*KP+hg?CxjA`*!yuPEcYlD-vlN)Qz7Fya{)J_!ol8Ce;TWAeG+lgHDkMX<*jCVS9 zL&^O*8oI)LG8l#e)2TKj5(qn(AAXZ8qF1a`?2@V{FPkMfF zmF#->$r*)m3Gpx&JA2G?vU^T8eB}XupT<6l6tVyb_E-EXJ9Gz6{!`?24a@i$E^qur?SSRwD#a}NnqpM-@u=r>v$D{BJRSwvf3kqJkwA{KlS!#SyP=39|uwC8L;mb-)3*oKjxqiwMyzWVqW*Y zN}Rvh)@^#_@34)=muOX2C@jBYRF~vJE{mRjGFumUzO%6KG}JpOJ8}$i9#d;`6e1pS z9c0sAL&p7(lww7FYb6qGV|Q6Q@6wrK)7i}TH1oaK^8j9T5Fb30{SIf(qs6JO!Ou6n z>r`sg9JdQy%YA|m`RJ(Y-cCGkujd?Q_p0qEcke=vg(9})(BwU=VvUSqguR=r*6qIW zF1+r5wet-=_q|nr((1p!3NEpbzpbNdtSAm!6R@XLtn>lanc4L)bt8w{$60VxlbR1c z7NFieiR~1}kcz=3${Zm>IufB(9i)bH34!(C>urEavlVze}vY*=JUws$0AG%wkrn zB9E0&%F3wXs%JH{fXeN?YhP;6AezH)`o(yw?RDSrEvPeHEHuw*T;NkL^4Z__E#AY$ zKfvKu@B{B)kMHy1QAh8^Ji=@~;Vt*yHVRV`Z(kP2b;1EVTluZsw)MLg#o;e_?(=@G z;rUOC$SWj9;nOf&T@|ohX8}?>((+07eEOuKrF)otQd&Y%pY5LYsAj{x5VAz~qMws` zcJzBKVf;or6MvD!Pw~WC;N-c{|Dk#R@BifPM}HYTJ9^~5+l0i|h@PFy;|Ync9(XM9 z3Q;vn^xWv(qW4Ma-hco7_bKitJ|jqckLZ*Bzt0&xH+ud5{w8`n`Y(E&&^L?TDWLU5 zk42w3>Z%xhD$PX_KXdfX(X;>iF43z;&kS*g=u`jqyXgBx&xsz7?nQrnFY))$cmD4c zqrZ&)=D+VLQ;ZDNow~S zcJ0%3v_$a|xt{FVt;2xcow{`E*||uGqQ#4r zELR}bty8I7^`h6QSt@C)nmg59X@gw4nO7f_qe=7T&HbFs)woZWfv>dh-}(Q4%_a?N LmGH~||K0x&nEwkp literal 0 HcmV?d00001 From 8ce71d58bb48ecd72509f073e06470a4ce2a8221 Mon Sep 17 00:00:00 2001 From: arne123 Date: Sun, 9 May 2021 01:28:50 +0200 Subject: [PATCH 027/606] initial commit for new folder structure --- {misc => resources}/alphabet-abc.mp3 | Bin {misc => resources}/alphabet-def.mp3 | Bin {misc => resources}/alphabet-g-k.mp3 | Bin {misc => resources}/alphabet-l-p.mp3 | Bin {misc => resources}/alphabet-q-s.mp3 | Bin {misc => resources}/alphabet-t-w.mp3 | Bin {misc => resources}/alphabet-x-z.mp3 | Bin {misc => resources}/audiofiletype01.mp3 | Bin {misc => resources}/audiofiletype02.wav | Bin {misc => resources}/audiofiletype03.aac | Bin {misc => resources}/audiofiletype04.flac | Bin {misc => resources}/audiofiletype05.ac3 | Bin {misc => resources}/audiofiletype06.ogg | Bin {misc => resources}/audiofiletype07.m4a | Bin {misc => resources}/audiofiletype08.aiff | Bin {misc => resources}/audiofiletype09.wma | Bin {misc => resources}/lastplayed.dat.sample | 0 {misc => resources}/number0..mp3 | Bin {misc => resources}/number00.mp3 | Bin {misc => resources}/number01.mp3 | Bin {misc => resources}/number02.mp3 | Bin {misc => resources}/number03.mp3 | Bin {misc => resources}/number04.mp3 | Bin {misc => resources}/number05.mp3 | Bin {misc => resources}/number06.mp3 | Bin {misc => resources}/number07.mp3 | Bin {misc => resources}/number08.mp3 | Bin {misc => resources}/number09.mp3 | Bin {misc => resources}/number10.mp3 | Bin {misc => resources}/presets.csv.sample | 0 .../15-fastcgi-php.conf.buster-default.sample | 0 ...15-fastcgi-php.conf.stretch-default.sample | 0 ...ot.service.stretch-default2-Hotspot.sample | 0 ...utohotspot.stretch-default2-Hotspot.sample | 0 .../crontab-pi.jessie-default.sample | 0 .../sampleconfigs/deviceName.txt.sample | 0 .../deviceName.txt.stretch-default.sample | 0 ...hcpcd.conf.buster-default-noHotspot.sample | 0 .../dhcpcd.conf.jessie-WlanAP.sample | 0 .../dhcpcd.conf.jessie-default.sample | 0 .../dhcpcd.conf.stretch-default.sample | 0 ...hcpcd.conf.stretch-default2-Hotspot.sample | 0 ...pcd.conf.stretch-default2-noHotspot.sample | 0 .../dnsmasq.conf.jessie-WlanAP.sample | 0 .../sampleconfigs/gpio_settings.ini.sample | 0 .../hostapd.conf.jessie-WlanAP.sample | 0 ...stapd.conf.stretch-default2-Hotspot.sample | 0 .../hostapd.jessie-WlanAP.sample | 0 .../hostapd.stretch-default2-Hotspot.sample | 0 .../interfaces.jessie-WlanAP.sample | 0 ...interfaces.stretch-default2-Hotspot.sample | 0 .../lighttpd.conf.buster-default.sample | 0 .../lighttpd.conf.jessie-default.sample | 0 .../lighttpd.conf.stretch-default.sample | 0 .../sampleconfigs/locale.gen.sample | 0 .../sampleconfigs/locale.sample | 0 .../sampleconfigs/mopidy-etc.sample | 0 .../sampleconfigs/mopidy.sample | 0 .../mpd.conf.buster-default.sample | 0 .../sampleconfigs/mpd.conf.sample | 0 ...e-amplifier.service.stretch-default.sample | 0 .../phoniebox-gpio-control.service.sample | 0 ...box-idle-watchdog-countdown.service.sample | 0 .../phoniebox-idle-watchdog.service.sample | 0 ...rfid-reader.service.stretch-default.sample | 0 ...tup-scripts.service.stretch-default.sample | 0 ...artup-sound.service.stretch-default.sample | 0 .../php.ini.buster-default.sample | 0 .../php.ini.stretch-default.sample | 0 .../sampleconfigs/shutdownsound.mp3.sample | Bin .../smb.conf.buster-default.sample | 0 .../smb.conf.jessie-default.sample | 0 .../smb.conf.stretch-default.sample | 0 .../smb.conf.stretch-default2.sample | 0 .../sampleconfigs/startupsound.mp3.sample | Bin .../sudoers.buster-default.sample | 0 .../sudoers.jessie-default.sample | 0 .../sudoers.stretch-default.sample | 0 .../wpa_supplicant.conf.buster-default.sample | 0 .../wpa_supplicant.conf.stretch.sample | 0 {shared => resources}/shutdownsound.wav | Bin {misc => resources}/silence-0.5sec.mp3 | Bin {misc => resources}/silence-2sec.mp3 | Bin {shared => resources}/startupsound.wav | Bin {Phoniebox => src}/__init__.py | 0 {Phoniebox => src}/cli_client/pbc.c | 0 .../controls => src/jukebox}/__init__.py | 0 .../components}/MQTT-protocol/README.md | 0 .../MQTT-protocol/daemon_mqtt_client.py | 0 ...mqtt-client.service.stretch-default.sample | 0 .../components}/PirateAudioHAT/README.md | 0 .../PirateAudioHAT/requirements.txt | 0 .../PirateAudioHAT/setup_pirateAudioHAT.sh | 0 .../components}/buttons_usb_encoder/README.md | 0 .../buttons_usb_encoder/__init__.py | 0 .../buttons-usb-encoder.jpg | Bin .../buttons_usb_encoder.py | 0 .../io_buttons_usb_encoder.py | 0 .../map_buttons_usb_encoder.py | 0 ...oniebox-buttons-usb-encoder.service.sample | 0 .../register_buttons_usb_encoder.py | 0 .../setup-buttons-usb-encoder.sh | 0 .../displays/HD44780-i2c/README.md | 0 .../i2c-lcd.service.default.sample | 0 .../displays/HD44780-i2c/i2c_lcd.py | 0 .../displays/HD44780-i2c/i2c_lcd_driver.py | 0 .../dot-matrix-module-MAX7219/README.md | 0 .../dot-matrix-module-MAX7219/display.ino | 0 .../dot-matrix-module-MAX7219/still.jpg | Bin .../dot-matrix-module-MAX7219/ticker.gif | Bin .../gpio_control/GPIODevices/VolumeControl.py | 0 .../gpio_control/GPIODevices/__init__.py | 0 .../gpio_control/GPIODevices/led.py | 0 .../GPIODevices/rotary_encoder.py | 0 .../GPIODevices/shutdown_button.py | 0 .../gpio_control/GPIODevices/simple_button.py | 0 .../GPIODevices/two_button_control.py | 0 .../components}/gpio_control/README.md | 0 .../components}/gpio_control/__init__.py | 0 .../gpio_control/check_installation.sh | 0 .../gpio_setting_rotary_vol_prevnext.ini | 0 .../example_configs/gpio_settings.ini | 0 .../gpio_settings_rotary_and_led.ini | 0 .../example_configs/gpio_settings_test.ini | 0 .../gpio_control/function_calls.py | 0 .../components}/gpio_control/gpio_control.py | 0 .../components}/gpio_control/install.sh | 0 .../components}/gpio_control/requirements.txt | 0 .../components}/gpio_control/test/__init__.py | 0 .../components}/gpio_control/test/conftest.py | 0 .../gpio_control/test/gpio_settings_test.ini | 0 .../gpio_control/test/test_RotaryEncoder.py | 0 .../gpio_control/test/test_SimpleButton.py | 0 .../test/test_TwoButtonControl.py | 0 .../gpio_control/test/test_gpio_control.py | 0 .../gpio_control/test/test_shutdown_button.py | 0 .../components}/rfid_reader/FakeRfidReader.py | 0 .../components}/rfid_reader/PN532/README.md | 0 .../rfid_reader/PN532/requirements.txt | 0 .../rfid_reader/PN532/reset_pn532.sh | 0 .../rfid_reader/PN532/setup_pn532.sh | 0 .../rfid_reader/PhonieboxRfidReader.py | 2 +- .../components}/rfid_reader/RC522/README.md | 0 .../rfid_reader/RC522/requirements.txt | 0 .../rfid_reader/RC522/setup_rc522.sh | 0 .../rfid_reader/RfidReader_PN532.py | 0 .../rfid_reader/RfidReader_RC522.py | 0 .../rfid_reader/RfidReader_RDM6300.py | 0 .../components/rfid_reader}/__init__.py | 0 .../jukebox/jukebox/NvManager.py | 0 .../jukebox/jukebox/System.py | 0 .../jukebox/jukebox/Volume.py | 0 .../jukebox/jukebox}/__init__.py | 0 .../jukebox/jukebox/daemon.py | 39 +++++++----------- .../jukebox/jukebox/rpc/Server.py | 0 .../jukebox/jukebox}/rpc/__init__.py | 0 .../jukebox/jukebox/rpc/client.py | 0 .../jukebox/player/PlayerMPD.py | 0 src/jukebox/player/__init__.py | 0 src/jukebox/run_jukebox.py | 21 ++++++++++ {Phoniebox => src}/requirements.txt | 0 .../css/materialdesignicons.min.css | 0 .../css/materialdesignicons.min.css.map | 0 .../fonts/materialdesignicons-webfont.eot | Bin .../fonts/materialdesignicons-webfont.svg | 0 .../fonts/materialdesignicons-webfont.ttf | Bin .../fonts/materialdesignicons-webfont.woff | Bin .../fonts/materialdesignicons-webfont.woff2 | Bin .../MaterialDesign-Webfont-master/license.md | 0 .../bootstrap-3/css/bootstrap.cosmo.css | 0 .../bootstrap-3/css/bootstrap.darkly.css | 0 .../_assets/bootstrap-3/css/bootstrap.min.css | 0 .../_assets/bootstrap-3/js/bootstrap.min.js | 0 .../ui}/_assets/bootstrap-3/js/collapse.js | 0 .../bootstrap-3/js/html5shiv3.7.2.min.js | 0 .../bootstrap-3/js/respond1.4.2.min.js | 0 .../ui}/_assets/bootstrap-3/js/transition.js | 0 {htdocs => src/ui}/_assets/css/circle.css | 0 .../ui}/_assets/css/collapsible.css | 0 .../font-awesome/css/font-awesome.min.css | 0 .../font-awesome/fonts/FontAwesome.otf | Bin .../fonts/fontawesome-webfont.eot | Bin .../fonts/fontawesome-webfont.svg | 0 .../fonts/fontawesome-webfont.ttf | Bin .../fonts/fontawesome-webfont.woff | Bin .../fonts/fontawesome-webfont.woff2 | Bin .../ui}/_assets/fonts/Source_Sans_Pro/OFL.txt | 0 .../Source_Sans_Pro/SourceSansPro-Black.ttf | Bin .../SourceSansPro-BlackItalic.ttf | Bin .../Source_Sans_Pro/SourceSansPro-Bold.ttf | Bin .../SourceSansPro-BoldItalic.ttf | Bin .../SourceSansPro-ExtraLight.ttf | Bin .../SourceSansPro-ExtraLightItalic.ttf | Bin .../Source_Sans_Pro/SourceSansPro-Italic.ttf | Bin .../Source_Sans_Pro/SourceSansPro-Light.ttf | Bin .../SourceSansPro-LightItalic.ttf | Bin .../Source_Sans_Pro/SourceSansPro-Regular.ttf | Bin .../SourceSansPro-SemiBold.ttf | Bin .../SourceSansPro-SemiBoldItalic.ttf | Bin .../_assets/icons/android-icon-144x144.png | Bin .../_assets/icons/android-icon-192x192.png | Bin .../ui}/_assets/icons/android-icon-36x36.png | Bin .../ui}/_assets/icons/android-icon-48x48.png | Bin .../ui}/_assets/icons/android-icon-72x72.png | Bin .../ui}/_assets/icons/android-icon-96x96.png | Bin .../ui}/_assets/icons/apple-icon-114x114.png | Bin .../ui}/_assets/icons/apple-icon-120x120.png | Bin .../ui}/_assets/icons/apple-icon-144x144.png | Bin .../ui}/_assets/icons/apple-icon-152x152.png | Bin .../ui}/_assets/icons/apple-icon-180x180.png | Bin .../ui}/_assets/icons/apple-icon-57x57.png | Bin .../ui}/_assets/icons/apple-icon-60x60.png | Bin .../ui}/_assets/icons/apple-icon-72x72.png | Bin .../ui}/_assets/icons/apple-icon-76x76.png | Bin .../_assets/icons/apple-icon-precomposed.png | Bin .../ui}/_assets/icons/apple-icon.png | Bin .../ui}/_assets/icons/browserconfig.xml | 0 .../ui}/_assets/icons/favicon-16x16.png | Bin .../ui}/_assets/icons/favicon-32x32.png | Bin .../ui}/_assets/icons/favicon-96x96.png | Bin {htdocs => src/ui}/_assets/icons/favicon.ico | Bin .../ui}/_assets/icons/manifest.json | 0 .../ui}/_assets/icons/ms-icon-144x144.png | Bin .../ui}/_assets/icons/ms-icon-150x150.png | Bin .../ui}/_assets/icons/ms-icon-310x310.png | Bin .../ui}/_assets/icons/ms-icon-70x70.png | Bin {htdocs => src/ui}/_assets/img/No_Cover.jpg | Bin .../css/jquery.fileupload-ui.css | 0 .../css/jquery.fileupload.css | 0 .../ui}/_assets/js/jquery.1.12.4.min.js | 0 {htdocs => src/ui}/ajax.loadInfo.php | 0 {htdocs => src/ui}/ajax.loadMPDStatus.php | 0 {htdocs => src/ui}/ajax.loadMopidyStatus.php | 0 {htdocs => src/ui}/ajax.loadOverallTime.php | 0 {htdocs => src/ui}/ajax.refresh_id.php | 0 {htdocs => src/ui}/api/PhonieboxRpcClient.php | 0 {htdocs => src/ui}/api/common.php | 0 {htdocs => src/ui}/api/cover.php | 10 ++--- {htdocs => src/ui}/api/latest.php | 0 {htdocs => src/ui}/api/player.php | 14 +++---- {htdocs => src/ui}/api/playlist.php | 6 +-- .../ui}/api/playlist/appendFileToPlaylist.php | 0 .../api/playlist/moveDownSongInPlaylist.php | 0 .../ui}/api/playlist/moveUpSongInPlaylist.php | 0 .../ui}/api/playlist/playsinglefile.php | 0 .../api/playlist/removeSongFromPlaylist.php | 0 {htdocs => src/ui}/api/playlist/resume.php | 0 {htdocs => src/ui}/api/playlist/shuffle.php | 0 {htdocs => src/ui}/api/playlist/single.php | 0 {htdocs => src/ui}/api/playlist/song.php | 0 {htdocs => src/ui}/api/volume.php | 0 {htdocs => src/ui}/cardEdit.php | 0 {htdocs => src/ui}/cardRegisterNew.php | 0 src/ui/config.php | 8 ++++ {htdocs => src/ui}/config.php.sample | 0 {htdocs => src/ui}/func.php | 0 {htdocs => src/ui}/inc.addSystemInfo.php | 0 {htdocs => src/ui}/inc.controlPlayer.php | 0 .../ui}/inc.controlVolumeUpDown.php | 0 {htdocs => src/ui}/inc.debug.php | 0 {htdocs => src/ui}/inc.formCardEdit.php | 0 {htdocs => src/ui}/inc.header.php | 31 +++++++------- {htdocs => src/ui}/inc.langLoad.php | 0 {htdocs => src/ui}/inc.loadControls.php | 0 {htdocs => src/ui}/inc.loadCover.php | 0 {htdocs => src/ui}/inc.loadedPlaylist.php | 0 {htdocs => src/ui}/inc.navigation.php | 0 {htdocs => src/ui}/inc.playerStatus.php | 0 .../ui}/inc.processCheckCardEditRegister.php | 0 {htdocs => src/ui}/inc.setDebugLogConf.php | 2 +- {htdocs => src/ui}/inc.setIdleShutdown.php | 0 {htdocs => src/ui}/inc.setInputDevices.php | 0 {htdocs => src/ui}/inc.setLanguage.php | 0 {htdocs => src/ui}/inc.setMaxVolume.php | 0 .../ui}/inc.setPlayerBehaviourRFID.php | 0 {htdocs => src/ui}/inc.setSecondSwipe.php | 0 .../ui}/inc.setSecondSwipePause.php | 0 .../ui}/inc.setSecondSwipePauseControls.php | 0 .../ui}/inc.setShutdownVolumeReduction.php | 0 {htdocs => src/ui}/inc.setSleeptimer.php | 0 {htdocs => src/ui}/inc.setStartupVolume.php | 0 {htdocs => src/ui}/inc.setStoptimer.php | 0 {htdocs => src/ui}/inc.setVolume.php | 0 {htdocs => src/ui}/inc.setVolumeStep.php | 0 {htdocs => src/ui}/inc.setWebUI.php | 0 {htdocs => src/ui}/inc.setWifi.php | 0 {htdocs => src/ui}/inc.setWlanIpMail.php | 0 {htdocs => src/ui}/inc.setWlanIpRead.php | 0 {htdocs => src/ui}/inc.viewFolderTree.php | 0 {htdocs => src/ui}/index-lcd.php | 0 {htdocs => src/ui}/index.php | 0 {htdocs => src/ui}/js/jukebox.js | 0 {htdocs => src/ui}/lang/lang-de-DE.php | 0 {htdocs => src/ui}/lang/lang-en-UK.php | 0 {htdocs => src/ui}/lang/lang-nl-NL.php | 0 {htdocs => src/ui}/manageFilesFolders.php | 0 {htdocs => src/ui}/phpinfo.php | 0 {htdocs => src/ui}/rfidExportCsv.php | 0 {htdocs => src/ui}/rss-mp3.php | 0 {htdocs => src/ui}/search.php | 0 {htdocs => src/ui}/settings.php | 0 {htdocs => src/ui}/systemInfo.php | 0 {htdocs => src/ui}/test.php | 0 {htdocs => src/ui}/trackEdit.php | 2 +- {htdocs => src/ui}/userScripts.php | 0 {htdocs => src/ui}/utils/Files.php | 0 {htdocs => src/ui}/utils/Strings.php | 0 tests/htdocs/api/playListTest.php | 2 +- tests/htdocs/api/playerTest.php | 2 +- tests/htdocs/trackEditTest.php | 6 +-- 310 files changed, 83 insertions(+), 62 deletions(-) rename {misc => resources}/alphabet-abc.mp3 (100%) rename {misc => resources}/alphabet-def.mp3 (100%) rename {misc => resources}/alphabet-g-k.mp3 (100%) rename {misc => resources}/alphabet-l-p.mp3 (100%) rename {misc => resources}/alphabet-q-s.mp3 (100%) rename {misc => resources}/alphabet-t-w.mp3 (100%) rename {misc => resources}/alphabet-x-z.mp3 (100%) rename {misc => resources}/audiofiletype01.mp3 (100%) rename {misc => resources}/audiofiletype02.wav (100%) rename {misc => resources}/audiofiletype03.aac (100%) rename {misc => resources}/audiofiletype04.flac (100%) rename {misc => resources}/audiofiletype05.ac3 (100%) rename {misc => resources}/audiofiletype06.ogg (100%) rename {misc => resources}/audiofiletype07.m4a (100%) rename {misc => resources}/audiofiletype08.aiff (100%) rename {misc => resources}/audiofiletype09.wma (100%) rename {misc => resources}/lastplayed.dat.sample (100%) rename {misc => resources}/number0..mp3 (100%) rename {misc => resources}/number00.mp3 (100%) rename {misc => resources}/number01.mp3 (100%) rename {misc => resources}/number02.mp3 (100%) rename {misc => resources}/number03.mp3 (100%) rename {misc => resources}/number04.mp3 (100%) rename {misc => resources}/number05.mp3 (100%) rename {misc => resources}/number06.mp3 (100%) rename {misc => resources}/number07.mp3 (100%) rename {misc => resources}/number08.mp3 (100%) rename {misc => resources}/number09.mp3 (100%) rename {misc => resources}/number10.mp3 (100%) rename {misc => resources}/presets.csv.sample (100%) rename {misc => resources}/sampleconfigs/15-fastcgi-php.conf.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/15-fastcgi-php.conf.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/autohotspot.service.stretch-default2-Hotspot.sample (100%) rename {misc => resources}/sampleconfigs/autohotspot.stretch-default2-Hotspot.sample (100%) rename {misc => resources}/sampleconfigs/crontab-pi.jessie-default.sample (100%) rename {misc => resources}/sampleconfigs/deviceName.txt.sample (100%) rename {misc => resources}/sampleconfigs/deviceName.txt.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/dhcpcd.conf.buster-default-noHotspot.sample (100%) rename {misc => resources}/sampleconfigs/dhcpcd.conf.jessie-WlanAP.sample (100%) rename {misc => resources}/sampleconfigs/dhcpcd.conf.jessie-default.sample (100%) rename {misc => resources}/sampleconfigs/dhcpcd.conf.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/dhcpcd.conf.stretch-default2-Hotspot.sample (100%) rename {misc => resources}/sampleconfigs/dhcpcd.conf.stretch-default2-noHotspot.sample (100%) rename {misc => resources}/sampleconfigs/dnsmasq.conf.jessie-WlanAP.sample (100%) rename {misc => resources}/sampleconfigs/gpio_settings.ini.sample (100%) rename {misc => resources}/sampleconfigs/hostapd.conf.jessie-WlanAP.sample (100%) rename {misc => resources}/sampleconfigs/hostapd.conf.stretch-default2-Hotspot.sample (100%) rename {misc => resources}/sampleconfigs/hostapd.jessie-WlanAP.sample (100%) rename {misc => resources}/sampleconfigs/hostapd.stretch-default2-Hotspot.sample (100%) rename {misc => resources}/sampleconfigs/interfaces.jessie-WlanAP.sample (100%) rename {misc => resources}/sampleconfigs/interfaces.stretch-default2-Hotspot.sample (100%) rename {misc => resources}/sampleconfigs/lighttpd.conf.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/lighttpd.conf.jessie-default.sample (100%) rename {misc => resources}/sampleconfigs/lighttpd.conf.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/locale.gen.sample (100%) rename {misc => resources}/sampleconfigs/locale.sample (100%) rename {misc => resources}/sampleconfigs/mopidy-etc.sample (100%) rename {misc => resources}/sampleconfigs/mopidy.sample (100%) rename {misc => resources}/sampleconfigs/mpd.conf.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/mpd.conf.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-activate-amplifier.service.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-gpio-control.service.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-idle-watchdog-countdown.service.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-idle-watchdog.service.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/php.ini.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/php.ini.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/shutdownsound.mp3.sample (100%) rename {misc => resources}/sampleconfigs/smb.conf.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/smb.conf.jessie-default.sample (100%) rename {misc => resources}/sampleconfigs/smb.conf.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/smb.conf.stretch-default2.sample (100%) rename {misc => resources}/sampleconfigs/startupsound.mp3.sample (100%) rename {misc => resources}/sampleconfigs/sudoers.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/sudoers.jessie-default.sample (100%) rename {misc => resources}/sampleconfigs/sudoers.stretch-default.sample (100%) rename {misc => resources}/sampleconfigs/wpa_supplicant.conf.buster-default.sample (100%) rename {misc => resources}/sampleconfigs/wpa_supplicant.conf.stretch.sample (100%) rename {shared => resources}/shutdownsound.wav (100%) rename {misc => resources}/silence-0.5sec.mp3 (100%) rename {misc => resources}/silence-2sec.mp3 (100%) rename {shared => resources}/startupsound.wav (100%) rename {Phoniebox => src}/__init__.py (100%) rename {Phoniebox => src}/cli_client/pbc.c (100%) rename {Phoniebox/controls => src/jukebox}/__init__.py (100%) rename {Phoniebox/smart-home-automation => src/jukebox/components}/MQTT-protocol/README.md (100%) rename {Phoniebox/smart-home-automation => src/jukebox/components}/MQTT-protocol/daemon_mqtt_client.py (100%) rename {Phoniebox/smart-home-automation => src/jukebox/components}/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample (100%) rename {Phoniebox/audio => src/jukebox/components}/PirateAudioHAT/README.md (100%) rename {Phoniebox/audio => src/jukebox/components}/PirateAudioHAT/requirements.txt (100%) rename {Phoniebox/audio => src/jukebox/components}/PirateAudioHAT/setup_pirateAudioHAT.sh (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/README.md (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/__init__.py (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/buttons-usb-encoder.jpg (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/buttons_usb_encoder.py (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/io_buttons_usb_encoder.py (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/map_buttons_usb_encoder.py (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/register_buttons_usb_encoder.py (100%) rename {Phoniebox/controls => src/jukebox/components}/buttons_usb_encoder/setup-buttons-usb-encoder.sh (100%) rename {Phoniebox => src/jukebox/components}/displays/HD44780-i2c/README.md (100%) rename {Phoniebox => src/jukebox/components}/displays/HD44780-i2c/i2c-lcd.service.default.sample (100%) rename {Phoniebox => src/jukebox/components}/displays/HD44780-i2c/i2c_lcd.py (100%) rename {Phoniebox => src/jukebox/components}/displays/HD44780-i2c/i2c_lcd_driver.py (100%) rename {Phoniebox => src/jukebox/components}/displays/dot-matrix-module-MAX7219/README.md (100%) rename {Phoniebox => src/jukebox/components}/displays/dot-matrix-module-MAX7219/display.ino (100%) rename {Phoniebox => src/jukebox/components}/displays/dot-matrix-module-MAX7219/still.jpg (100%) rename {Phoniebox => src/jukebox/components}/displays/dot-matrix-module-MAX7219/ticker.gif (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/VolumeControl.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/__init__.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/led.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/rotary_encoder.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/shutdown_button.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/simple_button.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/GPIODevices/two_button_control.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/README.md (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/__init__.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/check_installation.sh (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/example_configs/gpio_settings.ini (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/example_configs/gpio_settings_rotary_and_led.ini (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/example_configs/gpio_settings_test.ini (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/function_calls.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/gpio_control.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/install.sh (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/requirements.txt (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/__init__.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/conftest.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/gpio_settings_test.ini (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/test_RotaryEncoder.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/test_SimpleButton.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/test_TwoButtonControl.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/test_gpio_control.py (100%) rename {Phoniebox => src/jukebox/components}/gpio_control/test/test_shutdown_button.py (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/FakeRfidReader.py (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/PN532/README.md (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/PN532/requirements.txt (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/PN532/reset_pn532.sh (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/PN532/setup_pn532.sh (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/PhonieboxRfidReader.py (99%) rename {Phoniebox => src/jukebox/components}/rfid_reader/RC522/README.md (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/RC522/requirements.txt (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/RC522/setup_rc522.sh (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/RfidReader_PN532.py (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/RfidReader_RC522.py (100%) rename {Phoniebox => src/jukebox/components}/rfid_reader/RfidReader_RDM6300.py (100%) rename {Phoniebox/player => src/jukebox/components/rfid_reader}/__init__.py (100%) rename Phoniebox/PhonieboxNvManager.py => src/jukebox/jukebox/NvManager.py (100%) rename Phoniebox/PhonieboxSystem.py => src/jukebox/jukebox/System.py (100%) rename Phoniebox/PhonieboxVolume.py => src/jukebox/jukebox/Volume.py (100%) rename {Phoniebox/rfid_reader => src/jukebox/jukebox}/__init__.py (100%) rename Phoniebox/PhonieboxDaemon.py => src/jukebox/jukebox/daemon.py (75%) rename Phoniebox/rpc/PhonieboxRpcServer.py => src/jukebox/jukebox/rpc/Server.py (100%) rename {Phoniebox => src/jukebox/jukebox}/rpc/__init__.py (100%) rename Phoniebox/rpc/PhonieboxRpcClient.py => src/jukebox/jukebox/rpc/client.py (100%) rename Phoniebox/player/PhonieboxPlayerMPD.py => src/jukebox/player/PlayerMPD.py (100%) create mode 100644 src/jukebox/player/__init__.py create mode 100644 src/jukebox/run_jukebox.py rename {Phoniebox => src}/requirements.txt (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css.map (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.eot (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.svg (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.ttf (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff2 (100%) rename {htdocs => src/ui}/_assets/MaterialDesign-Webfont-master/license.md (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/css/bootstrap.cosmo.css (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/css/bootstrap.darkly.css (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/css/bootstrap.min.css (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/js/bootstrap.min.js (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/js/collapse.js (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/js/html5shiv3.7.2.min.js (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/js/respond1.4.2.min.js (100%) rename {htdocs => src/ui}/_assets/bootstrap-3/js/transition.js (100%) rename {htdocs => src/ui}/_assets/css/circle.css (100%) rename {htdocs => src/ui}/_assets/css/collapsible.css (100%) rename {htdocs => src/ui}/_assets/font-awesome/css/font-awesome.min.css (100%) rename {htdocs => src/ui}/_assets/font-awesome/fonts/FontAwesome.otf (100%) rename {htdocs => src/ui}/_assets/font-awesome/fonts/fontawesome-webfont.eot (100%) rename {htdocs => src/ui}/_assets/font-awesome/fonts/fontawesome-webfont.svg (100%) rename {htdocs => src/ui}/_assets/font-awesome/fonts/fontawesome-webfont.ttf (100%) rename {htdocs => src/ui}/_assets/font-awesome/fonts/fontawesome-webfont.woff (100%) rename {htdocs => src/ui}/_assets/font-awesome/fonts/fontawesome-webfont.woff2 (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/OFL.txt (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-Black.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-BlackItalic.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-BoldItalic.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightItalic.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-Italic.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-LightItalic.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf (100%) rename {htdocs => src/ui}/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBoldItalic.ttf (100%) rename {htdocs => src/ui}/_assets/icons/android-icon-144x144.png (100%) rename {htdocs => src/ui}/_assets/icons/android-icon-192x192.png (100%) rename {htdocs => src/ui}/_assets/icons/android-icon-36x36.png (100%) rename {htdocs => src/ui}/_assets/icons/android-icon-48x48.png (100%) rename {htdocs => src/ui}/_assets/icons/android-icon-72x72.png (100%) rename {htdocs => src/ui}/_assets/icons/android-icon-96x96.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-114x114.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-120x120.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-144x144.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-152x152.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-180x180.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-57x57.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-60x60.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-72x72.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-76x76.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon-precomposed.png (100%) rename {htdocs => src/ui}/_assets/icons/apple-icon.png (100%) rename {htdocs => src/ui}/_assets/icons/browserconfig.xml (100%) rename {htdocs => src/ui}/_assets/icons/favicon-16x16.png (100%) rename {htdocs => src/ui}/_assets/icons/favicon-32x32.png (100%) rename {htdocs => src/ui}/_assets/icons/favicon-96x96.png (100%) rename {htdocs => src/ui}/_assets/icons/favicon.ico (100%) rename {htdocs => src/ui}/_assets/icons/manifest.json (100%) rename {htdocs => src/ui}/_assets/icons/ms-icon-144x144.png (100%) rename {htdocs => src/ui}/_assets/icons/ms-icon-150x150.png (100%) rename {htdocs => src/ui}/_assets/icons/ms-icon-310x310.png (100%) rename {htdocs => src/ui}/_assets/icons/ms-icon-70x70.png (100%) rename {htdocs => src/ui}/_assets/img/No_Cover.jpg (100%) rename {htdocs => src/ui}/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload-ui.css (100%) rename {htdocs => src/ui}/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload.css (100%) rename {htdocs => src/ui}/_assets/js/jquery.1.12.4.min.js (100%) rename {htdocs => src/ui}/ajax.loadInfo.php (100%) rename {htdocs => src/ui}/ajax.loadMPDStatus.php (100%) rename {htdocs => src/ui}/ajax.loadMopidyStatus.php (100%) rename {htdocs => src/ui}/ajax.loadOverallTime.php (100%) rename {htdocs => src/ui}/ajax.refresh_id.php (100%) rename {htdocs => src/ui}/api/PhonieboxRpcClient.php (100%) rename {htdocs => src/ui}/api/common.php (100%) rename {htdocs => src/ui}/api/cover.php (64%) rename {htdocs => src/ui}/api/latest.php (100%) rename {htdocs => src/ui}/api/player.php (83%) rename {htdocs => src/ui}/api/playlist.php (89%) rename {htdocs => src/ui}/api/playlist/appendFileToPlaylist.php (100%) rename {htdocs => src/ui}/api/playlist/moveDownSongInPlaylist.php (100%) rename {htdocs => src/ui}/api/playlist/moveUpSongInPlaylist.php (100%) rename {htdocs => src/ui}/api/playlist/playsinglefile.php (100%) rename {htdocs => src/ui}/api/playlist/removeSongFromPlaylist.php (100%) rename {htdocs => src/ui}/api/playlist/resume.php (100%) rename {htdocs => src/ui}/api/playlist/shuffle.php (100%) rename {htdocs => src/ui}/api/playlist/single.php (100%) rename {htdocs => src/ui}/api/playlist/song.php (100%) rename {htdocs => src/ui}/api/volume.php (100%) rename {htdocs => src/ui}/cardEdit.php (100%) rename {htdocs => src/ui}/cardRegisterNew.php (100%) create mode 100644 src/ui/config.php rename {htdocs => src/ui}/config.php.sample (100%) rename {htdocs => src/ui}/func.php (100%) rename {htdocs => src/ui}/inc.addSystemInfo.php (100%) rename {htdocs => src/ui}/inc.controlPlayer.php (100%) rename {htdocs => src/ui}/inc.controlVolumeUpDown.php (100%) rename {htdocs => src/ui}/inc.debug.php (100%) rename {htdocs => src/ui}/inc.formCardEdit.php (100%) rename {htdocs => src/ui}/inc.header.php (94%) rename {htdocs => src/ui}/inc.langLoad.php (100%) rename {htdocs => src/ui}/inc.loadControls.php (100%) rename {htdocs => src/ui}/inc.loadCover.php (100%) rename {htdocs => src/ui}/inc.loadedPlaylist.php (100%) rename {htdocs => src/ui}/inc.navigation.php (100%) rename {htdocs => src/ui}/inc.playerStatus.php (100%) rename {htdocs => src/ui}/inc.processCheckCardEditRegister.php (100%) rename {htdocs => src/ui}/inc.setDebugLogConf.php (98%) rename {htdocs => src/ui}/inc.setIdleShutdown.php (100%) rename {htdocs => src/ui}/inc.setInputDevices.php (100%) rename {htdocs => src/ui}/inc.setLanguage.php (100%) rename {htdocs => src/ui}/inc.setMaxVolume.php (100%) rename {htdocs => src/ui}/inc.setPlayerBehaviourRFID.php (100%) rename {htdocs => src/ui}/inc.setSecondSwipe.php (100%) rename {htdocs => src/ui}/inc.setSecondSwipePause.php (100%) rename {htdocs => src/ui}/inc.setSecondSwipePauseControls.php (100%) rename {htdocs => src/ui}/inc.setShutdownVolumeReduction.php (100%) rename {htdocs => src/ui}/inc.setSleeptimer.php (100%) rename {htdocs => src/ui}/inc.setStartupVolume.php (100%) rename {htdocs => src/ui}/inc.setStoptimer.php (100%) rename {htdocs => src/ui}/inc.setVolume.php (100%) rename {htdocs => src/ui}/inc.setVolumeStep.php (100%) rename {htdocs => src/ui}/inc.setWebUI.php (100%) rename {htdocs => src/ui}/inc.setWifi.php (100%) rename {htdocs => src/ui}/inc.setWlanIpMail.php (100%) rename {htdocs => src/ui}/inc.setWlanIpRead.php (100%) rename {htdocs => src/ui}/inc.viewFolderTree.php (100%) rename {htdocs => src/ui}/index-lcd.php (100%) rename {htdocs => src/ui}/index.php (100%) rename {htdocs => src/ui}/js/jukebox.js (100%) rename {htdocs => src/ui}/lang/lang-de-DE.php (100%) rename {htdocs => src/ui}/lang/lang-en-UK.php (100%) rename {htdocs => src/ui}/lang/lang-nl-NL.php (100%) rename {htdocs => src/ui}/manageFilesFolders.php (100%) rename {htdocs => src/ui}/phpinfo.php (100%) rename {htdocs => src/ui}/rfidExportCsv.php (100%) rename {htdocs => src/ui}/rss-mp3.php (100%) rename {htdocs => src/ui}/search.php (100%) rename {htdocs => src/ui}/settings.php (100%) rename {htdocs => src/ui}/systemInfo.php (100%) rename {htdocs => src/ui}/test.php (100%) rename {htdocs => src/ui}/trackEdit.php (99%) rename {htdocs => src/ui}/userScripts.php (100%) rename {htdocs => src/ui}/utils/Files.php (100%) rename {htdocs => src/ui}/utils/Strings.php (100%) diff --git a/misc/alphabet-abc.mp3 b/resources/alphabet-abc.mp3 similarity index 100% rename from misc/alphabet-abc.mp3 rename to resources/alphabet-abc.mp3 diff --git a/misc/alphabet-def.mp3 b/resources/alphabet-def.mp3 similarity index 100% rename from misc/alphabet-def.mp3 rename to resources/alphabet-def.mp3 diff --git a/misc/alphabet-g-k.mp3 b/resources/alphabet-g-k.mp3 similarity index 100% rename from misc/alphabet-g-k.mp3 rename to resources/alphabet-g-k.mp3 diff --git a/misc/alphabet-l-p.mp3 b/resources/alphabet-l-p.mp3 similarity index 100% rename from misc/alphabet-l-p.mp3 rename to resources/alphabet-l-p.mp3 diff --git a/misc/alphabet-q-s.mp3 b/resources/alphabet-q-s.mp3 similarity index 100% rename from misc/alphabet-q-s.mp3 rename to resources/alphabet-q-s.mp3 diff --git a/misc/alphabet-t-w.mp3 b/resources/alphabet-t-w.mp3 similarity index 100% rename from misc/alphabet-t-w.mp3 rename to resources/alphabet-t-w.mp3 diff --git a/misc/alphabet-x-z.mp3 b/resources/alphabet-x-z.mp3 similarity index 100% rename from misc/alphabet-x-z.mp3 rename to resources/alphabet-x-z.mp3 diff --git a/misc/audiofiletype01.mp3 b/resources/audiofiletype01.mp3 similarity index 100% rename from misc/audiofiletype01.mp3 rename to resources/audiofiletype01.mp3 diff --git a/misc/audiofiletype02.wav b/resources/audiofiletype02.wav similarity index 100% rename from misc/audiofiletype02.wav rename to resources/audiofiletype02.wav diff --git a/misc/audiofiletype03.aac b/resources/audiofiletype03.aac similarity index 100% rename from misc/audiofiletype03.aac rename to resources/audiofiletype03.aac diff --git a/misc/audiofiletype04.flac b/resources/audiofiletype04.flac similarity index 100% rename from misc/audiofiletype04.flac rename to resources/audiofiletype04.flac diff --git a/misc/audiofiletype05.ac3 b/resources/audiofiletype05.ac3 similarity index 100% rename from misc/audiofiletype05.ac3 rename to resources/audiofiletype05.ac3 diff --git a/misc/audiofiletype06.ogg b/resources/audiofiletype06.ogg similarity index 100% rename from misc/audiofiletype06.ogg rename to resources/audiofiletype06.ogg diff --git a/misc/audiofiletype07.m4a b/resources/audiofiletype07.m4a similarity index 100% rename from misc/audiofiletype07.m4a rename to resources/audiofiletype07.m4a diff --git a/misc/audiofiletype08.aiff b/resources/audiofiletype08.aiff similarity index 100% rename from misc/audiofiletype08.aiff rename to resources/audiofiletype08.aiff diff --git a/misc/audiofiletype09.wma b/resources/audiofiletype09.wma similarity index 100% rename from misc/audiofiletype09.wma rename to resources/audiofiletype09.wma diff --git a/misc/lastplayed.dat.sample b/resources/lastplayed.dat.sample similarity index 100% rename from misc/lastplayed.dat.sample rename to resources/lastplayed.dat.sample diff --git a/misc/number0..mp3 b/resources/number0..mp3 similarity index 100% rename from misc/number0..mp3 rename to resources/number0..mp3 diff --git a/misc/number00.mp3 b/resources/number00.mp3 similarity index 100% rename from misc/number00.mp3 rename to resources/number00.mp3 diff --git a/misc/number01.mp3 b/resources/number01.mp3 similarity index 100% rename from misc/number01.mp3 rename to resources/number01.mp3 diff --git a/misc/number02.mp3 b/resources/number02.mp3 similarity index 100% rename from misc/number02.mp3 rename to resources/number02.mp3 diff --git a/misc/number03.mp3 b/resources/number03.mp3 similarity index 100% rename from misc/number03.mp3 rename to resources/number03.mp3 diff --git a/misc/number04.mp3 b/resources/number04.mp3 similarity index 100% rename from misc/number04.mp3 rename to resources/number04.mp3 diff --git a/misc/number05.mp3 b/resources/number05.mp3 similarity index 100% rename from misc/number05.mp3 rename to resources/number05.mp3 diff --git a/misc/number06.mp3 b/resources/number06.mp3 similarity index 100% rename from misc/number06.mp3 rename to resources/number06.mp3 diff --git a/misc/number07.mp3 b/resources/number07.mp3 similarity index 100% rename from misc/number07.mp3 rename to resources/number07.mp3 diff --git a/misc/number08.mp3 b/resources/number08.mp3 similarity index 100% rename from misc/number08.mp3 rename to resources/number08.mp3 diff --git a/misc/number09.mp3 b/resources/number09.mp3 similarity index 100% rename from misc/number09.mp3 rename to resources/number09.mp3 diff --git a/misc/number10.mp3 b/resources/number10.mp3 similarity index 100% rename from misc/number10.mp3 rename to resources/number10.mp3 diff --git a/misc/presets.csv.sample b/resources/presets.csv.sample similarity index 100% rename from misc/presets.csv.sample rename to resources/presets.csv.sample diff --git a/misc/sampleconfigs/15-fastcgi-php.conf.buster-default.sample b/resources/sampleconfigs/15-fastcgi-php.conf.buster-default.sample similarity index 100% rename from misc/sampleconfigs/15-fastcgi-php.conf.buster-default.sample rename to resources/sampleconfigs/15-fastcgi-php.conf.buster-default.sample diff --git a/misc/sampleconfigs/15-fastcgi-php.conf.stretch-default.sample b/resources/sampleconfigs/15-fastcgi-php.conf.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/15-fastcgi-php.conf.stretch-default.sample rename to resources/sampleconfigs/15-fastcgi-php.conf.stretch-default.sample diff --git a/misc/sampleconfigs/autohotspot.service.stretch-default2-Hotspot.sample b/resources/sampleconfigs/autohotspot.service.stretch-default2-Hotspot.sample similarity index 100% rename from misc/sampleconfigs/autohotspot.service.stretch-default2-Hotspot.sample rename to resources/sampleconfigs/autohotspot.service.stretch-default2-Hotspot.sample diff --git a/misc/sampleconfigs/autohotspot.stretch-default2-Hotspot.sample b/resources/sampleconfigs/autohotspot.stretch-default2-Hotspot.sample similarity index 100% rename from misc/sampleconfigs/autohotspot.stretch-default2-Hotspot.sample rename to resources/sampleconfigs/autohotspot.stretch-default2-Hotspot.sample diff --git a/misc/sampleconfigs/crontab-pi.jessie-default.sample b/resources/sampleconfigs/crontab-pi.jessie-default.sample similarity index 100% rename from misc/sampleconfigs/crontab-pi.jessie-default.sample rename to resources/sampleconfigs/crontab-pi.jessie-default.sample diff --git a/misc/sampleconfigs/deviceName.txt.sample b/resources/sampleconfigs/deviceName.txt.sample similarity index 100% rename from misc/sampleconfigs/deviceName.txt.sample rename to resources/sampleconfigs/deviceName.txt.sample diff --git a/misc/sampleconfigs/deviceName.txt.stretch-default.sample b/resources/sampleconfigs/deviceName.txt.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/deviceName.txt.stretch-default.sample rename to resources/sampleconfigs/deviceName.txt.stretch-default.sample diff --git a/misc/sampleconfigs/dhcpcd.conf.buster-default-noHotspot.sample b/resources/sampleconfigs/dhcpcd.conf.buster-default-noHotspot.sample similarity index 100% rename from misc/sampleconfigs/dhcpcd.conf.buster-default-noHotspot.sample rename to resources/sampleconfigs/dhcpcd.conf.buster-default-noHotspot.sample diff --git a/misc/sampleconfigs/dhcpcd.conf.jessie-WlanAP.sample b/resources/sampleconfigs/dhcpcd.conf.jessie-WlanAP.sample similarity index 100% rename from misc/sampleconfigs/dhcpcd.conf.jessie-WlanAP.sample rename to resources/sampleconfigs/dhcpcd.conf.jessie-WlanAP.sample diff --git a/misc/sampleconfigs/dhcpcd.conf.jessie-default.sample b/resources/sampleconfigs/dhcpcd.conf.jessie-default.sample similarity index 100% rename from misc/sampleconfigs/dhcpcd.conf.jessie-default.sample rename to resources/sampleconfigs/dhcpcd.conf.jessie-default.sample diff --git a/misc/sampleconfigs/dhcpcd.conf.stretch-default.sample b/resources/sampleconfigs/dhcpcd.conf.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/dhcpcd.conf.stretch-default.sample rename to resources/sampleconfigs/dhcpcd.conf.stretch-default.sample diff --git a/misc/sampleconfigs/dhcpcd.conf.stretch-default2-Hotspot.sample b/resources/sampleconfigs/dhcpcd.conf.stretch-default2-Hotspot.sample similarity index 100% rename from misc/sampleconfigs/dhcpcd.conf.stretch-default2-Hotspot.sample rename to resources/sampleconfigs/dhcpcd.conf.stretch-default2-Hotspot.sample diff --git a/misc/sampleconfigs/dhcpcd.conf.stretch-default2-noHotspot.sample b/resources/sampleconfigs/dhcpcd.conf.stretch-default2-noHotspot.sample similarity index 100% rename from misc/sampleconfigs/dhcpcd.conf.stretch-default2-noHotspot.sample rename to resources/sampleconfigs/dhcpcd.conf.stretch-default2-noHotspot.sample diff --git a/misc/sampleconfigs/dnsmasq.conf.jessie-WlanAP.sample b/resources/sampleconfigs/dnsmasq.conf.jessie-WlanAP.sample similarity index 100% rename from misc/sampleconfigs/dnsmasq.conf.jessie-WlanAP.sample rename to resources/sampleconfigs/dnsmasq.conf.jessie-WlanAP.sample diff --git a/misc/sampleconfigs/gpio_settings.ini.sample b/resources/sampleconfigs/gpio_settings.ini.sample similarity index 100% rename from misc/sampleconfigs/gpio_settings.ini.sample rename to resources/sampleconfigs/gpio_settings.ini.sample diff --git a/misc/sampleconfigs/hostapd.conf.jessie-WlanAP.sample b/resources/sampleconfigs/hostapd.conf.jessie-WlanAP.sample similarity index 100% rename from misc/sampleconfigs/hostapd.conf.jessie-WlanAP.sample rename to resources/sampleconfigs/hostapd.conf.jessie-WlanAP.sample diff --git a/misc/sampleconfigs/hostapd.conf.stretch-default2-Hotspot.sample b/resources/sampleconfigs/hostapd.conf.stretch-default2-Hotspot.sample similarity index 100% rename from misc/sampleconfigs/hostapd.conf.stretch-default2-Hotspot.sample rename to resources/sampleconfigs/hostapd.conf.stretch-default2-Hotspot.sample diff --git a/misc/sampleconfigs/hostapd.jessie-WlanAP.sample b/resources/sampleconfigs/hostapd.jessie-WlanAP.sample similarity index 100% rename from misc/sampleconfigs/hostapd.jessie-WlanAP.sample rename to resources/sampleconfigs/hostapd.jessie-WlanAP.sample diff --git a/misc/sampleconfigs/hostapd.stretch-default2-Hotspot.sample b/resources/sampleconfigs/hostapd.stretch-default2-Hotspot.sample similarity index 100% rename from misc/sampleconfigs/hostapd.stretch-default2-Hotspot.sample rename to resources/sampleconfigs/hostapd.stretch-default2-Hotspot.sample diff --git a/misc/sampleconfigs/interfaces.jessie-WlanAP.sample b/resources/sampleconfigs/interfaces.jessie-WlanAP.sample similarity index 100% rename from misc/sampleconfigs/interfaces.jessie-WlanAP.sample rename to resources/sampleconfigs/interfaces.jessie-WlanAP.sample diff --git a/misc/sampleconfigs/interfaces.stretch-default2-Hotspot.sample b/resources/sampleconfigs/interfaces.stretch-default2-Hotspot.sample similarity index 100% rename from misc/sampleconfigs/interfaces.stretch-default2-Hotspot.sample rename to resources/sampleconfigs/interfaces.stretch-default2-Hotspot.sample diff --git a/misc/sampleconfigs/lighttpd.conf.buster-default.sample b/resources/sampleconfigs/lighttpd.conf.buster-default.sample similarity index 100% rename from misc/sampleconfigs/lighttpd.conf.buster-default.sample rename to resources/sampleconfigs/lighttpd.conf.buster-default.sample diff --git a/misc/sampleconfigs/lighttpd.conf.jessie-default.sample b/resources/sampleconfigs/lighttpd.conf.jessie-default.sample similarity index 100% rename from misc/sampleconfigs/lighttpd.conf.jessie-default.sample rename to resources/sampleconfigs/lighttpd.conf.jessie-default.sample diff --git a/misc/sampleconfigs/lighttpd.conf.stretch-default.sample b/resources/sampleconfigs/lighttpd.conf.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/lighttpd.conf.stretch-default.sample rename to resources/sampleconfigs/lighttpd.conf.stretch-default.sample diff --git a/misc/sampleconfigs/locale.gen.sample b/resources/sampleconfigs/locale.gen.sample similarity index 100% rename from misc/sampleconfigs/locale.gen.sample rename to resources/sampleconfigs/locale.gen.sample diff --git a/misc/sampleconfigs/locale.sample b/resources/sampleconfigs/locale.sample similarity index 100% rename from misc/sampleconfigs/locale.sample rename to resources/sampleconfigs/locale.sample diff --git a/misc/sampleconfigs/mopidy-etc.sample b/resources/sampleconfigs/mopidy-etc.sample similarity index 100% rename from misc/sampleconfigs/mopidy-etc.sample rename to resources/sampleconfigs/mopidy-etc.sample diff --git a/misc/sampleconfigs/mopidy.sample b/resources/sampleconfigs/mopidy.sample similarity index 100% rename from misc/sampleconfigs/mopidy.sample rename to resources/sampleconfigs/mopidy.sample diff --git a/misc/sampleconfigs/mpd.conf.buster-default.sample b/resources/sampleconfigs/mpd.conf.buster-default.sample similarity index 100% rename from misc/sampleconfigs/mpd.conf.buster-default.sample rename to resources/sampleconfigs/mpd.conf.buster-default.sample diff --git a/misc/sampleconfigs/mpd.conf.sample b/resources/sampleconfigs/mpd.conf.sample similarity index 100% rename from misc/sampleconfigs/mpd.conf.sample rename to resources/sampleconfigs/mpd.conf.sample diff --git a/misc/sampleconfigs/phoniebox-activate-amplifier.service.stretch-default.sample b/resources/sampleconfigs/phoniebox-activate-amplifier.service.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-activate-amplifier.service.stretch-default.sample rename to resources/sampleconfigs/phoniebox-activate-amplifier.service.stretch-default.sample diff --git a/misc/sampleconfigs/phoniebox-gpio-control.service.sample b/resources/sampleconfigs/phoniebox-gpio-control.service.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-gpio-control.service.sample rename to resources/sampleconfigs/phoniebox-gpio-control.service.sample diff --git a/misc/sampleconfigs/phoniebox-idle-watchdog-countdown.service.sample b/resources/sampleconfigs/phoniebox-idle-watchdog-countdown.service.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-idle-watchdog-countdown.service.sample rename to resources/sampleconfigs/phoniebox-idle-watchdog-countdown.service.sample diff --git a/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample b/resources/sampleconfigs/phoniebox-idle-watchdog.service.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-idle-watchdog.service.sample rename to resources/sampleconfigs/phoniebox-idle-watchdog.service.sample diff --git a/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample b/resources/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample rename to resources/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample diff --git a/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample b/resources/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample rename to resources/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample diff --git a/misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample b/resources/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample rename to resources/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample diff --git a/misc/sampleconfigs/php.ini.buster-default.sample b/resources/sampleconfigs/php.ini.buster-default.sample similarity index 100% rename from misc/sampleconfigs/php.ini.buster-default.sample rename to resources/sampleconfigs/php.ini.buster-default.sample diff --git a/misc/sampleconfigs/php.ini.stretch-default.sample b/resources/sampleconfigs/php.ini.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/php.ini.stretch-default.sample rename to resources/sampleconfigs/php.ini.stretch-default.sample diff --git a/misc/sampleconfigs/shutdownsound.mp3.sample b/resources/sampleconfigs/shutdownsound.mp3.sample similarity index 100% rename from misc/sampleconfigs/shutdownsound.mp3.sample rename to resources/sampleconfigs/shutdownsound.mp3.sample diff --git a/misc/sampleconfigs/smb.conf.buster-default.sample b/resources/sampleconfigs/smb.conf.buster-default.sample similarity index 100% rename from misc/sampleconfigs/smb.conf.buster-default.sample rename to resources/sampleconfigs/smb.conf.buster-default.sample diff --git a/misc/sampleconfigs/smb.conf.jessie-default.sample b/resources/sampleconfigs/smb.conf.jessie-default.sample similarity index 100% rename from misc/sampleconfigs/smb.conf.jessie-default.sample rename to resources/sampleconfigs/smb.conf.jessie-default.sample diff --git a/misc/sampleconfigs/smb.conf.stretch-default.sample b/resources/sampleconfigs/smb.conf.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/smb.conf.stretch-default.sample rename to resources/sampleconfigs/smb.conf.stretch-default.sample diff --git a/misc/sampleconfigs/smb.conf.stretch-default2.sample b/resources/sampleconfigs/smb.conf.stretch-default2.sample similarity index 100% rename from misc/sampleconfigs/smb.conf.stretch-default2.sample rename to resources/sampleconfigs/smb.conf.stretch-default2.sample diff --git a/misc/sampleconfigs/startupsound.mp3.sample b/resources/sampleconfigs/startupsound.mp3.sample similarity index 100% rename from misc/sampleconfigs/startupsound.mp3.sample rename to resources/sampleconfigs/startupsound.mp3.sample diff --git a/misc/sampleconfigs/sudoers.buster-default.sample b/resources/sampleconfigs/sudoers.buster-default.sample similarity index 100% rename from misc/sampleconfigs/sudoers.buster-default.sample rename to resources/sampleconfigs/sudoers.buster-default.sample diff --git a/misc/sampleconfigs/sudoers.jessie-default.sample b/resources/sampleconfigs/sudoers.jessie-default.sample similarity index 100% rename from misc/sampleconfigs/sudoers.jessie-default.sample rename to resources/sampleconfigs/sudoers.jessie-default.sample diff --git a/misc/sampleconfigs/sudoers.stretch-default.sample b/resources/sampleconfigs/sudoers.stretch-default.sample similarity index 100% rename from misc/sampleconfigs/sudoers.stretch-default.sample rename to resources/sampleconfigs/sudoers.stretch-default.sample diff --git a/misc/sampleconfigs/wpa_supplicant.conf.buster-default.sample b/resources/sampleconfigs/wpa_supplicant.conf.buster-default.sample similarity index 100% rename from misc/sampleconfigs/wpa_supplicant.conf.buster-default.sample rename to resources/sampleconfigs/wpa_supplicant.conf.buster-default.sample diff --git a/misc/sampleconfigs/wpa_supplicant.conf.stretch.sample b/resources/sampleconfigs/wpa_supplicant.conf.stretch.sample similarity index 100% rename from misc/sampleconfigs/wpa_supplicant.conf.stretch.sample rename to resources/sampleconfigs/wpa_supplicant.conf.stretch.sample diff --git a/shared/shutdownsound.wav b/resources/shutdownsound.wav similarity index 100% rename from shared/shutdownsound.wav rename to resources/shutdownsound.wav diff --git a/misc/silence-0.5sec.mp3 b/resources/silence-0.5sec.mp3 similarity index 100% rename from misc/silence-0.5sec.mp3 rename to resources/silence-0.5sec.mp3 diff --git a/misc/silence-2sec.mp3 b/resources/silence-2sec.mp3 similarity index 100% rename from misc/silence-2sec.mp3 rename to resources/silence-2sec.mp3 diff --git a/shared/startupsound.wav b/resources/startupsound.wav similarity index 100% rename from shared/startupsound.wav rename to resources/startupsound.wav diff --git a/Phoniebox/__init__.py b/src/__init__.py similarity index 100% rename from Phoniebox/__init__.py rename to src/__init__.py diff --git a/Phoniebox/cli_client/pbc.c b/src/cli_client/pbc.c similarity index 100% rename from Phoniebox/cli_client/pbc.c rename to src/cli_client/pbc.c diff --git a/Phoniebox/controls/__init__.py b/src/jukebox/__init__.py similarity index 100% rename from Phoniebox/controls/__init__.py rename to src/jukebox/__init__.py diff --git a/Phoniebox/smart-home-automation/MQTT-protocol/README.md b/src/jukebox/components/MQTT-protocol/README.md similarity index 100% rename from Phoniebox/smart-home-automation/MQTT-protocol/README.md rename to src/jukebox/components/MQTT-protocol/README.md diff --git a/Phoniebox/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py b/src/jukebox/components/MQTT-protocol/daemon_mqtt_client.py similarity index 100% rename from Phoniebox/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py rename to src/jukebox/components/MQTT-protocol/daemon_mqtt_client.py diff --git a/Phoniebox/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample b/src/jukebox/components/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample similarity index 100% rename from Phoniebox/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample rename to src/jukebox/components/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample diff --git a/Phoniebox/audio/PirateAudioHAT/README.md b/src/jukebox/components/PirateAudioHAT/README.md similarity index 100% rename from Phoniebox/audio/PirateAudioHAT/README.md rename to src/jukebox/components/PirateAudioHAT/README.md diff --git a/Phoniebox/audio/PirateAudioHAT/requirements.txt b/src/jukebox/components/PirateAudioHAT/requirements.txt similarity index 100% rename from Phoniebox/audio/PirateAudioHAT/requirements.txt rename to src/jukebox/components/PirateAudioHAT/requirements.txt diff --git a/Phoniebox/audio/PirateAudioHAT/setup_pirateAudioHAT.sh b/src/jukebox/components/PirateAudioHAT/setup_pirateAudioHAT.sh similarity index 100% rename from Phoniebox/audio/PirateAudioHAT/setup_pirateAudioHAT.sh rename to src/jukebox/components/PirateAudioHAT/setup_pirateAudioHAT.sh diff --git a/Phoniebox/controls/buttons_usb_encoder/README.md b/src/jukebox/components/buttons_usb_encoder/README.md similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/README.md rename to src/jukebox/components/buttons_usb_encoder/README.md diff --git a/Phoniebox/controls/buttons_usb_encoder/__init__.py b/src/jukebox/components/buttons_usb_encoder/__init__.py similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/__init__.py rename to src/jukebox/components/buttons_usb_encoder/__init__.py diff --git a/Phoniebox/controls/buttons_usb_encoder/buttons-usb-encoder.jpg b/src/jukebox/components/buttons_usb_encoder/buttons-usb-encoder.jpg similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/buttons-usb-encoder.jpg rename to src/jukebox/components/buttons_usb_encoder/buttons-usb-encoder.jpg diff --git a/Phoniebox/controls/buttons_usb_encoder/buttons_usb_encoder.py b/src/jukebox/components/buttons_usb_encoder/buttons_usb_encoder.py similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/buttons_usb_encoder.py rename to src/jukebox/components/buttons_usb_encoder/buttons_usb_encoder.py diff --git a/Phoniebox/controls/buttons_usb_encoder/io_buttons_usb_encoder.py b/src/jukebox/components/buttons_usb_encoder/io_buttons_usb_encoder.py similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/io_buttons_usb_encoder.py rename to src/jukebox/components/buttons_usb_encoder/io_buttons_usb_encoder.py diff --git a/Phoniebox/controls/buttons_usb_encoder/map_buttons_usb_encoder.py b/src/jukebox/components/buttons_usb_encoder/map_buttons_usb_encoder.py similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/map_buttons_usb_encoder.py rename to src/jukebox/components/buttons_usb_encoder/map_buttons_usb_encoder.py diff --git a/Phoniebox/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample b/src/jukebox/components/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample rename to src/jukebox/components/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample diff --git a/Phoniebox/controls/buttons_usb_encoder/register_buttons_usb_encoder.py b/src/jukebox/components/buttons_usb_encoder/register_buttons_usb_encoder.py similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/register_buttons_usb_encoder.py rename to src/jukebox/components/buttons_usb_encoder/register_buttons_usb_encoder.py diff --git a/Phoniebox/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh b/src/jukebox/components/buttons_usb_encoder/setup-buttons-usb-encoder.sh similarity index 100% rename from Phoniebox/controls/buttons_usb_encoder/setup-buttons-usb-encoder.sh rename to src/jukebox/components/buttons_usb_encoder/setup-buttons-usb-encoder.sh diff --git a/Phoniebox/displays/HD44780-i2c/README.md b/src/jukebox/components/displays/HD44780-i2c/README.md similarity index 100% rename from Phoniebox/displays/HD44780-i2c/README.md rename to src/jukebox/components/displays/HD44780-i2c/README.md diff --git a/Phoniebox/displays/HD44780-i2c/i2c-lcd.service.default.sample b/src/jukebox/components/displays/HD44780-i2c/i2c-lcd.service.default.sample similarity index 100% rename from Phoniebox/displays/HD44780-i2c/i2c-lcd.service.default.sample rename to src/jukebox/components/displays/HD44780-i2c/i2c-lcd.service.default.sample diff --git a/Phoniebox/displays/HD44780-i2c/i2c_lcd.py b/src/jukebox/components/displays/HD44780-i2c/i2c_lcd.py similarity index 100% rename from Phoniebox/displays/HD44780-i2c/i2c_lcd.py rename to src/jukebox/components/displays/HD44780-i2c/i2c_lcd.py diff --git a/Phoniebox/displays/HD44780-i2c/i2c_lcd_driver.py b/src/jukebox/components/displays/HD44780-i2c/i2c_lcd_driver.py similarity index 100% rename from Phoniebox/displays/HD44780-i2c/i2c_lcd_driver.py rename to src/jukebox/components/displays/HD44780-i2c/i2c_lcd_driver.py diff --git a/Phoniebox/displays/dot-matrix-module-MAX7219/README.md b/src/jukebox/components/displays/dot-matrix-module-MAX7219/README.md similarity index 100% rename from Phoniebox/displays/dot-matrix-module-MAX7219/README.md rename to src/jukebox/components/displays/dot-matrix-module-MAX7219/README.md diff --git a/Phoniebox/displays/dot-matrix-module-MAX7219/display.ino b/src/jukebox/components/displays/dot-matrix-module-MAX7219/display.ino similarity index 100% rename from Phoniebox/displays/dot-matrix-module-MAX7219/display.ino rename to src/jukebox/components/displays/dot-matrix-module-MAX7219/display.ino diff --git a/Phoniebox/displays/dot-matrix-module-MAX7219/still.jpg b/src/jukebox/components/displays/dot-matrix-module-MAX7219/still.jpg similarity index 100% rename from Phoniebox/displays/dot-matrix-module-MAX7219/still.jpg rename to src/jukebox/components/displays/dot-matrix-module-MAX7219/still.jpg diff --git a/Phoniebox/displays/dot-matrix-module-MAX7219/ticker.gif b/src/jukebox/components/displays/dot-matrix-module-MAX7219/ticker.gif similarity index 100% rename from Phoniebox/displays/dot-matrix-module-MAX7219/ticker.gif rename to src/jukebox/components/displays/dot-matrix-module-MAX7219/ticker.gif diff --git a/Phoniebox/gpio_control/GPIODevices/VolumeControl.py b/src/jukebox/components/gpio_control/GPIODevices/VolumeControl.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/VolumeControl.py rename to src/jukebox/components/gpio_control/GPIODevices/VolumeControl.py diff --git a/Phoniebox/gpio_control/GPIODevices/__init__.py b/src/jukebox/components/gpio_control/GPIODevices/__init__.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/__init__.py rename to src/jukebox/components/gpio_control/GPIODevices/__init__.py diff --git a/Phoniebox/gpio_control/GPIODevices/led.py b/src/jukebox/components/gpio_control/GPIODevices/led.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/led.py rename to src/jukebox/components/gpio_control/GPIODevices/led.py diff --git a/Phoniebox/gpio_control/GPIODevices/rotary_encoder.py b/src/jukebox/components/gpio_control/GPIODevices/rotary_encoder.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/rotary_encoder.py rename to src/jukebox/components/gpio_control/GPIODevices/rotary_encoder.py diff --git a/Phoniebox/gpio_control/GPIODevices/shutdown_button.py b/src/jukebox/components/gpio_control/GPIODevices/shutdown_button.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/shutdown_button.py rename to src/jukebox/components/gpio_control/GPIODevices/shutdown_button.py diff --git a/Phoniebox/gpio_control/GPIODevices/simple_button.py b/src/jukebox/components/gpio_control/GPIODevices/simple_button.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/simple_button.py rename to src/jukebox/components/gpio_control/GPIODevices/simple_button.py diff --git a/Phoniebox/gpio_control/GPIODevices/two_button_control.py b/src/jukebox/components/gpio_control/GPIODevices/two_button_control.py similarity index 100% rename from Phoniebox/gpio_control/GPIODevices/two_button_control.py rename to src/jukebox/components/gpio_control/GPIODevices/two_button_control.py diff --git a/Phoniebox/gpio_control/README.md b/src/jukebox/components/gpio_control/README.md similarity index 100% rename from Phoniebox/gpio_control/README.md rename to src/jukebox/components/gpio_control/README.md diff --git a/Phoniebox/gpio_control/__init__.py b/src/jukebox/components/gpio_control/__init__.py similarity index 100% rename from Phoniebox/gpio_control/__init__.py rename to src/jukebox/components/gpio_control/__init__.py diff --git a/Phoniebox/gpio_control/check_installation.sh b/src/jukebox/components/gpio_control/check_installation.sh similarity index 100% rename from Phoniebox/gpio_control/check_installation.sh rename to src/jukebox/components/gpio_control/check_installation.sh diff --git a/Phoniebox/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini b/src/jukebox/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini similarity index 100% rename from Phoniebox/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini rename to src/jukebox/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini diff --git a/Phoniebox/gpio_control/example_configs/gpio_settings.ini b/src/jukebox/components/gpio_control/example_configs/gpio_settings.ini similarity index 100% rename from Phoniebox/gpio_control/example_configs/gpio_settings.ini rename to src/jukebox/components/gpio_control/example_configs/gpio_settings.ini diff --git a/Phoniebox/gpio_control/example_configs/gpio_settings_rotary_and_led.ini b/src/jukebox/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini similarity index 100% rename from Phoniebox/gpio_control/example_configs/gpio_settings_rotary_and_led.ini rename to src/jukebox/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini diff --git a/Phoniebox/gpio_control/example_configs/gpio_settings_test.ini b/src/jukebox/components/gpio_control/example_configs/gpio_settings_test.ini similarity index 100% rename from Phoniebox/gpio_control/example_configs/gpio_settings_test.ini rename to src/jukebox/components/gpio_control/example_configs/gpio_settings_test.ini diff --git a/Phoniebox/gpio_control/function_calls.py b/src/jukebox/components/gpio_control/function_calls.py similarity index 100% rename from Phoniebox/gpio_control/function_calls.py rename to src/jukebox/components/gpio_control/function_calls.py diff --git a/Phoniebox/gpio_control/gpio_control.py b/src/jukebox/components/gpio_control/gpio_control.py similarity index 100% rename from Phoniebox/gpio_control/gpio_control.py rename to src/jukebox/components/gpio_control/gpio_control.py diff --git a/Phoniebox/gpio_control/install.sh b/src/jukebox/components/gpio_control/install.sh similarity index 100% rename from Phoniebox/gpio_control/install.sh rename to src/jukebox/components/gpio_control/install.sh diff --git a/Phoniebox/gpio_control/requirements.txt b/src/jukebox/components/gpio_control/requirements.txt similarity index 100% rename from Phoniebox/gpio_control/requirements.txt rename to src/jukebox/components/gpio_control/requirements.txt diff --git a/Phoniebox/gpio_control/test/__init__.py b/src/jukebox/components/gpio_control/test/__init__.py similarity index 100% rename from Phoniebox/gpio_control/test/__init__.py rename to src/jukebox/components/gpio_control/test/__init__.py diff --git a/Phoniebox/gpio_control/test/conftest.py b/src/jukebox/components/gpio_control/test/conftest.py similarity index 100% rename from Phoniebox/gpio_control/test/conftest.py rename to src/jukebox/components/gpio_control/test/conftest.py diff --git a/Phoniebox/gpio_control/test/gpio_settings_test.ini b/src/jukebox/components/gpio_control/test/gpio_settings_test.ini similarity index 100% rename from Phoniebox/gpio_control/test/gpio_settings_test.ini rename to src/jukebox/components/gpio_control/test/gpio_settings_test.ini diff --git a/Phoniebox/gpio_control/test/test_RotaryEncoder.py b/src/jukebox/components/gpio_control/test/test_RotaryEncoder.py similarity index 100% rename from Phoniebox/gpio_control/test/test_RotaryEncoder.py rename to src/jukebox/components/gpio_control/test/test_RotaryEncoder.py diff --git a/Phoniebox/gpio_control/test/test_SimpleButton.py b/src/jukebox/components/gpio_control/test/test_SimpleButton.py similarity index 100% rename from Phoniebox/gpio_control/test/test_SimpleButton.py rename to src/jukebox/components/gpio_control/test/test_SimpleButton.py diff --git a/Phoniebox/gpio_control/test/test_TwoButtonControl.py b/src/jukebox/components/gpio_control/test/test_TwoButtonControl.py similarity index 100% rename from Phoniebox/gpio_control/test/test_TwoButtonControl.py rename to src/jukebox/components/gpio_control/test/test_TwoButtonControl.py diff --git a/Phoniebox/gpio_control/test/test_gpio_control.py b/src/jukebox/components/gpio_control/test/test_gpio_control.py similarity index 100% rename from Phoniebox/gpio_control/test/test_gpio_control.py rename to src/jukebox/components/gpio_control/test/test_gpio_control.py diff --git a/Phoniebox/gpio_control/test/test_shutdown_button.py b/src/jukebox/components/gpio_control/test/test_shutdown_button.py similarity index 100% rename from Phoniebox/gpio_control/test/test_shutdown_button.py rename to src/jukebox/components/gpio_control/test/test_shutdown_button.py diff --git a/Phoniebox/rfid_reader/FakeRfidReader.py b/src/jukebox/components/rfid_reader/FakeRfidReader.py similarity index 100% rename from Phoniebox/rfid_reader/FakeRfidReader.py rename to src/jukebox/components/rfid_reader/FakeRfidReader.py diff --git a/Phoniebox/rfid_reader/PN532/README.md b/src/jukebox/components/rfid_reader/PN532/README.md similarity index 100% rename from Phoniebox/rfid_reader/PN532/README.md rename to src/jukebox/components/rfid_reader/PN532/README.md diff --git a/Phoniebox/rfid_reader/PN532/requirements.txt b/src/jukebox/components/rfid_reader/PN532/requirements.txt similarity index 100% rename from Phoniebox/rfid_reader/PN532/requirements.txt rename to src/jukebox/components/rfid_reader/PN532/requirements.txt diff --git a/Phoniebox/rfid_reader/PN532/reset_pn532.sh b/src/jukebox/components/rfid_reader/PN532/reset_pn532.sh similarity index 100% rename from Phoniebox/rfid_reader/PN532/reset_pn532.sh rename to src/jukebox/components/rfid_reader/PN532/reset_pn532.sh diff --git a/Phoniebox/rfid_reader/PN532/setup_pn532.sh b/src/jukebox/components/rfid_reader/PN532/setup_pn532.sh similarity index 100% rename from Phoniebox/rfid_reader/PN532/setup_pn532.sh rename to src/jukebox/components/rfid_reader/PN532/setup_pn532.sh diff --git a/Phoniebox/rfid_reader/PhonieboxRfidReader.py b/src/jukebox/components/rfid_reader/PhonieboxRfidReader.py similarity index 99% rename from Phoniebox/rfid_reader/PhonieboxRfidReader.py rename to src/jukebox/components/rfid_reader/PhonieboxRfidReader.py index 8cf357e18..07a09e9ef 100644 --- a/Phoniebox/rfid_reader/PhonieboxRfidReader.py +++ b/src/jukebox/components/rfid_reader/PhonieboxRfidReader.py @@ -10,7 +10,7 @@ import logging -from rpc.PhonieboxRpcClient import PhonieboxRpcClient +from jukebox.rpc.client import PhonieboxRpcClient #from evdev import InputDevice, categorize, ecodes, list_devices diff --git a/Phoniebox/rfid_reader/RC522/README.md b/src/jukebox/components/rfid_reader/RC522/README.md similarity index 100% rename from Phoniebox/rfid_reader/RC522/README.md rename to src/jukebox/components/rfid_reader/RC522/README.md diff --git a/Phoniebox/rfid_reader/RC522/requirements.txt b/src/jukebox/components/rfid_reader/RC522/requirements.txt similarity index 100% rename from Phoniebox/rfid_reader/RC522/requirements.txt rename to src/jukebox/components/rfid_reader/RC522/requirements.txt diff --git a/Phoniebox/rfid_reader/RC522/setup_rc522.sh b/src/jukebox/components/rfid_reader/RC522/setup_rc522.sh similarity index 100% rename from Phoniebox/rfid_reader/RC522/setup_rc522.sh rename to src/jukebox/components/rfid_reader/RC522/setup_rc522.sh diff --git a/Phoniebox/rfid_reader/RfidReader_PN532.py b/src/jukebox/components/rfid_reader/RfidReader_PN532.py similarity index 100% rename from Phoniebox/rfid_reader/RfidReader_PN532.py rename to src/jukebox/components/rfid_reader/RfidReader_PN532.py diff --git a/Phoniebox/rfid_reader/RfidReader_RC522.py b/src/jukebox/components/rfid_reader/RfidReader_RC522.py similarity index 100% rename from Phoniebox/rfid_reader/RfidReader_RC522.py rename to src/jukebox/components/rfid_reader/RfidReader_RC522.py diff --git a/Phoniebox/rfid_reader/RfidReader_RDM6300.py b/src/jukebox/components/rfid_reader/RfidReader_RDM6300.py similarity index 100% rename from Phoniebox/rfid_reader/RfidReader_RDM6300.py rename to src/jukebox/components/rfid_reader/RfidReader_RDM6300.py diff --git a/Phoniebox/player/__init__.py b/src/jukebox/components/rfid_reader/__init__.py similarity index 100% rename from Phoniebox/player/__init__.py rename to src/jukebox/components/rfid_reader/__init__.py diff --git a/Phoniebox/PhonieboxNvManager.py b/src/jukebox/jukebox/NvManager.py similarity index 100% rename from Phoniebox/PhonieboxNvManager.py rename to src/jukebox/jukebox/NvManager.py diff --git a/Phoniebox/PhonieboxSystem.py b/src/jukebox/jukebox/System.py similarity index 100% rename from Phoniebox/PhonieboxSystem.py rename to src/jukebox/jukebox/System.py diff --git a/Phoniebox/PhonieboxVolume.py b/src/jukebox/jukebox/Volume.py similarity index 100% rename from Phoniebox/PhonieboxVolume.py rename to src/jukebox/jukebox/Volume.py diff --git a/Phoniebox/rfid_reader/__init__.py b/src/jukebox/jukebox/__init__.py similarity index 100% rename from Phoniebox/rfid_reader/__init__.py rename to src/jukebox/jukebox/__init__.py diff --git a/Phoniebox/PhonieboxDaemon.py b/src/jukebox/jukebox/daemon.py similarity index 75% rename from Phoniebox/PhonieboxDaemon.py rename to src/jukebox/jukebox/daemon.py index 72b3812ea..eaad200e1 100755 --- a/Phoniebox/PhonieboxDaemon.py +++ b/src/jukebox/jukebox/daemon.py @@ -4,16 +4,15 @@ import threading import sys, os.path import signal -import argparse import configparser from time import sleep, time -import PhonieboxVolume -import PhonieboxSystem -from player import PhonieboxPlayerMPD -from rpc.PhonieboxRpcServer import PhonieboxRpcServer -from PhonieboxNvManager import nv_manager -from rfid_reader.PhonieboxRfidReader import RFID_Reader +import jukebox.Volume +import jukebox.System +from player import PlayerMPD +from jukebox.rpc.Server import PhonieboxRpcServer +from jukebox.NvManager import nv_manager +from components.rfid_reader.PhonieboxRfidReader import RFID_Reader #from gpio_control import gpio_control g_nvm = None @@ -46,28 +45,20 @@ def dump_config_options(phoniebox_config,filename): print(option+" = "+phoniebox_config.get(section, option)) print ("\n") -if __name__ == "__main__": - - # get absolute path of this script - dir_path = os.path.dirname(os.path.realpath(__file__)) - defaultconfigFilePath = os.path.join(dir_path, '../settings/phoniebox.conf') - - argparser = argparse.ArgumentParser(description='The PhonieboxDaemon') - argparser.add_argument('configuration_file', type=argparse.FileType('r'),nargs='?',default=defaultconfigFilePath) - argparser.add_argument('--verbose', '-v', action='count', default=0) - args = argparser.parse_args() - +def jukebox_daemon(configuration_file=None, verbose=0): + phoniebox_config = configparser.ConfigParser(inline_comment_prefixes=";") - phoniebox_config.read(args.configuration_file.name) + phoniebox_config.read(configuration_file) print ("Starting the "+ phoniebox_config.get('SYSTEM', 'BOX_NAME') +" Daemon") - if args.verbose: + if verbose: dump_config_options(phoniebox_config,args.configuration_file.name) # Play Startup Sound - volume_control = PhonieboxVolume.volume_control_alsa(listcards=False) - startsound_thread = threading.Thread(target=volume_control.play_wave_file, args=["../shared/startupsound.wav"]) + volume_control = jukebox.Volume.volume_control_alsa(listcards=False) + + startsound_thread = threading.Thread(target=volume_control.play_wave_file, args=[phoniebox_config.get('SYSTEM', 'STARTUP_SOUND')]) startsound_thread.start() g_nvm = nv_manager() @@ -80,8 +71,8 @@ def dump_config_options(phoniebox_config,filename): #initialize Phonibox objcts objects = {'volume':volume_control, - 'player':PhonieboxPlayerMPD.player_control(music_player_status,volume_control), - 'system':PhonieboxSystem.system_control} + 'player':PlayerMPD.player_control(music_player_status,volume_control), + 'system':jukebox.System.system_control} print ("Init Phonibox RPC Server ") rpcs = PhonieboxRpcServer(objects) diff --git a/Phoniebox/rpc/PhonieboxRpcServer.py b/src/jukebox/jukebox/rpc/Server.py similarity index 100% rename from Phoniebox/rpc/PhonieboxRpcServer.py rename to src/jukebox/jukebox/rpc/Server.py diff --git a/Phoniebox/rpc/__init__.py b/src/jukebox/jukebox/rpc/__init__.py similarity index 100% rename from Phoniebox/rpc/__init__.py rename to src/jukebox/jukebox/rpc/__init__.py diff --git a/Phoniebox/rpc/PhonieboxRpcClient.py b/src/jukebox/jukebox/rpc/client.py similarity index 100% rename from Phoniebox/rpc/PhonieboxRpcClient.py rename to src/jukebox/jukebox/rpc/client.py diff --git a/Phoniebox/player/PhonieboxPlayerMPD.py b/src/jukebox/player/PlayerMPD.py similarity index 100% rename from Phoniebox/player/PhonieboxPlayerMPD.py rename to src/jukebox/player/PlayerMPD.py diff --git a/src/jukebox/player/__init__.py b/src/jukebox/player/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/jukebox/run_jukebox.py b/src/jukebox/run_jukebox.py new file mode 100644 index 000000000..6b437de75 --- /dev/null +++ b/src/jukebox/run_jukebox.py @@ -0,0 +1,21 @@ +import sys, os.path +import argparse +import jukebox.daemon + + +if __name__ == "__main__": + + print ("hallo") + + # get absolute path of this script + dir_path = os.path.dirname(os.path.realpath(__file__)) + defaultconfigFilePath = os.path.join(dir_path, '../../settings/phoniebox.conf') + + argparser = argparse.ArgumentParser(description='The JukeboxDaemon') + argparser.add_argument('configuration_file', type=argparse.FileType('r'),nargs='?',default=defaultconfigFilePath) + ##help=f"Reader configuration file [default: '{default_reader_cfg_file}']", + argparser.add_argument('--verbose', '-v', action='count', default=0) + + args = argparser.parse_args() + + jukebox.daemon.jukebox_daemon(args.configuration_file.name) diff --git a/Phoniebox/requirements.txt b/src/requirements.txt similarity index 100% rename from Phoniebox/requirements.txt rename to src/requirements.txt diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css b/src/ui/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css rename to src/ui/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css.map b/src/ui/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css.map similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css.map rename to src/ui/_assets/MaterialDesign-Webfont-master/css/materialdesignicons.min.css.map diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.eot b/src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.eot similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.eot rename to src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.eot diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.svg b/src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.svg similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.svg rename to src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.svg diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.ttf b/src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.ttf similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.ttf rename to src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.ttf diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff b/src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff rename to src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff2 b/src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff2 similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff2 rename to src/ui/_assets/MaterialDesign-Webfont-master/fonts/materialdesignicons-webfont.woff2 diff --git a/htdocs/_assets/MaterialDesign-Webfont-master/license.md b/src/ui/_assets/MaterialDesign-Webfont-master/license.md similarity index 100% rename from htdocs/_assets/MaterialDesign-Webfont-master/license.md rename to src/ui/_assets/MaterialDesign-Webfont-master/license.md diff --git a/htdocs/_assets/bootstrap-3/css/bootstrap.cosmo.css b/src/ui/_assets/bootstrap-3/css/bootstrap.cosmo.css similarity index 100% rename from htdocs/_assets/bootstrap-3/css/bootstrap.cosmo.css rename to src/ui/_assets/bootstrap-3/css/bootstrap.cosmo.css diff --git a/htdocs/_assets/bootstrap-3/css/bootstrap.darkly.css b/src/ui/_assets/bootstrap-3/css/bootstrap.darkly.css similarity index 100% rename from htdocs/_assets/bootstrap-3/css/bootstrap.darkly.css rename to src/ui/_assets/bootstrap-3/css/bootstrap.darkly.css diff --git a/htdocs/_assets/bootstrap-3/css/bootstrap.min.css b/src/ui/_assets/bootstrap-3/css/bootstrap.min.css similarity index 100% rename from htdocs/_assets/bootstrap-3/css/bootstrap.min.css rename to src/ui/_assets/bootstrap-3/css/bootstrap.min.css diff --git a/htdocs/_assets/bootstrap-3/js/bootstrap.min.js b/src/ui/_assets/bootstrap-3/js/bootstrap.min.js similarity index 100% rename from htdocs/_assets/bootstrap-3/js/bootstrap.min.js rename to src/ui/_assets/bootstrap-3/js/bootstrap.min.js diff --git a/htdocs/_assets/bootstrap-3/js/collapse.js b/src/ui/_assets/bootstrap-3/js/collapse.js similarity index 100% rename from htdocs/_assets/bootstrap-3/js/collapse.js rename to src/ui/_assets/bootstrap-3/js/collapse.js diff --git a/htdocs/_assets/bootstrap-3/js/html5shiv3.7.2.min.js b/src/ui/_assets/bootstrap-3/js/html5shiv3.7.2.min.js similarity index 100% rename from htdocs/_assets/bootstrap-3/js/html5shiv3.7.2.min.js rename to src/ui/_assets/bootstrap-3/js/html5shiv3.7.2.min.js diff --git a/htdocs/_assets/bootstrap-3/js/respond1.4.2.min.js b/src/ui/_assets/bootstrap-3/js/respond1.4.2.min.js similarity index 100% rename from htdocs/_assets/bootstrap-3/js/respond1.4.2.min.js rename to src/ui/_assets/bootstrap-3/js/respond1.4.2.min.js diff --git a/htdocs/_assets/bootstrap-3/js/transition.js b/src/ui/_assets/bootstrap-3/js/transition.js similarity index 100% rename from htdocs/_assets/bootstrap-3/js/transition.js rename to src/ui/_assets/bootstrap-3/js/transition.js diff --git a/htdocs/_assets/css/circle.css b/src/ui/_assets/css/circle.css similarity index 100% rename from htdocs/_assets/css/circle.css rename to src/ui/_assets/css/circle.css diff --git a/htdocs/_assets/css/collapsible.css b/src/ui/_assets/css/collapsible.css similarity index 100% rename from htdocs/_assets/css/collapsible.css rename to src/ui/_assets/css/collapsible.css diff --git a/htdocs/_assets/font-awesome/css/font-awesome.min.css b/src/ui/_assets/font-awesome/css/font-awesome.min.css similarity index 100% rename from htdocs/_assets/font-awesome/css/font-awesome.min.css rename to src/ui/_assets/font-awesome/css/font-awesome.min.css diff --git a/htdocs/_assets/font-awesome/fonts/FontAwesome.otf b/src/ui/_assets/font-awesome/fonts/FontAwesome.otf similarity index 100% rename from htdocs/_assets/font-awesome/fonts/FontAwesome.otf rename to src/ui/_assets/font-awesome/fonts/FontAwesome.otf diff --git a/htdocs/_assets/font-awesome/fonts/fontawesome-webfont.eot b/src/ui/_assets/font-awesome/fonts/fontawesome-webfont.eot similarity index 100% rename from htdocs/_assets/font-awesome/fonts/fontawesome-webfont.eot rename to src/ui/_assets/font-awesome/fonts/fontawesome-webfont.eot diff --git a/htdocs/_assets/font-awesome/fonts/fontawesome-webfont.svg b/src/ui/_assets/font-awesome/fonts/fontawesome-webfont.svg similarity index 100% rename from htdocs/_assets/font-awesome/fonts/fontawesome-webfont.svg rename to src/ui/_assets/font-awesome/fonts/fontawesome-webfont.svg diff --git a/htdocs/_assets/font-awesome/fonts/fontawesome-webfont.ttf b/src/ui/_assets/font-awesome/fonts/fontawesome-webfont.ttf similarity index 100% rename from htdocs/_assets/font-awesome/fonts/fontawesome-webfont.ttf rename to src/ui/_assets/font-awesome/fonts/fontawesome-webfont.ttf diff --git a/htdocs/_assets/font-awesome/fonts/fontawesome-webfont.woff b/src/ui/_assets/font-awesome/fonts/fontawesome-webfont.woff similarity index 100% rename from htdocs/_assets/font-awesome/fonts/fontawesome-webfont.woff rename to src/ui/_assets/font-awesome/fonts/fontawesome-webfont.woff diff --git a/htdocs/_assets/font-awesome/fonts/fontawesome-webfont.woff2 b/src/ui/_assets/font-awesome/fonts/fontawesome-webfont.woff2 similarity index 100% rename from htdocs/_assets/font-awesome/fonts/fontawesome-webfont.woff2 rename to src/ui/_assets/font-awesome/fonts/fontawesome-webfont.woff2 diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/OFL.txt b/src/ui/_assets/fonts/Source_Sans_Pro/OFL.txt similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/OFL.txt rename to src/ui/_assets/fonts/Source_Sans_Pro/OFL.txt diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Black.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Black.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Black.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Black.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-BlackItalic.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-BlackItalic.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-BlackItalic.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-BlackItalic.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-BoldItalic.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-BoldItalic.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-BoldItalic.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-BoldItalic.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLight.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightItalic.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightItalic.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightItalic.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-ExtraLightItalic.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Italic.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Italic.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Italic.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Italic.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-LightItalic.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-LightItalic.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-LightItalic.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-LightItalic.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf diff --git a/htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBoldItalic.ttf b/src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBoldItalic.ttf similarity index 100% rename from htdocs/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBoldItalic.ttf rename to src/ui/_assets/fonts/Source_Sans_Pro/SourceSansPro-SemiBoldItalic.ttf diff --git a/htdocs/_assets/icons/android-icon-144x144.png b/src/ui/_assets/icons/android-icon-144x144.png similarity index 100% rename from htdocs/_assets/icons/android-icon-144x144.png rename to src/ui/_assets/icons/android-icon-144x144.png diff --git a/htdocs/_assets/icons/android-icon-192x192.png b/src/ui/_assets/icons/android-icon-192x192.png similarity index 100% rename from htdocs/_assets/icons/android-icon-192x192.png rename to src/ui/_assets/icons/android-icon-192x192.png diff --git a/htdocs/_assets/icons/android-icon-36x36.png b/src/ui/_assets/icons/android-icon-36x36.png similarity index 100% rename from htdocs/_assets/icons/android-icon-36x36.png rename to src/ui/_assets/icons/android-icon-36x36.png diff --git a/htdocs/_assets/icons/android-icon-48x48.png b/src/ui/_assets/icons/android-icon-48x48.png similarity index 100% rename from htdocs/_assets/icons/android-icon-48x48.png rename to src/ui/_assets/icons/android-icon-48x48.png diff --git a/htdocs/_assets/icons/android-icon-72x72.png b/src/ui/_assets/icons/android-icon-72x72.png similarity index 100% rename from htdocs/_assets/icons/android-icon-72x72.png rename to src/ui/_assets/icons/android-icon-72x72.png diff --git a/htdocs/_assets/icons/android-icon-96x96.png b/src/ui/_assets/icons/android-icon-96x96.png similarity index 100% rename from htdocs/_assets/icons/android-icon-96x96.png rename to src/ui/_assets/icons/android-icon-96x96.png diff --git a/htdocs/_assets/icons/apple-icon-114x114.png b/src/ui/_assets/icons/apple-icon-114x114.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-114x114.png rename to src/ui/_assets/icons/apple-icon-114x114.png diff --git a/htdocs/_assets/icons/apple-icon-120x120.png b/src/ui/_assets/icons/apple-icon-120x120.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-120x120.png rename to src/ui/_assets/icons/apple-icon-120x120.png diff --git a/htdocs/_assets/icons/apple-icon-144x144.png b/src/ui/_assets/icons/apple-icon-144x144.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-144x144.png rename to src/ui/_assets/icons/apple-icon-144x144.png diff --git a/htdocs/_assets/icons/apple-icon-152x152.png b/src/ui/_assets/icons/apple-icon-152x152.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-152x152.png rename to src/ui/_assets/icons/apple-icon-152x152.png diff --git a/htdocs/_assets/icons/apple-icon-180x180.png b/src/ui/_assets/icons/apple-icon-180x180.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-180x180.png rename to src/ui/_assets/icons/apple-icon-180x180.png diff --git a/htdocs/_assets/icons/apple-icon-57x57.png b/src/ui/_assets/icons/apple-icon-57x57.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-57x57.png rename to src/ui/_assets/icons/apple-icon-57x57.png diff --git a/htdocs/_assets/icons/apple-icon-60x60.png b/src/ui/_assets/icons/apple-icon-60x60.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-60x60.png rename to src/ui/_assets/icons/apple-icon-60x60.png diff --git a/htdocs/_assets/icons/apple-icon-72x72.png b/src/ui/_assets/icons/apple-icon-72x72.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-72x72.png rename to src/ui/_assets/icons/apple-icon-72x72.png diff --git a/htdocs/_assets/icons/apple-icon-76x76.png b/src/ui/_assets/icons/apple-icon-76x76.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-76x76.png rename to src/ui/_assets/icons/apple-icon-76x76.png diff --git a/htdocs/_assets/icons/apple-icon-precomposed.png b/src/ui/_assets/icons/apple-icon-precomposed.png similarity index 100% rename from htdocs/_assets/icons/apple-icon-precomposed.png rename to src/ui/_assets/icons/apple-icon-precomposed.png diff --git a/htdocs/_assets/icons/apple-icon.png b/src/ui/_assets/icons/apple-icon.png similarity index 100% rename from htdocs/_assets/icons/apple-icon.png rename to src/ui/_assets/icons/apple-icon.png diff --git a/htdocs/_assets/icons/browserconfig.xml b/src/ui/_assets/icons/browserconfig.xml similarity index 100% rename from htdocs/_assets/icons/browserconfig.xml rename to src/ui/_assets/icons/browserconfig.xml diff --git a/htdocs/_assets/icons/favicon-16x16.png b/src/ui/_assets/icons/favicon-16x16.png similarity index 100% rename from htdocs/_assets/icons/favicon-16x16.png rename to src/ui/_assets/icons/favicon-16x16.png diff --git a/htdocs/_assets/icons/favicon-32x32.png b/src/ui/_assets/icons/favicon-32x32.png similarity index 100% rename from htdocs/_assets/icons/favicon-32x32.png rename to src/ui/_assets/icons/favicon-32x32.png diff --git a/htdocs/_assets/icons/favicon-96x96.png b/src/ui/_assets/icons/favicon-96x96.png similarity index 100% rename from htdocs/_assets/icons/favicon-96x96.png rename to src/ui/_assets/icons/favicon-96x96.png diff --git a/htdocs/_assets/icons/favicon.ico b/src/ui/_assets/icons/favicon.ico similarity index 100% rename from htdocs/_assets/icons/favicon.ico rename to src/ui/_assets/icons/favicon.ico diff --git a/htdocs/_assets/icons/manifest.json b/src/ui/_assets/icons/manifest.json similarity index 100% rename from htdocs/_assets/icons/manifest.json rename to src/ui/_assets/icons/manifest.json diff --git a/htdocs/_assets/icons/ms-icon-144x144.png b/src/ui/_assets/icons/ms-icon-144x144.png similarity index 100% rename from htdocs/_assets/icons/ms-icon-144x144.png rename to src/ui/_assets/icons/ms-icon-144x144.png diff --git a/htdocs/_assets/icons/ms-icon-150x150.png b/src/ui/_assets/icons/ms-icon-150x150.png similarity index 100% rename from htdocs/_assets/icons/ms-icon-150x150.png rename to src/ui/_assets/icons/ms-icon-150x150.png diff --git a/htdocs/_assets/icons/ms-icon-310x310.png b/src/ui/_assets/icons/ms-icon-310x310.png similarity index 100% rename from htdocs/_assets/icons/ms-icon-310x310.png rename to src/ui/_assets/icons/ms-icon-310x310.png diff --git a/htdocs/_assets/icons/ms-icon-70x70.png b/src/ui/_assets/icons/ms-icon-70x70.png similarity index 100% rename from htdocs/_assets/icons/ms-icon-70x70.png rename to src/ui/_assets/icons/ms-icon-70x70.png diff --git a/htdocs/_assets/img/No_Cover.jpg b/src/ui/_assets/img/No_Cover.jpg similarity index 100% rename from htdocs/_assets/img/No_Cover.jpg rename to src/ui/_assets/img/No_Cover.jpg diff --git a/htdocs/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload-ui.css b/src/ui/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload-ui.css similarity index 100% rename from htdocs/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload-ui.css rename to src/ui/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload-ui.css diff --git a/htdocs/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload.css b/src/ui/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload.css similarity index 100% rename from htdocs/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload.css rename to src/ui/_assets/jQuery-File-Upload-9.22.0/css/jquery.fileupload.css diff --git a/htdocs/_assets/js/jquery.1.12.4.min.js b/src/ui/_assets/js/jquery.1.12.4.min.js similarity index 100% rename from htdocs/_assets/js/jquery.1.12.4.min.js rename to src/ui/_assets/js/jquery.1.12.4.min.js diff --git a/htdocs/ajax.loadInfo.php b/src/ui/ajax.loadInfo.php similarity index 100% rename from htdocs/ajax.loadInfo.php rename to src/ui/ajax.loadInfo.php diff --git a/htdocs/ajax.loadMPDStatus.php b/src/ui/ajax.loadMPDStatus.php similarity index 100% rename from htdocs/ajax.loadMPDStatus.php rename to src/ui/ajax.loadMPDStatus.php diff --git a/htdocs/ajax.loadMopidyStatus.php b/src/ui/ajax.loadMopidyStatus.php similarity index 100% rename from htdocs/ajax.loadMopidyStatus.php rename to src/ui/ajax.loadMopidyStatus.php diff --git a/htdocs/ajax.loadOverallTime.php b/src/ui/ajax.loadOverallTime.php similarity index 100% rename from htdocs/ajax.loadOverallTime.php rename to src/ui/ajax.loadOverallTime.php diff --git a/htdocs/ajax.refresh_id.php b/src/ui/ajax.refresh_id.php similarity index 100% rename from htdocs/ajax.refresh_id.php rename to src/ui/ajax.refresh_id.php diff --git a/htdocs/api/PhonieboxRpcClient.php b/src/ui/api/PhonieboxRpcClient.php similarity index 100% rename from htdocs/api/PhonieboxRpcClient.php rename to src/ui/api/PhonieboxRpcClient.php diff --git a/htdocs/api/common.php b/src/ui/api/common.php similarity index 100% rename from htdocs/api/common.php rename to src/ui/api/common.php diff --git a/htdocs/api/cover.php b/src/ui/api/cover.php similarity index 64% rename from htdocs/api/cover.php rename to src/ui/api/cover.php index 8ee1f5655..354451651 100755 --- a/htdocs/api/cover.php +++ b/src/ui/api/cover.php @@ -5,18 +5,18 @@ * debug? Conf file line: * DEBUG_WebApp_API="TRUE" */ -$debugLoggingConf = parse_ini_file("../../settings/debugLogging.conf"); +$debugLoggingConf = parse_ini_file("../../../settings/debugLogging.conf"); if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n# WebApp API # " . __FILE__ , FILE_APPEND | LOCK_EX); - file_put_contents("../../logs/debug.log", "\n # \$_SERVER['REQUEST_METHOD']: " . $_SERVER['REQUEST_METHOD'] , FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n# WebApp API # " . __FILE__ , FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n # \$_SERVER['REQUEST_METHOD']: " . $_SERVER['REQUEST_METHOD'] , FILE_APPEND | LOCK_EX); } /** * Returns the cover of the currently played folder. */ -$Audio_Folders_Path = trim(file_get_contents('../../settings/Audio_Folders_Path')); -$Latest_Folder_Played = trim(file_get_contents('../../settings/Latest_Folder_Played')); +$Audio_Folders_Path = trim(file_get_contents('../../../settings/Audio_Folders_Path')); +$Latest_Folder_Played = trim(file_get_contents('../../../settings/Latest_Folder_Played')); $spover = $Audio_Folders_Path."/../../settings/cover.jpg"; $ocover = $Audio_Folders_Path."/".$Latest_Folder_Played."/cover.jpg"; diff --git a/htdocs/api/latest.php b/src/ui/api/latest.php similarity index 100% rename from htdocs/api/latest.php rename to src/ui/api/latest.php diff --git a/htdocs/api/player.php b/src/ui/api/player.php similarity index 83% rename from htdocs/api/player.php rename to src/ui/api/player.php index aadd3e348..5eaf5bc31 100755 --- a/htdocs/api/player.php +++ b/src/ui/api/player.php @@ -32,11 +32,11 @@ * debug? Conf file line: * DEBUG_WebApp_API="TRUE" */ -$debugLoggingConf = parse_ini_file("../../settings/debugLogging.conf"); +$debugLoggingConf = parse_ini_file("../../../settings/debugLogging.conf"); if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n# WebApp API # " . __FILE__, FILE_APPEND | LOCK_EX); - file_put_contents("../../logs/debug.log", "\n # \$_SERVER['REQUEST_METHOD']: " . $_SERVER['REQUEST_METHOD'], FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n# WebApp API # " . __FILE__, FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n # \$_SERVER['REQUEST_METHOD']: " . $_SERVER['REQUEST_METHOD'], FILE_APPEND | LOCK_EX); } if ($_SERVER['REQUEST_METHOD'] === 'PUT') { handlePut(); @@ -50,13 +50,13 @@ function handlePut() { global $debugLoggingConf; global $command_map; if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n # function handlePut() ", FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n # function handlePut() ", FILE_APPEND | LOCK_EX); } $body = file_get_contents('php://input'); $json = json_decode(trim($body), TRUE); if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n # \$json['command']:" . $json['command'], FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n # \$json['command']:" . $json['command'], FILE_APPEND | LOCK_EX); } $inputCommand = $json['command']; $inputValue = $json['value'] ?? ""; @@ -119,8 +119,8 @@ function handleGet() { */ if ($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n # function handleGet() ", FILE_APPEND | LOCK_EX); - file_put_contents("../../logs/debug.log", "\n\$responseList: " . json_encode($responseList) . $_SERVER['REQUEST_METHOD'], FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n # function handleGet() ", FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n\$responseList: " . json_encode($responseList) . $_SERVER['REQUEST_METHOD'], FILE_APPEND | LOCK_EX); } header('Content-Type: application/json'); diff --git a/htdocs/api/playlist.php b/src/ui/api/playlist.php similarity index 89% rename from htdocs/api/playlist.php rename to src/ui/api/playlist.php index e43d63c59..a13f8c0c4 100755 --- a/htdocs/api/playlist.php +++ b/src/ui/api/playlist.php @@ -7,10 +7,10 @@ * debug? Conf file line: * DEBUG_WebApp_API="TRUE" */ -$debugLoggingConf = parse_ini_file("../../settings/debugLogging.conf"); +$debugLoggingConf = parse_ini_file("../../../settings/debugLogging.conf"); if($debugLoggingConf['DEBUG_WebApp_API'] == "TRUE") { - file_put_contents("../../logs/debug.log", "\n# WebApp API # " . __FILE__ , FILE_APPEND | LOCK_EX); - file_put_contents("../../logs/debug.log", "\n # \$_SERVER['REQUEST_METHOD']: " . $_SERVER['REQUEST_METHOD'] , FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n# WebApp API # " . __FILE__ , FILE_APPEND | LOCK_EX); + file_put_contents("../../../logs/debug.log", "\n # \$_SERVER['REQUEST_METHOD']: " . $_SERVER['REQUEST_METHOD'] , FILE_APPEND | LOCK_EX); } /*** diff --git a/htdocs/api/playlist/appendFileToPlaylist.php b/src/ui/api/playlist/appendFileToPlaylist.php similarity index 100% rename from htdocs/api/playlist/appendFileToPlaylist.php rename to src/ui/api/playlist/appendFileToPlaylist.php diff --git a/htdocs/api/playlist/moveDownSongInPlaylist.php b/src/ui/api/playlist/moveDownSongInPlaylist.php similarity index 100% rename from htdocs/api/playlist/moveDownSongInPlaylist.php rename to src/ui/api/playlist/moveDownSongInPlaylist.php diff --git a/htdocs/api/playlist/moveUpSongInPlaylist.php b/src/ui/api/playlist/moveUpSongInPlaylist.php similarity index 100% rename from htdocs/api/playlist/moveUpSongInPlaylist.php rename to src/ui/api/playlist/moveUpSongInPlaylist.php diff --git a/htdocs/api/playlist/playsinglefile.php b/src/ui/api/playlist/playsinglefile.php similarity index 100% rename from htdocs/api/playlist/playsinglefile.php rename to src/ui/api/playlist/playsinglefile.php diff --git a/htdocs/api/playlist/removeSongFromPlaylist.php b/src/ui/api/playlist/removeSongFromPlaylist.php similarity index 100% rename from htdocs/api/playlist/removeSongFromPlaylist.php rename to src/ui/api/playlist/removeSongFromPlaylist.php diff --git a/htdocs/api/playlist/resume.php b/src/ui/api/playlist/resume.php similarity index 100% rename from htdocs/api/playlist/resume.php rename to src/ui/api/playlist/resume.php diff --git a/htdocs/api/playlist/shuffle.php b/src/ui/api/playlist/shuffle.php similarity index 100% rename from htdocs/api/playlist/shuffle.php rename to src/ui/api/playlist/shuffle.php diff --git a/htdocs/api/playlist/single.php b/src/ui/api/playlist/single.php similarity index 100% rename from htdocs/api/playlist/single.php rename to src/ui/api/playlist/single.php diff --git a/htdocs/api/playlist/song.php b/src/ui/api/playlist/song.php similarity index 100% rename from htdocs/api/playlist/song.php rename to src/ui/api/playlist/song.php diff --git a/htdocs/api/volume.php b/src/ui/api/volume.php similarity index 100% rename from htdocs/api/volume.php rename to src/ui/api/volume.php diff --git a/htdocs/cardEdit.php b/src/ui/cardEdit.php similarity index 100% rename from htdocs/cardEdit.php rename to src/ui/cardEdit.php diff --git a/htdocs/cardRegisterNew.php b/src/ui/cardRegisterNew.php similarity index 100% rename from htdocs/cardRegisterNew.php rename to src/ui/cardRegisterNew.php diff --git a/src/ui/config.php b/src/ui/config.php new file mode 100644 index 000000000..595fa05cf --- /dev/null +++ b/src/ui/config.php @@ -0,0 +1,8 @@ + "", generally: end with trailing slash +$conf['base_path'] = "/home/arne/ablage/tec/audiobert/RPi-Jukebox-RFID"; // absolute path to folder +$conf['local_url'] = $_SERVER['SERVER_NAME']; // put the fixed IP or Local name here + +?> \ No newline at end of file diff --git a/htdocs/config.php.sample b/src/ui/config.php.sample similarity index 100% rename from htdocs/config.php.sample rename to src/ui/config.php.sample diff --git a/htdocs/func.php b/src/ui/func.php similarity index 100% rename from htdocs/func.php rename to src/ui/func.php diff --git a/htdocs/inc.addSystemInfo.php b/src/ui/inc.addSystemInfo.php similarity index 100% rename from htdocs/inc.addSystemInfo.php rename to src/ui/inc.addSystemInfo.php diff --git a/htdocs/inc.controlPlayer.php b/src/ui/inc.controlPlayer.php similarity index 100% rename from htdocs/inc.controlPlayer.php rename to src/ui/inc.controlPlayer.php diff --git a/htdocs/inc.controlVolumeUpDown.php b/src/ui/inc.controlVolumeUpDown.php similarity index 100% rename from htdocs/inc.controlVolumeUpDown.php rename to src/ui/inc.controlVolumeUpDown.php diff --git a/htdocs/inc.debug.php b/src/ui/inc.debug.php similarity index 100% rename from htdocs/inc.debug.php rename to src/ui/inc.debug.php diff --git a/htdocs/inc.formCardEdit.php b/src/ui/inc.formCardEdit.php similarity index 100% rename from htdocs/inc.formCardEdit.php rename to src/ui/inc.formCardEdit.php diff --git a/htdocs/inc.header.php b/src/ui/inc.header.php similarity index 94% rename from htdocs/inc.header.php rename to src/ui/inc.header.php index feb542f90..106c39acc 100755 --- a/htdocs/inc.header.php +++ b/src/ui/inc.header.php @@ -24,9 +24,9 @@ // no config nor sample config found. die. print "

Configuration file not found

The files 'config.php' and 'config.php.sample' were not found in the - directory 'htdocs'. Please download 'htdocs/config.php.sample' from the + directory 'ui'. Please download 'ui/config.php.sample' from the online repository, - copy it locally to 'htdocs/config.php' and then adjust it to fit your system.

"; + copy it locally to 'ui/config.php' and then adjust it to fit your system.

"; die; } else { // no config but sample config found: make copy (and give warning) @@ -34,24 +34,24 @@ // sample config can not be copied. die. print "

Configuration file could not be created

The file 'config.php' was not found in the - directory 'htdocs'. Attempting to create this file from 'config.php.sample' + directory 'ui'. Attempting to create this file from 'config.php.sample' resulted in an error.

Are the folder settings correct? You could try to run the following commands inside the folder 'RPi-Jukebox-RFID' and then reload the page:

-sudo chmod -R 775 htdocs/
-sudo chgrp -R www-data htdocs/
+sudo chmod -R 775 ui/
+sudo chgrp -R www-data ui/
                 

- Alternatively, download 'htdocs/config.php.sample' from the + Alternatively, download 'ui/config.php.sample' from the online repository, - copy it locally to 'htdocs/config.php' and then adjust it to fit your system.

"; + copy it locally to 'ui/config.php' and then adjust it to fit your system.

"; die; } else { $warning = "

Configuration file created

The file 'config.php' was not found in the - directory 'htdocs'. A copy of the sample file 'config.php.sample' was made automatically. + directory 'ui'. A copy of the sample file 'config.php.sample' was made automatically. If you encounter any errors, edit the newly created 'config.php'.

"; @@ -80,16 +80,16 @@ ); $debugOptions = array("TRUE", "FALSE"); -if(!file_exists("../settings/debugLogging.conf")) { +if(!file_exists("../../settings/debugLogging.conf")) { // create file $debugLoggingConf = ""; foreach($debugAvail as $debugItem) { $debugLoggingConf .= $debugItem."=\"FALSE\"\n"; } - file_put_contents("../settings/debugLogging.conf", $debugLoggingConf); + file_put_contents("../../settings/debugLogging.conf", $debugLoggingConf); } // read file -$debugLoggingConf = parse_ini_file("../settings/debugLogging.conf"); +$debugLoggingConf = parse_ini_file("../../settings/debugLogging.conf"); /* * DEBUGGING * for debugging, set following var to true. @@ -132,21 +132,22 @@ function fileGetContentOrDefault($filename, $defaultValue) include("func.php"); // path to script folder from github repo on RPi -$conf['scripts_abs'] = realpath(getcwd().'/../scripts/'); +$conf['scripts_abs'] = realpath(getcwd().'/../../scripts/'); // path to shared folder from github repo on RPi -$conf['shared_abs'] = realpath(getcwd().'/../shared/'); +$conf['shared_abs'] = realpath(getcwd().'/../../shared/'); // path to settings folder from github repo on RPi -$conf['settings_abs'] = realpath(getcwd().'/../settings/'); +$conf['settings_abs'] = realpath(getcwd().'/../../settings/'); /* * Vars from the settings folder */ +/* if(!file_exists($conf['settings_abs']."/global.conf")) { // execute shell to create config file // scripts/inc.writeGlobalConfig.sh exec("sudo ".$conf['scripts_abs']."/inc.writeGlobalConfig.sh"); exec("sudo chmod 777 ".$conf['settings_abs']."/global.conf"); -} +}*/ // read the global conf file $globalConf = parse_ini_file($conf['settings_abs']."/global.conf", $process_sections = null); diff --git a/htdocs/inc.langLoad.php b/src/ui/inc.langLoad.php similarity index 100% rename from htdocs/inc.langLoad.php rename to src/ui/inc.langLoad.php diff --git a/htdocs/inc.loadControls.php b/src/ui/inc.loadControls.php similarity index 100% rename from htdocs/inc.loadControls.php rename to src/ui/inc.loadControls.php diff --git a/htdocs/inc.loadCover.php b/src/ui/inc.loadCover.php similarity index 100% rename from htdocs/inc.loadCover.php rename to src/ui/inc.loadCover.php diff --git a/htdocs/inc.loadedPlaylist.php b/src/ui/inc.loadedPlaylist.php similarity index 100% rename from htdocs/inc.loadedPlaylist.php rename to src/ui/inc.loadedPlaylist.php diff --git a/htdocs/inc.navigation.php b/src/ui/inc.navigation.php similarity index 100% rename from htdocs/inc.navigation.php rename to src/ui/inc.navigation.php diff --git a/htdocs/inc.playerStatus.php b/src/ui/inc.playerStatus.php similarity index 100% rename from htdocs/inc.playerStatus.php rename to src/ui/inc.playerStatus.php diff --git a/htdocs/inc.processCheckCardEditRegister.php b/src/ui/inc.processCheckCardEditRegister.php similarity index 100% rename from htdocs/inc.processCheckCardEditRegister.php rename to src/ui/inc.processCheckCardEditRegister.php diff --git a/htdocs/inc.setDebugLogConf.php b/src/ui/inc.setDebugLogConf.php similarity index 98% rename from htdocs/inc.setDebugLogConf.php rename to src/ui/inc.setDebugLogConf.php index 40e6f3d5d..11be8aa81 100755 --- a/htdocs/inc.setDebugLogConf.php +++ b/src/ui/inc.setDebugLogConf.php @@ -48,7 +48,7 @@ print '
- +
",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; -}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("