initialize

This commit is contained in:
Lurkars 2022-02-06 10:19:03 +01:00
commit 75749efa02
61 changed files with 5121 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.vscode
.local
.cache
library/*
config/*
bin/*

244
README.md Normal file
View File

@ -0,0 +1,244 @@
# luniebox
lunibox is a RFID jukebox based on a Raspberry Pi. It is similar to the [Phoniebox](https://www.phoniebox.de) \[[https://github.com/MiczFlor/RPi-Jukebox-RFID](GitHub)\] and an upgrade of the [TonUINO (de)](https://www.voss.earth/tonuino/) which both are DIY versions of the popular Toniebox©. The main focus for now is to play Spotify© content, playing local files or other sources will be integrated later.
## Prerequisites
You need to be familiar with ssh (Putty on Windows). For the usage of Spotify© a premium account is required.
### Hardware
- Raspberry Pi (tested with Raspberry Pi 3 Model B, Zero W, Zero 2 W) \[starting at ~14€\]
- power supply (5V ~2.5A) \[starting at ~6€\]
- Micro SD card (at least 4GB) \[starting at ~4€\]
- RC522 RFID Reader connected to [TODO] \[starting at ~1.50€\]
- RFID (MiFare) cards or chips \[starting at ~2€\]
- a Audio Card:
- Pimoroni Audio Amp SHIM (3W Mono Amp) \[starting at ~11€\] with passive speaker \[starting at ~5€\]
- Pimoroni Audio DAC SHIM \[starting at ~14€\] with active speaker
- Adafruit Speaker Bonnet for Raspberry Pi
- ...something else (use custom setup!)
- (optional) MPU9250 9-axis sensor [TODO] \[starting at ~1.50€\]
- (optional) Waveshare UPS HAT + 2x 18650 18650 Li battery \[starting at ~30€\]
- some wires or dupont connectors \[starting at ~2€\]
- case for all above
- depending on hardware: soldering equipment
A minimal setup (Raspberry Zero 2 W, power supply, Micro SD, RC522, Cards, Audio Amp) should be about ~40€ plus case materials.
### Software
- latest Raspberry Pi OS blank installation on Micro SD card (Instructions: [raspberrypi.com/software/](https://www.raspberrypi.com/software/))
- WiFi connection
- ssh enabled
### Optional: Headless installation
If you're using a Raspberry Pi Zero or have missing peripherals to setup WiFi and ssh please perform the following steps:
- insert Micro SD card with Raspberry Pi OS into a computer
- place an empty file called `ssh` into `/boot` folder/partioon
- place a filed called `wpa_supplicant.conf` into `/boot` folder/partition with following content
```
country=$COUNTRY_CODE
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="$WIFI_SSID"
psk="$WIFI_PASSWORD"
}
```
- replace `$COUNTRY_CODE` with upper-case country code (eg. GB or DE) and `$WIFI_SSID` and `$WIFI_PASSWORD` with your WiFi credentials
- plug card back into your Pi and connect power supply
## Setup
Establish a ssh connection to your Pi:
- the Pi should be reachable under `raspberrypi` or `raspberrypi.local` in your network, if not try to find out it's IP address from your router.
- default username is `pi` and default password is `raspberry`
- Example: `ssh pi@raspberrypi.local` or `ssh pi@192.168.2.100`
> ⚠️ Warning: you should change the default passwort by executing `passwd` after login
### Automatic setup
> ⚠️ Warning: executing scripts from the internet without checking is bad. This is only done to get things done fast. In any doubts you can perform the [Manual setup](#manual-setup) and execute all commands step-by-setop to understand what's going on.
- download and excecute setup script (you will be prompted to confirm certain steps beforehand anyway)
> `curl https://git.bstly.de/Lurkars/luniebox/raw/branch/main/luniebox.sh -o luniebox.sh`
>
> `chmod +x luniebox.sh`
>
> `./luniebox.sh`
### Manual setup
#### Software setup
go to home directory
> `cd /home/pi`
install `git`, `python3-venv` and `python3-pip`
> `sudo apt install -y git python3-venv python3-pip`
clone repository `https://git.bstly.de/Lurkars/luniebox.git` with sources and config:
> `git clone https://git.bstly.de/Lurkars/luniebox.git luniebox`
setup application
> `cd /home/pi/luniebox/application`
>
> `python -m venv venv`
>
> `source venv/bin/activate`
>
> `export CFLAGS=-fcommon`
>
> `pip install -r requirements.txt`
>
> `deactivate`
>
> `mkdir /home/pi/luniebox/config`
>
> `cp /home/pi/luniebox/contrib/config/luniebox.cfg /home/pi/luniebox/config/luniebox.cfg`
>
> `sudo cp /home/pi/luniebox/contrib/luniebox-app.service /etc/systemd/system/`
>
> `sudo cp /home/pi/luniebox/contrib/luniebox-daemon.service /etc/systemd/system/`
>
> `sudo systemctl daemon-reload`
>
> `sudo systemctl enable luniebox-app luniebox-daemon`
setup spotifyd
> `mkdir /home/pi/luniebox/bin`
>
> `wget -c https://github.com/Spotifyd/spotifyd/releases/download/v0.3.3/spotifyd-linux-armv6-slim.tar.gz -O - | tar -xz -C /home/pi/luniebox/bin`
>
> `cp /home/pi/luniebox/contrib/config/spotifyd.cfg /home/pi/luniebox/config/spotifyd.cfg` (if you use other audio hardware, you may need to adjust the `backend` and `device` properties to your needs!)
>
> `sudo cp /home/pi/luniebox/contrib/spotifyd.service /etc/systemd/system/`
>
> `sudo systemctl daemon-reload`
>
> `sudo systemctl enable spotifyd`
setup mpd
> `mkdir /home/pi/luniebox/library`
>
> `sudo apt install -y mpd`
>
> `sudo cp /home/pi/luniebox/contrib/config/mpd.conf /etc/mpd.conf` (if you use other audio hardware, you may need to adjust the `audio_output` section to your needs!)
>
start/restart all services
> `sudo systemctl restart mpd spotifyd luniebox-daemon luniebox-app`
setup ClSpotify
> `git clone https://github.com/agent255/clspotify.git /home/pi/clspotify`
>
> `cd /home/pi/clspotify`
>
> `python -m venv venv`
>
> `source venv/bin/activate`
>
> `pip install -r requirements.txt`
>
> `deactivate`
>
> `sed -i -i 's/^zspotify_path =.*$/zspotify_path = \/home\/pi\/clspotify\//' /home/pi/luniebox/config/luniebox.cfg`
#### Hardware Setup
##### enable SPI for RFID Reader
uncomment `dtparam=spi=on` in `/boot/config.txt`
> `sudo sed -i '/dtparam=spi=on/s/^#//g' /boot/config.txt`
##### enable I2C for MPU9250 9-axis sensor
install `i2c-tools` and `python3-smbus`
> `sudo apt install -y i2c-tools python3-smbus`
uncomment `dtparam=i2c_arm=on` in `/boot/config.txt`
> `sudo sed -i '/dtparam=i2c_arm=on/s/^#//g' /boot/config.txt`
add `dtoverlay=i2c-gpio,bus=4,i2c_gpio_delay_us=1,i2c_gpio_sda=23,i2c_gpio_scl=24` to `/boot/config.txt`
> `printf "dtoverlay=i2c-gpio,bus=4,i2c_gpio_delay_us=1,i2c_gpio_sda=23,i2c_gpio_scl=24" | sudo tee -a /boot/config.txt`
#### Setup Audio
##### for Pimoroni Amp or DAC
disable onboard audio comment out `dtparam=audio=on` in `/boot/config.txt`
> `sudo sed -i '/dtparam=audio=on/s/^/#/g' /boot/config.txt`
setup hifiberry-dac by adding
```
dtoverlay=hifiberry-dac
gpio=25=op,dh
```
to `/boot/config.txt`
> `printf "dtoverlay=hifiberry-dac\ngpio=25=op,dh" | sudo tee -a /boot/config.txt`
##### for Adafruit Speaker Bonnet for Raspberry Pi
disable onboard audio comment out `dtparam=audio=on` in `/boot/config.txt`
> `sudo sed -i '/dtparam=audio=on/s/^/#/g' /boot/config.txt`
setup hifiberry-dac and i2s by adding
```
dtoverlay=hifiberry-dac
dtoverlay=i2s-mmap
```
to `/boot/config.txt`
> `printf "dtoverlay=hifiberry-dac\ndtoverlay=i2s-mmap" | sudo tee -a /boot/config.txt`
create `/etc/asound.conf` file with following content:
```
pcm.speakerbonnet {
type hw card 0
}
pcm.dmixer {
type dmix
ipc_key 1024
ipc_perm 0666
slave {
pcm "speakerbonnet"
period_time 0
period_size 1024
buffer_size 8192
rate 44100
channels 2
}
}
ctl.dmixer {
type hw card 0
}
pcm.softvol {
type softvol
slave.pcm "dmixer"
control.name "PCM"
control.card 0
}
ctl.softvol {
type hw card 0
}
pcm.!default {
type plug
slave.pcm "softvol"
}
```
After setup, reboot system.
> `sudo reboot`
## Planned features
- status LEDs
- indicators for UPS HAT
- WiFi Hotspot (https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection)

3
application/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin
venv
__pycache__

View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__name__ = "ExtendedMFRC522"
from mfrc522 import SimpleMFRC522
class ExtendedMFRC522(SimpleMFRC522):
def __init__(self, start_section=1, sections=0, blocks_per_section=4, block_size=16, encoding='utf8'):
self.START_SECTION = start_section
self.SECTIONS = sections
self.BLOCKS_PER_SECTIONS = blocks_per_section
self.DATA_BLOCKS = self.BLOCKS_PER_SECTIONS - 1
self.BLOCK_SIZE = block_size
self.ENCODING = encoding
if self.START_SECTION < 1:
self.START_SECTION = 1
super().__init__()
def read_no_block(self):
(status, size) = self.READER.MFRC522_Request(self.READER.PICC_REQIDL)
if status != self.READER.MI_OK:
return None, None
(status, uid) = self.READER.MFRC522_Anticoll()
if status != self.READER.MI_OK:
return None, None
id = self.uid_to_num(uid)
self.READER.MFRC522_SelectTag(uid)
data = bytearray()
start_section = self.START_SECTION
if start_section > size:
start_section = size - 1
sections = size - start_section
if self.SECTIONS > 0 and self.SECTIONS <= sections:
sections = self.SECTIONS
for section in range(start_section, start_section + sections):
trailer_block = section * self.BLOCKS_PER_SECTIONS + self.DATA_BLOCKS
status = self.READER.MFRC522_Auth(
self.READER.PICC_AUTHENT1A, trailer_block, self.KEY, uid)
if status == self.READER.MI_OK:
for i in range(self.DATA_BLOCKS):
block_addr = section * self.BLOCKS_PER_SECTIONS + i
block = self.READER.MFRC522_Read(block_addr)
if block:
data.extend(bytearray(block))
else:
return None, None
text_read = ''
if data:
while data and data[0] == 0:
data.pop(0)
while data and data[-1] == 0:
data.pop()
if data:
text_read = data.decode(self.ENCODING)
self.READER.MFRC522_StopCrypto1()
return id, text_read
def write_no_block(self, text):
(status, size) = self.READER.MFRC522_Request(self.READER.PICC_REQIDL)
if status != self.READER.MI_OK:
return None, None
(status, uid) = self.READER.MFRC522_Anticoll()
if status != self.READER.MI_OK:
return None, None
id = self.uid_to_num(uid)
self.READER.MFRC522_SelectTag(uid)
start_section = self.START_SECTION
if start_section > size:
start_section = size - 1
sections = size - start_section
if self.SECTIONS > 0 and self.SECTIONS <= sections:
sections = self.SECTIONS
data = text.strip().encode(self.ENCODING)
data_sections = [data[i:i + self.BLOCK_SIZE * self.DATA_BLOCKS]
for i in range(0, len(data), self.BLOCK_SIZE * self.DATA_BLOCKS)]
for section in range(start_section, start_section + sections):
trailer_block = section * self.BLOCKS_PER_SECTIONS + self.DATA_BLOCKS
status = self.READER.MFRC522_Auth(
self.READER.PICC_AUTHENT1A, trailer_block, self.KEY, uid)
self.READER.MFRC522_Read(trailer_block)
section_data = bytearray(self.DATA_BLOCKS * self.BLOCK_SIZE)
if len(data_sections) > (section - start_section):
section_data = bytearray(
data_sections[section - start_section])
section_data.extend(
bytearray(self.DATA_BLOCKS * self.BLOCK_SIZE - len(section_data)))
if status == self.READER.MI_OK:
for i in range(self.DATA_BLOCKS):
block_addr = section * self.BLOCKS_PER_SECTIONS + i
block_data = section_data[(
i*self.BLOCK_SIZE):(i+1)*self.BLOCK_SIZE]
self.READER.MFRC522_Write(
block_addr, block_data)
self.READER.MFRC522_StopCrypto1()
return id, data[0:(len(self.BLOCK_ADDRS) * self.BLOCK_SIZE * sections)].decode(self.ENCODING)

176
application/api.py Normal file
View File

@ -0,0 +1,176 @@
from functools import wraps
from flask import Blueprint, request, abort, redirect, url_for, jsonify
from urllib.parse import urlencode
import subprocess
import logging
from luniebox import luniebox
from spotifydl import SpotifyDLStatus
import util
api = Blueprint('api', __name__)
def api_key_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not request.headers.has_key('Authorization') or request.headers.get('Authorization') != luniebox.get_setting('API', 'API_KEY'):
abort(401)
return f(*args, **kwargs)
return decorated_function
@api.route("/")
@api_key_required
def index():
return jsonify("test")
@api.route("/setup", methods=['POST'])
def setup():
spotify_username = request.form.get('spotify-username')
spotify_password = request.form.get('spotify-password')
spotify_client_id = request.form.get('spotify-client-id')
spotify_client_secret = request.form.get('spotify-client-secret')
spotify_redirect_uri = request.form.get('spotify-redirect-uri')
spotify_device_name = request.form.get('spotify-device-name')
luniebox.set_setting('luniebox', 'setup', True)
luniebox.set_setting('spotify', 'client_id', spotify_client_id)
luniebox.set_setting('spotify', 'client_secret', spotify_client_secret)
luniebox.set_setting('spotify', 'redirect_uri', spotify_redirect_uri)
luniebox.spotify.set_setting('username', spotify_username)
luniebox.spotify.set_setting('password', spotify_password)
luniebox.spotify.set_setting('device_name', spotify_device_name)
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
return redirect(url_for('api.spotify_authorize'))
spotify_authorize_state = False
@api.route("/spotify/authorize", methods=['GET'])
def spotify_authorize():
client_id = luniebox.get_setting('spotify', 'client_id')
redirect_uri = luniebox.get_setting('spotify', 'redirect_uri')
global spotify_authorize_state
spotify_authorize_state = util.randomString(16)
scope = 'user-read-playback-state user-modify-playback-state user-read-private'
return redirect('https://accounts.spotify.com/authorize?' +
urlencode({
'response_type': 'code',
'client_id': client_id,
'scope': scope,
'redirect_uri': redirect_uri,
'state': spotify_authorize_state
}))
@api.route("/spotify/callback", methods=['GET'])
def spotify_callback():
code = request.args['code']
returnedState = request.args['state']
global spotify_authorize_state
if not returnedState or returnedState != spotify_authorize_state:
abort(403)
luniebox.spotify.new_access_token(code)
return redirect(url_for('pages.index'))
@api.route("/play", methods=['GET'])
def play():
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
p.communicate()
uri = request.args['uri']
luniebox.play(uri)
return redirect(url_for('pages.success', play=uri))
@api.route("/spotify/dl", methods=['GET'])
def spotify_dl():
if luniebox.spotifydl_connect():
uri = request.args['uri']
dlStatus = luniebox.spotifydl.download(uri)
return redirect(url_for('pages.spotifydl', status=dlStatus, uri=uri))
else:
return redirect(url_for('pages.spotifydl', status=SpotifyDLStatus.DISABLED))
@api.route("/spotify/downloads", methods=['GET'])
def spotify_downloads():
if luniebox.spotifydl_connect():
return jsonify(luniebox.spotifydl.getDownloads())
else:
return redirect(url_for('pages.error', error='spotifydownloads'))
@api.route("/rfid/write", methods=['GET'])
def rfid_write():
value = request.args['value']
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
p.communicate()
luniebox.rfid_write(value)
logging.getLogger('luniebox').info("Write to RFID: " + value)
return redirect(url_for('pages.success', rfid_write=value))
@api.route("/rfid/read", methods=['GET'])
def rfid_read():
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
p.communicate()
value = luniebox.rfid_readOnce()
return redirect(url_for('pages.success', rfid_read=value))
@api.route("/rfid/play", methods=['GET'])
def rfid_play():
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
p.communicate()
value = luniebox.rfid_readOnce()
if luniebox.play(value):
return redirect(url_for('pages.success', rfid_play=value))
else:
return redirect(url_for('pages.error', error='play'))
@api.route("/spotifyd/restart", methods=['GET'])
def restart_spotifyd():
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
return redirect(url_for('pages.success', restart_spotifyd=True))
@api.route("/mpd/restart", methods=['GET'])
def restart_mpd():
subprocess.run(["sudo", "systemctl", "restart", "mpd"])
return redirect(url_for('pages.success', restart_mpd=True))
@api.route("/mpd/list", methods=['GET'])
def mpd_list():
if 'path' in request.args:
results = luniebox.mpd_list(request.args['path'])
else:
results = luniebox.mpd_list()
return jsonify(results)
@api.route("/daemon/start", methods=['GET'])
def daemon_start():
subprocess.run(["sudo", "systemctl", "start", "luniebox-daemon"])
return redirect(url_for('pages.success', start_daemon=True))
@api.route("/daemon/stop", methods=['GET'])
def daemon_stop():
subprocess.run(["sudo", "systemctl", "stop", "luniebox-daemon"])
return redirect(url_for('pages.success', stop_daemon=True))
@api.route("/restart", methods=['GET'])
def restart_app():
subprocess.run(["sudo", "systemctl", "restart", "luniebox-app"])
return redirect(url_for('pages.success', restart_luniebox=True))

31
application/app.py Normal file
View File

@ -0,0 +1,31 @@
import sys
from flask import Flask
import logging
from luniebox import luniebox
from api import api
from pages import pages
app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api')
app.register_blueprint(pages)
loglevel = 'INFO'
if luniebox.get_setting('logging', 'level'):
loglevel = luniebox.get_setting('logging', 'level')
logger = logging.getLogger('luniebox')
logger.setLevel(logging._nameToLevel[loglevel])
logFormatter = logging.Formatter(
style='{', datefmt='%Y-%m-%d %H:%M:%S', fmt='{asctime} {levelname}: {message}')
logstdoutHandler = logging.StreamHandler(sys.stdout)
logstdoutHandler.setFormatter(logFormatter)
logger.addHandler(logstdoutHandler)
if __name__ != '__main__':
gunicorn_logger = logging.getLogger('gunicorn.error')
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)
if __name__ == "__main__":
app.run(host='0.0.0.0')

107
application/daemon.py Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import signal
import time
import sys
import logging
from luniebox import luniebox
import RPi.GPIO as GPIO
from mpu9250_jmdev.registers import *
from mpu9250_jmdev.mpu_9250 import MPU9250
class LunieboxDaemon(object):
def __init__(self, luniebox, input1=7, input2=8, skip_thresh=0.5, wind_thresh=0.3):
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
self.luniebox = luniebox
self.input1 = input1
self.input2 = input2
self.skip_thresh = skip_thresh
self.wind_thresh = wind_thresh
GPIO.setup(self.input1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(self.input2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
self.tolerance = self.luniebox.get_setting(
'rfid', 'pause_tolerance', 4)
self.reads = 0
if self.luniebox.get_setting('hardware', 'mpu') == 'True':
self.mpu = MPU9250(
address_ak=AK8963_ADDRESS,
address_mpu_master=MPU9050_ADDRESS_68,
address_mpu_slave=None,
bus=4,
gfs=GFS_1000,
afs=AFS_8G,
mfs=AK8963_BIT_16,
mode=AK8963_MODE_C100HZ)
self.mpu.configure()
else:
self.mpu = False
def run(self):
logging.getLogger('luniebox').info("run luniebox")
while True:
# mpu
if self.mpu:
acc = self.mpu.readAccelerometerMaster()
rot_x = acc[0]
rot_y = acc[1]
if rot_x > self.skip_thresh:
self.luniebox.previous()
time.sleep(1)
elif rot_x < (self.skip_thresh * -1):
self.luniebox.next()
time.sleep(1)
if rot_y < (self.wind_thresh * -1):
self.luniebox.fastforward()
time.sleep(0.1)
elif rot_y > self.wind_thresh:
self.luniebox.rewind()
time.sleep(0.1)
# buttons
down_state = GPIO.input(self.input1)
if down_state == False:
self.luniebox.vol_down()
time.sleep(0.1)
up_state = GPIO.input(self.input2)
if up_state == False:
self.luniebox.vol_up()
time.sleep(0.1)
# rfid
id, text = self.luniebox.reader.read_no_block()
if text != None:
text = text.strip()
if text == None:
self.reads += 1
if self.reads >= self.tolerance:
self.reads = 0
self.luniebox.pause()
time.sleep(0.1)
else:
self.reads = 0
self.luniebox.play(text)
time.sleep(0.1)
def signal_handler(self, signal, frame):
logging.getLogger('luniebox').info(
"Caught signal {}, exiting...".format(signal))
luniebox.stop()
sys.exit(0)
if __name__ == "__main__":
daemon = LunieboxDaemon(luniebox=luniebox)
signal.signal(signal.SIGINT, daemon.signal_handler)
signal.signal(signal.SIGTERM, daemon.signal_handler)
daemon.run()

378
application/luniebox.py Normal file
View File

@ -0,0 +1,378 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__name__ = "Luniebox"
import sys
import logging
import subprocess
import time
from enum import Enum
from configparser import ConfigParser
import RPi.GPIO as GPIO
from spotifydl import SpotifyDL, SpotifyDLStatus
from mpd import MPDClient
from ExtendedMFRC522 import ExtendedMFRC522
from spotify import Spotify
import util
defaultConfigFilePath = '../config/luniebox.cfg'
class PlayerService(Enum):
NONE = 0
SPOTIFY = 1
MPD = 2
class Luniebox(object):
def __init__(self, configFilePath=defaultConfigFilePath):
self.configFilePath = configFilePath
self.read_config()
GPIO.setwarnings(False)
self.reader = ExtendedMFRC522(encoding="ascii")
loglevel = 'INFO'
if self.get_setting('logging', 'level'):
loglevel = self.get_setting('logging', 'level')
logger = logging.getLogger('luniebox')
logger.setLevel(logging._nameToLevel[loglevel])
logFormatter = logging.Formatter(
style='{', datefmt='%Y-%m-%d %H:%M:%S', fmt='{asctime} {levelname}: {message}')
logstdoutHandler = logging.StreamHandler(sys.stdout)
logstdoutHandler.setFormatter(logFormatter)
logger.addHandler(logstdoutHandler)
if self.get_setting('mpd', 'disabled', '') != 'True':
self.mpd = MPDClient()
self.mpd.host = "localhost"
self.mpd.port = 6600
self.mpd.timeout = 10
if self.mpd_connect():
logging.getLogger('luniebox').debug("connected to mpd")
else:
logging.getLogger('luniebox').info("mpd disabled")
self.mpd = False
if self.get_setting('spotify', 'disabled', '') != 'True':
self.spotify = Spotify(luniebox=self)
if self.spotify_connect():
logging.getLogger('luniebox').debug("connected to spotify")
self.zspotify_path = False
self.spotifydl = False
self.zspotify_path = self.get_setting('spotify', 'zspotify_path')
if self.zspotify_path and self.mpd:
self.spotifydl_connect()
else:
logging.getLogger('luniebox').info("spotify disabled")
self.spotify = False
if not self.config.has_option('api', 'api_key'):
self.set_setting('api', 'api_key', util.randomString(64))
self.current = self.get_setting('luniebox', 'current')
self.service = PlayerService.NONE
if self.current != None:
if self.current.startswith("spotify:"):
self.service = PlayerService.SPOTIFY
elif self.current.startswith("mpd:"):
self.service = PlayerService.MPD
self.volume_max = int(self.get_setting('luniebox', 'volume_max', 100))
self.volume = int(self.get_setting(
'luniebox', 'volume', self.volume_max))
self.volume_step = int(self.get_setting('luniebox', 'volume_step', 5))
self.wind_step = int(self.get_setting('luniebox', 'wind_step', 10))
self.resume = True
def mpd_connect(self):
if not self.get_setting('luniebox', 'setup'):
return False
if not self.mpd:
logging.getLogger('luniebox').warn("mpd disabled!")
return False
try:
self.mpd.disconnect()
except:
pass
tries = 0
while tries < 5:
try:
self.mpd.connect(self.mpd.host, self.mpd.port)
return True
except:
subprocess.call(["systemctl", "restart", "mpd"])
time.sleep(1)
tries += 1
logging.getLogger('luniebox').error(
"Could not connect to MPD service!")
return False
def spotify_connect(self, restart=False, max_tries=5):
if not self.get_setting('luniebox', 'setup'):
return False
if not self.spotify:
logging.getLogger('luniebox').warn("spotify disabled!")
return False
if restart:
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
tries = 0
spotifyd_status = subprocess.call(
["systemctl", "is-active", "--quiet", "spotifyd"])
while spotifyd_status != 0 and tries < max_tries:
subprocess.call(["systemctl", "restart", "spotifyd"])
time.sleep(1)
tries += 1
spotifyd_status = subprocess.call(
["systemctl", "is-active", "--quiet", "spotifyd"])
if spotifyd_status == 0:
return True
logging.getLogger('luniebox').error("spotifyd service not running!")
return False
def spotifydl_connect(self):
if not self.get_setting('luniebox', 'setup'):
return False
if self.spotifydl:
return True
if not self.zspotify_path or not self.mpd:
logging.getLogger('luniebox').warn("spotifydl disabled!")
return False
username = self.spotify.get_setting("username")
password = self.spotify.get_setting("password")
root = self.get_setting('mpd', 'library_path')
try:
self.spotifydl = SpotifyDL(
self.zspotify_path, username, password, root)
logging.getLogger('luniebox').info("spotifydl enabled!")
return True
except Exception as ex:
logging.getLogger('luniebox').warning(
"error on setup spotifydl: " + str(ex))
return False
def read_config(self):
configParser = ConfigParser()
dataset = configParser.read(self.configFilePath)
if len(dataset) != 1:
raise ValueError(
"Config file {} not found!".format(self.configFilePath))
self.config = configParser
def get_setting(self, section, key, default=None):
if self.config.has_option(section, key):
return self.config[section][key]
return default
def set_setting(self, section, key, value):
if not self.config.has_section(section):
self.config.add_section(section)
self.config.set(section, key, str(value))
with open(self.configFilePath, 'w') as configfile:
self.config.write(configfile)
def rfid_write(self, data):
self.reader.write(data.strip())
def rfid_readOnce(self):
id, text = self.reader.read()
logging.getLogger('luniebox').debug(
"Once ID: %s Text: %s" % (id, text))
return text.strip()
def vol_up(self):
self.change_volume(self.volume_step)
def vol_down(self):
self.change_volume(-1 * self.volume_step)
def change_volume(self, change):
self.volume += change
if self.volume > self.volume_max:
self.volume = self.volume_max
if self.volume < 0:
self.volume = 0
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
self.spotify.volume(self.volume)
elif self.service == PlayerService.MPD and self.mpd_connect():
self.mpd.setvol(self.volume)
self.set_setting('luniebox', 'volume', str(self.volume))
logging.getLogger('luniebox').debug("vol: " + str(self.volume))
def fastforward(self):
if not self.resume:
logging.getLogger('luniebox').debug('seek ff')
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
self.spotify.fastforward(self.wind_step)
elif self.service == PlayerService.MPD and self.mpd_connect():
self.mpd.seekcur(self.wind_step)
def rewind(self):
if not self.resume:
logging.getLogger('luniebox').debug('seek rew')
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
self.spotify.rewind(self.wind_step)
elif self.service == PlayerService.MPD and self.mpd_connect():
self.mpd.seekcur(-1 * self.wind_step)
def next(self):
if not self.resume:
logging.getLogger('luniebox').debug('next')
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
self.spotify.next()
elif self.service == PlayerService.MPD and self.mpd_connect():
self.mpd.next()
def previous(self):
if not self.resume:
logging.getLogger('luniebox').debug('prev')
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
self.spotify.previous()
elif self.service == PlayerService.MPD and self.mpd_connect():
self.mpd.previous()
def pause(self):
if not self.resume:
if self.mpd_connect():
self.mpd.pause(1)
logging.getLogger('luniebox').debug("pause mpd")
if self.spotify_connect(max_tries=2):
self.spotify.pause()
logging.getLogger('luniebox').debug("pause spotify")
self.resume = True
def play(self, text):
if text != "":
if text.startswith("spotify:"):
self.service = PlayerService.SPOTIFY
elif text.startswith("mpd:"):
self.service = PlayerService.MPD
if self.service == PlayerService.SPOTIFY and self.spotifydl_connect():
downloadStatus = self.spotifydl.downloadStatus(text)
if downloadStatus == SpotifyDLStatus.FINISHED:
self.mpd.update(text.replace('mpd:', ''))
self.service = PlayerService.MPD
elif self.get_setting('spotify', 'auto_download') == 'True' and downloadStatus == SpotifyDLStatus.NONE or downloadStatus == SpotifyDLStatus.ERROR:
self.spotifydl.download(text)
if self.service == PlayerService.SPOTIFY:
if text != self.current:
if self.spotify_connect():
self.spotify.volume(self.volume)
if self.spotify.play(text):
self.current = text
self.set_setting(
'luniebox', 'current', self.current)
self.resume = False
logging.getLogger('luniebox').debug(
"play spotify: " + self.current)
else:
logging.getLogger('luniebox').warn(
"cannot play spotify: " + self.current)
elif self.resume and text == self.current:
if self.spotify_connect():
self.spotify.volume(self.volume)
play = self.current
if self.spotify.is_active():
play = None
if self.spotify.play(play):
self.resume = False
logging.getLogger('luniebox').debug(
"resume spotify: " + self.current)
else:
logging.getLogger('luniebox').warn(
"cannot resume spotify: " + self.current)
elif self.service == PlayerService.MPD:
if text != self.current:
if self.mpd_connect():
self.mpd.setvol(self.volume)
self.mpd.clear()
text = text.replace('mpd:', '')
self.mpd.add(text)
self.mpd.play()
self.current = text
self.set_setting('luniebox', 'current', self.current)
self.resume = False
if text.startswith('spotify:'):
logging.getLogger('luniebox').debug(
"play spotify from mpd: " + text)
else:
logging.getLogger('luniebox').debug(
"play mpd: " + self.current)
elif self.resume and text == self.current:
if self.mpd_connect():
self.mpd.setvol(self.volume)
self.mpd.play()
self.resume = False
if text.startswith('spotify:'):
logging.getLogger('luniebox').debug(
"resume spotify from mpd: " + text)
else:
logging.getLogger('luniebox').debug(
"resume mpd: " + self.current)
elif text != None:
logging.getLogger('luniebox').info(
"invalid value(?): " + str(text))
def stop(self):
self.pause()
def sort_mpd(self, object):
if 'directory' in object:
return object['directory']
elif 'file' in object:
return object['file']
return ''
def mpd_list(self, path=''):
if self.mpd_connect():
try:
result = self.mpd.listfiles(path)
return sorted(result, key=self.sort_mpd)
except:
return []
return None
def mpd_status(self):
if self.mpd_connect():
status = self.mpd.status()
status['song'] = self.mpd.currentsong()
return status
return None
luniebox = Luniebox()

189
application/pages.py Normal file
View File

@ -0,0 +1,189 @@
from flask import Blueprint, render_template, redirect, url_for, request
from functools import wraps
import subprocess
from luniebox import luniebox
pages = Blueprint('pages', __name__)
def setup_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not luniebox.get_setting('luniebox', 'setup'):
return redirect(url_for('pages.setup'))
return f(*args, **kwargs)
return decorated_function
@pages.route("/setup")
def setup():
model = {}
if luniebox.get_setting('luniebox', 'setup'):
model['setup'] = True
model['client-id'] = luniebox.get_setting('spotify', 'client_id')
model['client-secret'] = luniebox.get_setting('spotify', 'client_secret')
model['redirect-uri'] = luniebox.get_setting('spotify', 'redirect_uri')
model['username'] = luniebox.spotify.get_setting('username', '')
model['password'] = luniebox.spotify.get_setting('password', '')
model['device-name'] = luniebox.spotify.get_setting('device_name', '')
model['host'] = request.host_url
if model['host'].endswith("/"):
model['host'] = model['host'][:len(model['host']) - 1]
if not model['redirect-uri']:
model['redirect-uri'] = model['host'] + url_for('api.spotify_callback')
return render_template('setup.html', model=model)
@pages.route("/")
@setup_required
def index():
devices = luniebox.spotify.devices()
device_id = luniebox.get_setting('spotify', 'device_id')
device_name = luniebox.spotify.get_setting('device_name')
mpdStatus = luniebox.mpd_status()
spotifyStatus = {
'status': False,
'device_name': device_name,
'device_id': device_id,
'errors': {'setup': True, 'not_running': True}
}
if not device_id:
spotifyStatus['errors']['not_running'] = False
for device in devices['devices']:
if device['name'] == device_name:
device_id = device['id']
luniebox.set_setting('spotify', 'device_id', device_id)
spotifyStatus['errors']['setup'] = False
else:
spotifyStatus['errors']['setup'] = False
for device in devices['devices']:
if device['id'] == device_id:
spotifyStatus['errors']['not_running'] = False
if spotifyStatus['errors']['not_running'] and luniebox.spotify.transfer_playback():
spotifyStatus['errors']['not_running'] = False
if not spotifyStatus['errors']['setup'] and not spotifyStatus['errors']['not_running']:
spotifyStatus['status'] = luniebox.spotify.playback_state()
daemon_error = False
daemon_status = subprocess.call(
["systemctl", "is-active", "--quiet", "luniebox-daemon"])
if daemon_status == 0:
daemon_status = "Running"
else:
daemon_error = "Not running"
daemon_status = False
return render_template('index.html', spotify=spotifyStatus, mpd=mpdStatus, daemon_status=daemon_status, daemon_error=daemon_error)
@pages.route("/success")
@setup_required
def success():
success = {}
if 'play' in request.args:
success['play'] = request.args['play']
if 'spotify_dl' in request.args:
success['spotify_dl'] = request.args['spotify_dl']
if 'restart_spotifyd' in request.args:
success['restart_spotifyd'] = request.args['restart_spotifyd']
if 'restart_mpd' in request.args:
success['restart_mpd'] = request.args['restart_mpd']
if 'start_daemon' in request.args:
success['start_daemon'] = request.args['start_daemon']
if 'stop_daemon' in request.args:
success['stop_daemon'] = request.args['stop_daemon']
if 'rfid_play' in request.args:
success['rfid_play'] = request.args['rfid_play']
if 'rfid_read' in request.args:
success['rfid_read'] = request.args['rfid_read']
if 'rfid_write' in request.args:
success['rfid_write'] = request.args['rfid_write']
return render_template('success.html', success=success)
@pages.route("/error")
@setup_required
def error():
error = False
if 'error' in request.args:
error = request.args['error']
return render_template('error.html', error=error)
@pages.route("/spotify/search", methods=['GET', 'POST'])
def spotify_search():
query = '' if not 'q' in request.args else request.args['q']
types = [
'track', 'album'] if not 'type' in request.args else request.args.getlist('type')
offset = 0 if not 'offset' in request.args else request.args['offset']
limit = 5
if not 'limit' in request.args:
if len(types) == 1:
limit = 20
if len(types) == 2:
limit = 10
else:
limit = int(request.args['limit'])
results = False if not query else luniebox.spotify.search(
query, types, limit, offset)
if results:
for result in results.values():
if 'items' in result:
for item in result['items']:
if luniebox.spotifydl_connect():
item['spotifydl'] = str(
luniebox.spotifydl.downloadStatus(item['uri']))
return render_template('spotify_search.html', results=results, query=query, types=types, limit=limit, offset=offset)
@pages.route("/spotify/download", methods=['GET'])
def spotifydl():
status = False
if 'status' in request.args:
status = request.args['status']
uri = False
if 'uri' in request.args:
uri = request.args['uri']
return render_template('spotifydl.html', status=status, uri=uri)
@pages.route("/mpd/list", methods=['GET'])
def mpd_list():
path = ''
spotify = False
if 'path' in request.args:
path = request.args['path']
if 'spotify' in request.args:
spotify = True
results = luniebox.mpd_list(path)
return render_template('mpd.html', results=results, path=path, spotify=spotify)

View File

@ -0,0 +1,9 @@
DateTime==4.3
Flask==2.0.2
gunicorn==20.1.0
mfrc522==0.0.7
mpu9250-jmdev==1.0.12
python-dateutil==2.8.2
python-mpd2==3.0.4
requests==2.26.0
smbus2==0.4.1

385
application/spotify.py Normal file
View File

@ -0,0 +1,385 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__name__ = "Spotify"
import base64
import datetime
from configparser import ConfigParser
from dateutil import parser
from urllib.parse import urlencode
import requests
import json
import subprocess
import logging
defaultSpotifyConfigFilePath = '../config/spotifyd.cfg'
class Spotify(object):
def __init__(self, luniebox, configFilePath=defaultSpotifyConfigFilePath):
self.luniebox = luniebox
self.configFilePath = configFilePath
self.read_config()
def read_config(self):
configParser = ConfigParser()
dataset = configParser.read(self.configFilePath)
if len(dataset) != 1:
raise ValueError(
"Config file {} not found!".format(self.configFilePath))
self.config = configParser
def get_setting(self, key, default=None):
if self.config.has_option('global', key):
return self.config['global'][key].strip('\"')
return default
def set_setting(self, key, value):
if not self.config.has_section('global'):
self.config.add_section('global')
self.config.set('global', key, str('\"' + value + '\"'))
with open(self.configFilePath, 'w') as configfile:
self.config.write(configfile)
def get_status(self):
devices = self.devices()
device_id = self.luniebox.get_setting('spotify', 'device_id')
device_name = self.get_setting('device_name')
status = {
'status': False,
'device_name': device_name,
'device_id': device_id,
'errors': {'setup': True, 'not_running': True}
}
if not device_id:
status['errors']['not_running'] = False
for device in devices['devices']:
if device['name'] == device_name:
device_id = device['id']
self.luniebox.get_setting(
'spotify', 'device_id', device_id)
status['errors']['setup'] = False
else:
status['errors']['setup'] = False
for device in devices['devices']:
if device['id'] == device_id:
status['errors']['not_running'] = False
if status['errors']['not_running'] and self.transfer_playback():
status['errors']['not_running'] = False
return status
def get_access_token(self):
access_token = self.luniebox.get_setting('spotify', 'access_token')
if not access_token:
return False
token_expires = parser.parse(
self.luniebox.get_setting('spotify', 'token_expires'))
if token_expires < datetime.datetime.now():
self.refresh_access_token()
return self.get_access_token()
return access_token
def new_access_token(self, code):
clientId = self.luniebox.get_setting('spotify', 'client_id')
clientSecret = self.luniebox.get_setting('spotify', 'client_secret')
redirectUri = self.luniebox.get_setting('spotify', 'redirect_uri')
credentials = clientId + ":" + clientSecret
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + str(base64.b64encode(credentials.encode('utf-8')), 'utf-8')
}
formData = {
'code': code,
'redirect_uri': redirectUri,
'grant_type': 'authorization_code'
}
response = requests.post(
'https://accounts.spotify.com/api/token', headers=headers, data=formData)
tokenData = response.json()
self.luniebox.set_setting(
'spotify', 'access_token', tokenData['access_token'])
self.luniebox.set_setting('spotify', 'refresh_token',
tokenData['refresh_token'])
self.luniebox.set_setting('spotify', 'token_expires', str(datetime.datetime.now(
) + datetime.timedelta(seconds=tokenData['expires_in'])))
def refresh_access_token(self):
clientId = self.luniebox.get_setting('spotify', 'client_id')
clientSecret = self.luniebox.get_setting('spotify', 'client_secret')
refresh_token = self.luniebox.get_setting('spotify', 'refresh_token')
if not refresh_token:
return False
credentials = clientId + ":" + clientSecret
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + str(base64.b64encode(credentials.encode('utf-8')), 'utf-8')
}
formData = {
'refresh_token': refresh_token,
'grant_type': 'refresh_token'
}
response = requests.post(
'https://accounts.spotify.com/api/token', headers=headers, data=formData)
tokenData = response.json()
self.luniebox.set_setting(
'spotify', 'access_token', tokenData['access_token'])
self.luniebox.set_setting('spotify', 'token_expires', str(datetime.datetime.now(
) + datetime.timedelta(seconds=tokenData['expires_in'])))
def api_call(self, url):
access_token = self.get_access_token()
headers = {
'Authorization': 'Bearer ' + access_token
}
return requests.get(
'https://api.spotify.com/v1' + url, headers=headers)
def search(self, query, types, limit, offset=0):
if limit > 50:
limit = 50
response = self.api_call(
'/search?' + urlencode({'q': query, 'type': ','.join(types), 'limit': limit, 'offset': offset}))
if response.status_code == 200:
return response.json()
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.search(query, types, limit, offset)
return False
def playback_state(self):
response = self.api_call('/me/player')
if response.status_code == 200:
return response.json()
if response.status_code == 204:
return {"not_active": True}
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.playback_state()
return False
def transfer_playback(self):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
data = {
'device_ids': [device_id],
'play': False
}
response = requests.put(
'https://api.spotify.com/v1/me/player', headers=headers, data=json.dumps(data))
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.transfer_playback()
logging.getLogger('luniebox').warn(
"error on spotify transfer playback", response, response.text)
return False
def is_active(self):
self.transfer_playback()
state = self.playback_state()
device_id = self.luniebox.get_setting('spotify', 'device_id')
if not state:
return False
return 'device' in state and 'id' in state['device'] and state['device']['id'] == device_id
def devices(self):
response = self.api_call('/me/player/devices')
return response.json()
def play(self, uri):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
data = {}
if uri:
if uri.startswith('spotify:track:'):
data = {
"uris": [uri]
}
else:
data = {
"context_uri": uri
}
response = requests.put(
'https://api.spotify.com/v1/me/player/play?' +
urlencode({'device_id': device_id}), headers=headers, data=json.dumps(data))
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.play(uri)
logging.getLogger('luniebox').warn(
"error on spotify play", response, response.text)
return False
def pause(self):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
response = requests.put(
'https://api.spotify.com/v1/me/player/pause?' +
urlencode({'device_id': device_id}), headers=headers)
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.pause()
logging.getLogger('luniebox').warn(
"error on spotify pause", response, response.text)
return False
def volume(self, value):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
if value < 0:
value = 0
if value > 100:
value = 100
response = requests.put(
'https://api.spotify.com/v1/me/player/volume?' +
urlencode({'device_id': device_id, 'volume_percent': value}), headers=headers)
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.volume(value)
logging.getLogger('luniebox').warn(
"error on spotify volume", response, response.text)
return False
def seek(self, position):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
if position < 0:
position = 0
response = requests.put(
'https://api.spotify.com/v1/me/player/seek?' +
urlencode({'device_id': device_id, 'position_ms': position}), headers=headers)
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.seek(position)
logging.getLogger('luniebox').warn(
"error on spotify seek", response, response.text)
return False
def rewind(self, seconds):
state = self.playback_state()
if state and 'progress_ms' in state:
progress = int(state['progress_ms'])
progress -= seconds * 1000
return self.seek(progress)
return False
def fastforward(self, seconds):
state = self.playback_state()
if state and 'progress_ms' in state:
progress = int(state['progress_ms'])
progress += seconds * 1000
return self.seek(progress)
return False
def previous(self):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
response = requests.post(
'https://api.spotify.com/v1/me/player/previous?' +
urlencode({'device_id': device_id}), headers=headers)
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.previous()
logging.getLogger('luniebox').warn(
"error on spotify previous", response, response.text)
return False
def next(self):
device_id = self.luniebox.get_setting('spotify', 'device_id')
access_token = self.get_access_token()
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + access_token
}
response = requests.post(
'https://api.spotify.com/v1/me/player/next?' +
urlencode({'device_id': device_id}), headers=headers)
if response.status_code == 204 or response.status_code == 202:
return True
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
return self.next()
logging.getLogger('luniebox').warn(
"error on spotify next", response, response.text)
return False

106
application/spotifydl.py Normal file
View File

@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__name__ = "SpotifyDL"
import subprocess
from configparser import ConfigParser
import os.path
import logging
from enum import Enum
defaultCredentialsLocation = '../config/zspotify.credentials'
class SpotifyDLStatus(Enum):
NONE = "none"
RUNNING = "running"
FINISHED = "finished"
ERROR = "error"
DISABLED = "disabled"
class SpotifyDL():
def __init__(self, zspotify_path, username, password, root, credentialsLocation=defaultCredentialsLocation):
if zspotify_path:
self.zspotify_path = zspotify_path
else:
raise ValueError("No zspotify path provivded!")
if credentialsLocation:
self.credentialsLocation = credentialsLocation
else:
raise ValueError("No credentialsLocation provivded!")
if not username:
raise ValueError("No username provided!")
if not password:
raise ValueError("No password provided!")
if root:
if not root.endswith("/"):
root = root + "/"
self.root = root
else:
raise ValueError("No root provided!")
if not os.path.isfile(self.credentialsLocation):
logging.getLogger('luniebox').info("initialize zspotify")
p = subprocess.Popen([self.zspotify_path + 'venv/bin/python', self.zspotify_path + 'zspotify/__main__.py', '-s',
'--credentials-location', self.credentialsLocation], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=False, group="pi", user="pi")
input = username + '\n' + password + '\n'
out, err = p.communicate(input=input.encode())
logging.getLogger('luniebox').info(out)
logging.getLogger('luniebox').warn(err)
self.downloads = {}
def download(self, uri):
status = self.downloadStatus(uri)
if status == SpotifyDLStatus.NONE or status == SpotifyDLStatus.ERROR:
logging.getLogger('luniebox').info(
"start download of '" + uri + "'")
trackprefix = ""
if uri.startswith('spotify:album:'):
trackprefix = "{album_num}. "
elif uri.startswith('spotify:playlist:'):
trackprefix = "{playlist_num}. "
p = subprocess.Popen([self.zspotify_path + 'venv/bin/python', self.zspotify_path + 'zspotify/__main__.py', "--root-path", self.root,
'--credentials-location', self.credentialsLocation, "--download-real-time", "True", "--chunk-size", "50000", "--skip-existing-files", "True", "--skip-previously-downloaded", "True", "--output", uri + "/" + trackprefix + "{artist} - {song_name}.{ext}", uri], stdin=None,
stdout=None, stderr=None, close_fds=True, shell=False, group="pi", user="pi")
self.downloads[uri] = p
return SpotifyDLStatus.RUNNING
elif status == SpotifyDLStatus.RUNNING:
logging.getLogger('luniebox').debug(
"download for '" + uri + "' still running")
elif status == SpotifyDLStatus.FINISHED:
logging.getLogger('luniebox').debug(
"alreaded downloaded '" + uri + "'")
return status
def downloadStatus(self, uri):
doneFiledPath = self.root + uri + '/.spotifydl'
if uri in self.downloads:
if self.downloads[uri]:
poll = self.downloads[uri].poll()
if poll != None:
self.downloads.pop(uri)
if poll == 0:
p = subprocess.Popen(["touch", doneFiledPath], stdin=None,
stdout=None, stderr=None, close_fds=True, shell=False, group="pi", user="pi")
p.communicate()
return SpotifyDLStatus.FINISHED
logging.getLogger('luniebox').warn(
"download of '" + uri + "' exited with: " + str(poll))
return SpotifyDLStatus.ERROR
return SpotifyDLStatus.RUNNING
if os.path.exists(doneFiledPath):
return SpotifyDLStatus.FINISHED
return SpotifyDLStatus.NONE
def getDownloads(self):
downloads = {}
for uri in self.downloads.keys():
downloads[uri] = str(self.downloadStatus(uri))
return downloads

1556
application/static/css/bootstrap-icons.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
img.item-image {
cursor: pointer;
width: 100px;
max-height: 100px;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
{% extends "template.html" %}
{% block content %}
{% if error == 'play': %}
<div class="my-3 alert alert-danger">
Error on playing from card.
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends "template.html" %}
{% block content %}
<h1>luniebox</h1>
<p>Welcome to luniebox web-interface. This is for configuring and control your luniebox device.</p>
<div class="row">
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Daemon</h5>
<p class="card-text">
{% if daemon_error: %}
<div class="my-3 alert alert-danger">
{{ daemon_error }}
<a href="{{ url_for('api.daemon_start') }}" class="btn btn-secondary">Start daemon</a>
</div>
{% endif %}
{% if daemon_status: %}
<div class="my-3 alert alert-success">
{{ daemon_status }}
</div>
{% endif %}
</p>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title">Spotify Integration</h5>
<p class="card-text">
{% if spotify['errors']['setup']: %}
<div class="alert alert-danger" role="alert">
No luniebox device found! Please setup spotifyd propertly (device name must match name from setup, default is 'luniebox')!
</div>
{% endif %}
{% if spotify['errors']['not_running']: %}
<div class="alert alert-warning" role="alert">
Spotifyd is not running. Please start spotifyd service on your luniebox!
<a href="{{ url_for('api.restart_spotifyd') }}" class="btn btn-primary">Restart spotifyd</a>
</div>
{% endif %}
{% if spotify['status']['not_active']: %}
<div class="alert alert-success" role="alert">
Spotify is available but currently inactive.
</div>
{% endif %}
<table class="table">
<tbody>
{% if spotify['status']['device']: %}
<tr class="{{ 'table-success' if spotify['status']['device']['id'] == spotify['device_id'] else '' }}">
<th scope="row">Current Device</th>
<td>{{ spotify['status']['device']['name'] }}</td>
</tr>
{% endif %}
{% if spotify['status']['item']: %}
{% with item = spotify['status']['item'] %}
<tr class="{{ '' if spotify['status']['is_playing'] else 'table-active' }}">
<th>{{ 'Currently playing' if spotify['status']['is_playing'] else 'Last played' }}</th>
<td class="d-flex w-100">
{% include "spotify/items/track.html" %}
</td>
</tr>
{% endwith %}
{% endif %}
</tbody>
</table>
</p>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title">Music Player Daemon Integration</h5>
<p class="card-text">
{% if not mpd: %}
<div class="my-3 alert alert-danger">
Music Player Daemon not connected
<a href="{{ url_for('api.restart_mpd') }}" class="btn btn-primary">Restart mpd</a>
</div>
{% endif %}
{% if mpd: %}
<table class="table">
<tbody>
<tr class="table-success">
<th scope="row">Current state</th>
<td>{{ mpd['state'] }}</td>
</tr>
{% if mpd['song']: %}
{% with item = mpd['song'] %}
<tr class="{{ '' if mpd['state'] == 'play' else 'table-active' }}">
<th>{{ 'Currently playing' if mpd['state'] == 'play' else 'Last played' }}</th>
<td class="d-flex w-100">
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['title'] }}</h5>
<p class="mb-1">
{{ item['artist'] }}
</p>
<small class="text-muted">{{ item['album'] }}</small>
</div>
</td>
</tr>
{% endwith %}
{% endif %}
</tbody>
</table>
{% endif %}
</p>
</div>
</div>
</div>
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
<div class="card">
<div class="card-body">
<p class="alert alert-warning">This will stop daemon. You need to restart daemon afterwards!</p>
<a href="{{ url_for('api.rfid_read') }}" class="btn btn-primary">Read card</a>
<a href="{{ url_for('api.rfid_play') }}" class="btn btn-secondary">Play card</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,50 @@
{% extends "template.html" %}
{% block content %}
<div class="mpd-search-results">
<div class="container">
{% if path: %}
<h3>{{ path }}</h3>
{% endif %}
<div class="d-flex flex-column mt-3">
<ul class="list-group mb-3 flex-fill">
{% for item in results %}
{% if item.directory and (spotify and item.directory.startswith('spotify:') or not spotify and not item.directory.startswith('spotify:')): %}
<li href="#" class="list-group-item flex-fill">
<div class="d-flex w-100">
<h5 class="mb-1"><a href="{{ url_for('pages.mpd_list', path=item.directory) }}">{{ item.directory }}</a>
</h5>
</div>
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value='mpd:' + item.directory) }}"><i
class="bi bi-pencil-square"></i>
Write to card</a>
<a class="btn btn-secondary" href="{{ url_for('api.play', uri='mpd:' + item.directory) }}"><i
class="bi bi-play"></i> Play
now</a>
{% endif %}
</li>
{% if item.file and not item.file.startswith('.') and (spotify and item.file.startswith('spotify:') or not spotify and not item.file.startswith('spotify:')): %}
<li href="#" class="list-group-item flex-fill">
<div class="d-flex w-100">
<h5 class="mb-1">{{ item.file }}</h5>
</div>
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value='mpd:' + path + '/' + item.file) }}"><i
class="bi bi-pencil-square"></i>
Write to card</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% if not spotify: %}
<a href="{{ url_for('pages.mpd_list', spotify=true) }}">List Spotify Downloads</a>
{% endif %}
{% if spotify: %}
<a href="{{ url_for('pages.mpd_list') }}">List MPD files</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "template.html" %}
{% block content %}
<h1> Setup </h1>
<p>Here you can setup the luniebox or restart services when having issues.</p>
<div class="row">
{% if model['setup']: %}
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
<div class="card">
<div class="card-body">
<a href="{{ url_for('api.restart_spotifyd') }}" class="btn btn-primary">Restart spotifyd</a>
<a href="{{ url_for('api.daemon_start') }}" class="btn btn-secondary">Start daemon</a>
<a href="{{ url_for('api.daemon_stop') }}" class="btn btn-warning">Stop daemon</a>
</div>
</div>
</div>
{% endif %}
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
{% if model['setup']: %}
<div class="my-3 alert alert-warning">
Already set-up! On changes you may need to restart spotifyd service.
</div>
{% endif %}
<form method="POST" action="{{ url_for('api.setup') }}">
<div class="mb-3">
<label for="spotify-device-name" class="form-label">Spotify Device Name</label>
<input type="text" class="form-control" id="spotify-device-name" name="spotify-device-name" required
value="{{'' if not model['device-name'] else '' + model['device-name'] }}">
</div>
<div class="mb-3">
<label for="spotify-username" class="form-label">Spotify Username</label>
<input type="text" class="form-control" id="spotify-username" name="spotify-username" required
value="{{'' if not model['username'] else '' + model['username'] }}">
</div>
<div class="mb-3">
<label for="spotify-password" class="form-label">Spotify Password</label>
<input type="password" class="form-control" id="spotify-password" name="spotify-password" required
value="{{'' if not model['password'] else '' + model['password'] }}">
</div>
<div class="mb-3">
<label for="spotify-client-id" class="form-label">Spotify Client Id</label>
<input type="text" class="form-control" id="spotify-client-id" name="spotify-client-id" required
value="{{'' if not model['client-id'] else '' + model['client-id'] }}">
</div>
<div class="mb-3">
<label for="spotify-client-secret" class="form-label">Spotify Client Secret</label>
<input type="text" class="form-control" id="spotify-client-secret" name="spotify-client-secret" required
value="{{'' if not model['client-secret'] else '' + model['client-secret'] }}">
</div>
<div class="mb-3">
<label for="spotify-redirect-uri" class="form-label">Spotify Redirect Uri</label>
<input type="text" class="form-control" id="spotify-redirect-uri" name="spotify-redirect-uri"
value="{{'' if not model['redirect-uri'] else '' + model['redirect-uri'] }}">
</div>
<button type="submit" class="btn btn-primary">Setup</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['name'] }}</h5>
<p class="mb-1">
{{ item['artists'] | map(attribute='name') | join(', ') }}
</p>
<small class="text-muted"></small>
</div>
{%if item['images']: %}
{% set images = item['images'] %}
{% include "spotify/items/cover.html" %}
{% endif %}

View File

@ -0,0 +1,9 @@
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['name'] }}</h5>
<p class="mb-1"></p>
<small class="text-muted"></small>
</div>
{%if item['images']: %}
{% set images = item['images'] %}
{% include "spotify/items/cover.html" %}
{% endif %}

View File

@ -0,0 +1,11 @@
<div class="dropdown">
<img src="{{ images | map(attribute='url') | first }}" class="item-image rounded dropdown-toggle"
data-bs-toggle="dropdown" data-bs-placement="top" title="Download Cover">
<ul class="dropdown-menu">
{% for image in images %}
<li>
<a class="dropdown-item" href="{{image['url']}}" target="_blank">{{image['width']}}px</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -0,0 +1,9 @@
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['name'] }}</h5>
<p class="mb-1"></p>
<small class="text-muted"></small>
</div>
{%if item['images']: %}
{% set images = item['images'] %}
{% include "spotify/items/cover.html" %}
{% endif %}

View File

@ -0,0 +1,22 @@
<div class="d-flex w-100 mt-3 justify-content-between">
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value=item.uri) }}"><i class="bi bi-pencil-square"></i>
Write to card</a>
<a class="btn btn-secondary" href="{{ url_for('api.play', uri=item.uri) }}"><i class="bi bi-play"></i> Play
now</a>
{% if 'spotifydl' in item: %}
{% if item['spotifydl'] == 'SpotifyDLStatus.NONE': %}
<a class="btn btn-warning" href="{{ url_for('api.spotify_dl', uri=item.uri) }}"><i class="bi bi-save"></i>
Download</a>
{% else: %}
<p>
{% if item['spotifydl'] == 'SpotifyDLStatus.FINISHED': %}
<span class="badge rounded-pill bg-success">Downloaded</span>
{% elif item['spotifydl'] == 'SpotifyDLStatus.RUNNING': %}
<span class="badge rounded-pill bg-info">Running</span>
{% elif item['spotifydl'] == 'SpotifyDLStatus.ERROR': %}
<span class="badge rounded-pill bg-danger">Error</span>
{% endif %}
</p>
{% endif %}
{% endif %}
</div>

View File

@ -0,0 +1,11 @@
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['name'] }}</h5>
<p class="mb-1">
{{ item['description'] }}
</p>
<small class="text-muted"></small>
</div>
{%if item['images']: %}
{% set images = item['images'] %}
{% include "spotify/items/cover.html" %}
{% endif %}

View File

@ -0,0 +1,9 @@
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['name'] }}</h5>
<p class="mb-1"></p>
<small class="text-muted"></small>
</div>
{%if item['images']: %}
{% set images = item['images'] %}
{% include "spotify/items/cover.html" %}
{% endif %}

View File

@ -0,0 +1,11 @@
<div class="d-flex flex-column flex-grow-1">
<h5 class="mb-1">{{ item['name'] }}</h5>
<p class="mb-1">
{{ item['artists'] | map(attribute='name') | join(', ') }}
</p>
<small class="text-muted">{{ item['album']['name'] }}</small>
</div>
{%if item['album']['images']: %}
{% set images = item['album']['images'] %}
{% include "spotify/items/cover.html" %}
{% endif %}

View File

@ -0,0 +1,117 @@
{% extends "template.html" %}
{% block content %}
<div class="mt-3">
<form method="GET" action="{{ url_for('pages.spotify_search') }}">
<div class="mb-3">
<label for="search-query" class="form-label">Search</label>
<input type="text" name="q" id="search-query" class="form-control" placeholder="Artists, songs, or podcasts"
required value="{{ query }}">
</div>
<div class="mb-3">
<div class="row">
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="type" value="track" id="search-type-track" {{'checked'
if 'track' in types else '' }}>
<label class="form-check-label" for="search-type-track">
Tracks
</label>
</div>
</div>
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="type" value="album" id="search-type-album" {{'checked'
if 'album' in types else '' }}>
<label class="form-check-label" for="search-type-album">
Albums
</label>
</div>
</div>
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="type" value="artist" id="search-type-artist"
{{'checked' if 'artist' in types else '' }}>
<label class="form-check-label" for="search-type-artist">
Artists
</label>
</div>
</div>
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="type" value="playlist" id="search-type-playlist"
{{'checked' if 'playlist' in types else '' }}>
<label class="form-check-label" for="search-type-playlist">
Playlists
</label>
</div>
</div>
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="type" value="show" id="search-type-show" {{'checked'
if 'show' in types else '' }}>
<label class="form-check-label" for="search-type-show">
Shows
</label>
</div>
</div>
<div class="col">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="type" value="episode" id="search-type-episode"
{{'checked' if 'episode' in types else '' }}>
<label class="form-check-label" for="search-type-episode">
Episodes
</label>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
{% if results: %}
<div class="alert alert-warning mt-3">
Writing to card will stop the daemon. You need to restart daemon afterwards!
</div>
<div class="spotify-search-results">
<div class="container">
<div class="row">
{% for type in types %}
{% if results[type + 's']: %}
{% with result = results[type + 's'] %}
<div
class="d-flex flex-column col col-xs-12 col-md-6 col-lg-6 {{ 'col-xl-4' if results|length > 2 else 'col-xl-6' }} mt-3">
<h3>{{ type | capitalize}}s</h3>
<ul class="list-group mb-3 flex-fill">
{% for item in result['items'] %}
<li href="#" class="list-group-item flex-fill">
<div class="d-flex w-100">
{% include "spotify/items/" + type + ".html" %}
</div>
{% include "spotify/items/menu.html" %}
</li>
{% endfor %}
</ul>
<div class="d-flex w-100 justify-content-between mt-auto">
<a href="{{ url_for('pages.spotify_search',q=query, type=type, limit=result['limit'], offset=result['offset']-result['limit']) }}"
class="btn btn-primary {{ 'disabled' if result['offset']==0 else '' }} " {{ 'disabled' if
result['offset']==0 else '' }}>&laquo;</a>
<a href="{{ url_for('pages.spotify_search',q=query, type=type, limit=result['limit'], offset=result['offset']+result['limit']) }}"
{{ 'disabled' if result['offset']>= result['total'] else ''}}
class="btn btn-primary {{ 'disabled' if result['offset']>= result['total'] else ''}}">&raquo;</a>
</div>
</div>
{% endwith %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "template.html" %}
{% block content %}
{% if uri: %}
<h3>{{ uri }}</h3>
{% endif %}
<p>{{ status }}</p>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "template.html" %}
{% block content %}
{% if success['rfid_write']: %}
<div class="my-3 alert alert-success">
Write card value: {{ success['rfid_write'] }}
</div>
{% endif %}
{% if success['rfid_read']: %}
<div class="my-3 alert alert-success">
Read card value: {{ success['rfid_read'] }}
</div>
{% endif %}
{% if success['rfid_play']: %}
<div class="my-3 alert alert-success">
Start playing from card.
</div>
{% endif %}
{% if success['play']: %}
<div class="my-3 alert alert-success">
Start playing {{ success['play'] }}.
</div>
{% endif %}
{% if success['restart_spotifyd']: %}
<div class="my-3 alert alert-secondary">
Restartet spotifyd service.
</div>
{% endif %}
{% if success['restart_mpd']: %}
<div class="my-3 alert alert-secondary">
Restartet mpd service.
</div>
{% endif %}
{% if success['start_daemon']: %}
<div class="my-3 alert alert-success">
Started luniebox-daemon service.
</div>
{% endif %}
{% if success['stop_daemon']: %}
<div class="my-3 alert alert-warning">
Stopped luniebox-daemon service.
</div>
{% endif %}
{% if success['restart_luniebox']: %}
<div class="my-3 alert alert-secondary">
Restartet luniebox-app service.
</div>
{% endif %}
<a class="btn btn-primary" href="{{ url_for('pages.index') }}">Go back to status page</a>
{% endblock %}

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>luniebox</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="{{ url_for('pages.index') }}">luniebox</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.index') }}">Status</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.spotify_search') }}">Search Spotify</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.mpd_list') }}">MPD listing</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.setup') }}">Setup</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container mt-3">
{% block content %}
{% endblock %}
</div>
<footer class="mt-5">
</footer>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
</body>
</html>

7
application/util.py Normal file
View File

@ -0,0 +1,7 @@
import random
import string
def randomString(len):
return ''.join(random.SystemRandom().choice(
string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(len))

View File

@ -0,0 +1,38 @@
pcm.speakerbonnet {
type hw card 0
}
pcm.dmixer {
type dmix
ipc_key 1024
ipc_perm 0666
slave {
pcm "speakerbonnet"
period_time 0
period_size 1024
buffer_size 8192
rate 44100
channels 2
}
}
ctl.dmixer {
type hw card 0
}
pcm.softvol {
type softvol
slave.pcm "dmixer"
control.name "PCM"
control.card 0
}
ctl.softvol {
type hw card 0
}
pcm.!default {
type plug
slave.pcm "softvol"
}

View File

@ -0,0 +1,17 @@
[api]
[hardware]
mpu = False
led = False
[logging]
level = DEBUG
[luniebox]
[mpd]
library_path = /home/pi/luniebox/library
[spotify]
auto_download = False
zspotify_path =

21
contrib/config/mpd.conf Normal file
View File

@ -0,0 +1,21 @@
music_directory "/home/pi/luniebox/library"
playlist_directory "/var/lib/mpd/playlists"
db_file "/var/lib/mpd/tag_cache"
restore_paused "yes"
log_file "/var/log/mpd/mpd.log"
pid_file "/run/mpd/pid"
state_file "/var/lib/mpd/state"
sticker_file "/var/lib/mpd/sticker.sql"
user "mpd"
bind_to_address "localhost"
audio_output {
type "alsa"
name "ALSA Device"
device "hw:CARD=sndrpihifiberry"
mixer_type "software"
mixer_device "default"
mixer_control "PCM"
mixer_index "0"
}
volume_normalization "yes"
filesystem_charset "UTF-8"

View File

@ -0,0 +1,11 @@
[global]
username = ""
password = ""
backend = "alsa"
device = "hw:CARD=sndrpihifiberry"
device_name = "luniebox"
device_type = "computer"
bitrate = 160
volume-normalisation = true
normalisation-pregain = 0
cache_path = "/home/pi/luniebox/.cache/spotify"

View File

@ -0,0 +1,12 @@
[Unit]
Description=Luniebox Application
[Service]
Type=simple
WorkingDirectory=/home/pi/luniebox/application
ExecStart=/home/pi/luniebox/application/venv/bin/gunicorn -b 0.0.0.0:80 app:app
Restart=always
RestartSec=12
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,12 @@
[Unit]
Description=Luniebox Daemon
[Service]
Type=simple
WorkingDirectory=/home/pi/luniebox/application
ExecStart=/home/pi/luniebox/application/venv/bin/python daemon.py
Restart=always
RestartSec=12
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,16 @@
[Unit]
Description=the spotify playing daemon
Wants=sound.target
After=sound.target
Wants=network-online.target
After=network-online.target
[Service]
User=pi
Group=pi
ExecStart=/home/pi/luniebox/bin/spotifyd --config-path /home/pi/luniebox/config/spotifyd.cfg --no-daemon
Restart=always
RestartSec=12
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,48 @@
include<luniebox-helper.scad>
module luniebox_back() {
difference() {
union() {
cube([length+depth,height+depth,depth]);
// screws RFID MC522
translate([length/2-22,height*0.6-13.5,depth])
screw();
translate([length/2-22,height*0.6+13.5,depth])
screw();
translate([length/2+16.5,height*0.6-18,depth])
screw();
translate([length/2+16.5,height*0.6+18,depth])
screw();
// clips
translate([clip_space,0,0])
clip_screw();
translate([clip_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,0,0])
clip_screw();
translate([clip_h,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([clip_h,clip_height_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_height_space,0])
rotate([0,0,90])
clip_screw();
}
// USB port hole
translate([54+depth,14+depth,0])
rounded_cube(s=[10,6,depth],r=1.5);
translate([38,20+depth,-render_limiter])
cube([10,1.5,depth+render_limiter*2]);
}
}
luniebox_back();

View File

@ -0,0 +1,48 @@
include<luniebox-helper.scad>
module luniebox_bottom() {
difference() {
union() {
cube([length+depth,width+depth,depth]);
// clips
translate([6,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([6,clip_width_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_width_space,0])
rotate([0,0,90])
clip_screw();
// screws Raspi Zero
translate([length-10,5+depth,depth])
screw();
translate([length-68,5+depth,depth])
screw();
translate([length-10,28+depth,depth])
screw();
translate([length-68,28+depth,depth])
screw();
// screws MCP
translate([length/2-7.5,width/2-7+depth,depth])
screw();
translate([length/2+7.5,width/2-7+depth,depth])
screw();
}
// clip holes
translate([clip_space,depth+clip_d/2,0])
clip_hole();
translate([clip_length_space,depth+clip_d/2,0])
clip_hole();
translate([clip_space,width-clip_d/2,0])
clip_hole();
translate([clip_length_space,width-clip_d/2,0])
clip_hole();
}
}
luniebox_bottom();

View File

@ -0,0 +1,10 @@
difference() {
cube([94,50,3]);
translate([6,-0.001,-0.001])
cube([82,44.002,1.502]);
translate([3,-0.001,1.499])
cube([88,47.002,1.502]);
}

View File

@ -0,0 +1,76 @@
include<luniebox-helper.scad>
module luniebox_front() {
difference() {
union() {
cube([length+depth,height+depth,depth]);
// screws speaker right
translate([height/2-22.5,height/2-22.5,depth])
screw();
translate([height/2+22.5,height/2-22.5,depth])
screw();
translate([height/2-22.5,height/2+22.5,depth])
screw();
translate([height/2+22.5,height/2+22.5,depth])
screw();
// screws speaker left
translate([length-height/2-22.5,height/2-22.5,depth])
screw();
translate([length-height/2+22.5,height/2-22.5,depth])
screw();
translate([length-height/2-22.5,height/2+22.5,depth])
screw();
translate([length-height/2+22.5,height/2+22.5,depth])
screw();
// clips
translate([clip_space,0,0])
clip_screw();
translate([clip_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,0,0])
clip_screw();
translate([clip_h,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([clip_h,clip_height_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_height_space,0])
rotate([0,0,90])
clip_screw();
}
// speaker hole right
translate([height/2,height/2,-depth / 2])
cylinder(d=50,h=depth*2);
// speaker hole left
translate([length-height/2,height/2,-depth / 2])
cylinder(d=50,h=depth * 2);
// screw holes speaker right
translate([height/2-22.5,height/2-22.5,depth])
screw_hole(depth);
translate([height/2+22.5,height/2-22.5,depth])
screw_hole(depth);
translate([height/2-22.5,height/2+22.5,depth])
screw_hole(depth);
translate([height/2+22.5,height/2+22.5,depth])
screw_hole(depth);
// screw holes speaker left
translate([length-height/2-22.5,height/2-22.5,depth])
screw_hole(depth);
translate([length-height/2+22.5,height/2-22.5,depth])
screw_hole(depth);
translate([length-height/2-22.5,height/2+22.5,depth])
screw_hole(depth);
translate([length-height/2+22.5,height/2+22.5,depth])
screw_hole(depth);
}
}
luniebox_front();

View File

@ -0,0 +1,74 @@
$fa = 1;
$fs = 0.05;
render_limiter = 0.001;
depth = 2;
length = 190;
width = 70;
height = 70;
clip_space=20;
clip_d=5;
clip_h=6;
clip_length_space=length - clip_space + depth;
clip_width_space=width - clip_space + depth;
clip_height_space=height - clip_space + depth;
module clip_screw() {
d=clip_d;
h=clip_h;
translate([0,0,d/2+depth])
rotate([-90,0,0])
difference() {
union() {
cylinder(d=d,h=h);
translate([-d/2,0,0])
cube([d,d/2,h]);
}
translate([0,0,-render_limiter])
cylinder(d=d/2,h=h+render_limiter*2);
}
}
module clip_hole(h=0) {
d=clip_d;
translate([0,0,-render_limiter])
cylinder(d=d/2,h=h+depth+render_limiter * 2);
}
module clip(width=15) {
translate([-render_limiter,-render_limiter,-render_limiter])
cube([width +render_limiter * 2,depth + render_limiter * 2,depth + render_limiter * 2]);
}
module screw_hole(depth=0,d=clip_d/2) {
z= -depth - render_limiter;
h=7+depth;
translate([0,0,z])
cylinder(d=d,h=h);
}
module screw(d=clip_d,h=clip_h) {
difference() {
cylinder(d=d,h=h);
screw_hole(d=d/2);
}
}
module rounded_cube(s=[1,1,1],r=0.5) {
x = s[0];
y = s[1];
z = s[2];
mx = x - 2*r;
my = y - 2*r;
mz = z / 2;
minkowski() {
cube([mx,my,mz]);
translate([r,r,-render_limiter])
cylinder(r=r,h=mz+render_limiter*2);
}
}

View File

@ -0,0 +1,31 @@
include<luniebox-helper.scad>
module luniebox_side() {
difference() {
cube([width+depth,height+depth*3,depth]);
// clip holes
translate([clip_space,clip_d/2+depth,0])
clip_hole();
translate([clip_width_space,clip_d/2+depth,0])
clip_hole();
translate([clip_space,height-clip_d/2+depth*2,0])
clip_hole();
translate([clip_width_space,height-clip_d/2+depth*2,0])
clip_hole();
translate([clip_d/2+depth,clip_space+depth,0])
clip_hole();
translate([clip_d/2+depth,clip_height_space+depth,0])
clip_hole();
translate([width-clip_d/2,clip_space+depth,0])
clip_hole();
translate([width-clip_d/2,clip_height_space+depth,0])
clip_hole();
}
}
luniebox_side();

View File

@ -0,0 +1,46 @@
include<luniebox-helper.scad>
module luniebox_top() {
difference() {
union() {
cube([length+depth,width+depth,depth]);
// clips
translate([clip_h,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([clip_h,clip_width_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_width_space,0])
rotate([0,0,90])
clip_screw();
}
// clip holes
translate([clip_space,depth+clip_d/2,0])
clip_hole();
translate([clip_length_space,depth+clip_d/2,0])
clip_hole();
translate([clip_space,width-clip_d/2,0])
clip_hole();
translate([clip_length_space,width-clip_d/2,0])
clip_hole();
// right btn hole
translate([length/2-25+depth,(width+depth)/2,-depth/2])
cylinder(d=30,h=depth*2);
// left button hole
translate([length/2+25+depth,(width+depth)/2,-depth/2])
cylinder(d=30,h=depth*2);
// led holes
translate([length/2+depth,(width+depth)/2-16.666,-depth/2])
cylinder(d=2,h=depth*2);
translate([length/2+depth,(width+depth)/2,-depth/2])
cylinder(d=2,h=depth*2);
translate([length/2+depth,(width+depth)/2+16.666,-depth/2])
cylinder(d=2,h=depth*2);
}
}
luniebox_top();

View File

@ -0,0 +1,34 @@
include<luniebox-helper.scad>
use<luniebox-bottom.scad>
use<luniebox-front.scad>
use<luniebox-side.scad>
use<luniebox-top.scad>
use<luniebox-back.scad>
color(c=[1,0.1,0.1])
luniebox_bottom();
color(c=[0.2,1,0.2])
translate([0,width+depth,depth])
rotate([90,0,0])
luniebox_front();
color(c=[0.3,0.3,1])
translate([-depth,0,0])
rotate([90,0,90])
luniebox_side();
color(c=[1,1,0.4])
translate([length+depth,0,0])
rotate([90,0,90])
luniebox_side();
translate([0,width+depth,height+depth*3])
rotate([180,0,0])
color(c=[1,0.5,1])
luniebox_top();
color(c=[1,0.6,1])
translate([length+depth,0,depth])
rotate([90,0,180])
luniebox_back();

View File

@ -0,0 +1,48 @@
include<luniebox-helper.scad>
module luniebox_back() {
difference() {
union() {
cube([length+depth,height+depth,depth]);
// screws RFID MC522
translate([length/2-22,height*0.6-13.5,depth])
screw();
translate([length/2-22,height*0.6+13.5,depth])
screw();
translate([length/2+16.5,height*0.6-18,depth])
screw();
translate([length/2+16.5,height*0.6+18,depth])
screw();
// clips
translate([clip_space,0,0])
clip_screw();
translate([clip_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,0,0])
clip_screw();
translate([clip_h,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([clip_h,clip_height_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_height_space,0])
rotate([0,0,90])
clip_screw();
}
// USB port hole
translate([54+depth,14+depth,0])
rounded_cube(s=[10,6,depth],r=1.5);
translate([38,20+depth,-render_limiter])
cube([10,1.5,depth+render_limiter*2]);
}
}
luniebox_back();

View File

@ -0,0 +1,48 @@
include<luniebox-helper.scad>
module luniebox_bottom() {
difference() {
union() {
cube([length+depth,width+depth,depth]);
// clips
translate([6,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([6,clip_width_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_width_space,0])
rotate([0,0,90])
clip_screw();
// screws Raspi Zero
translate([length-10,5+depth,depth])
screw();
translate([length-68,5+depth,depth])
screw();
translate([length-10,28+depth,depth])
screw();
translate([length-68,28+depth,depth])
screw();
// screws MCP
translate([length/2-7.5,width/2-7+depth,depth])
screw();
translate([length/2+7.5,width/2-7+depth,depth])
screw();
}
// clip holes
translate([clip_space,depth+clip_d/2,0])
clip_hole();
translate([clip_length_space,depth+clip_d/2,0])
clip_hole();
translate([clip_space,width-clip_d/2,0])
clip_hole();
translate([clip_length_space,width-clip_d/2,0])
clip_hole();
}
}
luniebox_bottom();

View File

@ -0,0 +1,10 @@
difference() {
cube([94,50,3]);
translate([6,-0.001,-0.001])
cube([82,44.002,1.502]);
translate([3,-0.001,1.499])
cube([88,47.002,1.502]);
}

View File

@ -0,0 +1,76 @@
include<luniebox-helper.scad>
module luniebox_front() {
difference() {
union() {
cube([length+depth,height+depth,depth]);
// screws speaker right
translate([height/2-22.5,height/2-22.5,depth])
screw();
translate([height/2+22.5,height/2-22.5,depth])
screw();
translate([height/2-22.5,height/2+22.5,depth])
screw();
translate([height/2+22.5,height/2+22.5,depth])
screw();
// screws speaker left
translate([length-height/2-22.5,height/2-22.5,depth])
screw();
translate([length-height/2+22.5,height/2-22.5,depth])
screw();
translate([length-height/2-22.5,height/2+22.5,depth])
screw();
translate([length-height/2+22.5,height/2+22.5,depth])
screw();
// clips
translate([clip_space,0,0])
clip_screw();
translate([clip_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,height-clip_h+depth,0])
clip_screw();
translate([clip_length_space,0,0])
clip_screw();
translate([clip_h,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([clip_h,clip_height_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_height_space,0])
rotate([0,0,90])
clip_screw();
}
// speaker hole right
translate([height/2,height/2,-depth / 2])
cylinder(d=50,h=depth*2);
// speaker hole left
translate([length-height/2,height/2,-depth / 2])
cylinder(d=50,h=depth * 2);
// screw holes speaker right
translate([height/2-22.5,height/2-22.5,depth])
screw_hole(depth);
translate([height/2+22.5,height/2-22.5,depth])
screw_hole(depth);
translate([height/2-22.5,height/2+22.5,depth])
screw_hole(depth);
translate([height/2+22.5,height/2+22.5,depth])
screw_hole(depth);
// screw holes speaker left
translate([length-height/2-22.5,height/2-22.5,depth])
screw_hole(depth);
translate([length-height/2+22.5,height/2-22.5,depth])
screw_hole(depth);
translate([length-height/2-22.5,height/2+22.5,depth])
screw_hole(depth);
translate([length-height/2+22.5,height/2+22.5,depth])
screw_hole(depth);
}
}
luniebox_front();

View File

@ -0,0 +1,74 @@
$fa = 1;
$fs = 0.05;
render_limiter = 0.001;
depth = 2;
length = 100;
width = 100;
height = 100;
clip_space=20;
clip_d=5;
clip_h=6;
clip_length_space=length - clip_space + depth;
clip_width_space=width - clip_space + depth;
clip_height_space=height - clip_space + depth;
module clip_screw() {
d=clip_d;
h=clip_h;
translate([0,0,d/2+depth])
rotate([-90,0,0])
difference() {
union() {
cylinder(d=d,h=h);
translate([-d/2,0,0])
cube([d,d/2,h]);
}
translate([0,0,-render_limiter])
cylinder(d=d/2,h=h+render_limiter*2);
}
}
module clip_hole(h=0) {
d=clip_d;
translate([0,0,-render_limiter])
cylinder(d=d/2,h=h+depth+render_limiter * 2);
}
module clip(width=15) {
translate([-render_limiter,-render_limiter,-render_limiter])
cube([width +render_limiter * 2,depth + render_limiter * 2,depth + render_limiter * 2]);
}
module screw_hole(depth=0,d=clip_d/2) {
z= -depth - render_limiter;
h=7+depth;
translate([0,0,z])
cylinder(d=d,h=h);
}
module screw(d=clip_d,h=clip_h) {
difference() {
cylinder(d=d,h=h);
screw_hole(d=d/2);
}
}
module rounded_cube(s=[1,1,1],r=0.5) {
x = s[0];
y = s[1];
z = s[2];
mx = x - 2*r;
my = y - 2*r;
mz = z / 2;
minkowski() {
cube([mx,my,mz]);
translate([r,r,-render_limiter])
cylinder(r=r,h=mz+render_limiter*2);
}
}

View File

@ -0,0 +1,31 @@
include<luniebox-helper.scad>
module luniebox_side() {
difference() {
cube([width+depth,height+depth*3,depth]);
// clip holes
translate([clip_space,clip_d/2+depth,0])
clip_hole();
translate([clip_width_space,clip_d/2+depth,0])
clip_hole();
translate([clip_space,height-clip_d/2+depth*2,0])
clip_hole();
translate([clip_width_space,height-clip_d/2+depth*2,0])
clip_hole();
translate([clip_d/2+depth,clip_space+depth,0])
clip_hole();
translate([clip_d/2+depth,clip_height_space+depth,0])
clip_hole();
translate([width-clip_d/2,clip_space+depth,0])
clip_hole();
translate([width-clip_d/2,clip_height_space+depth,0])
clip_hole();
}
}
luniebox_side();

View File

@ -0,0 +1,46 @@
include<luniebox-helper.scad>
module luniebox_top() {
difference() {
union() {
cube([length+depth,width+depth,depth]);
// clips
translate([clip_h,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([clip_h,clip_width_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_space,0])
rotate([0,0,90])
clip_screw();
translate([length+depth,clip_width_space,0])
rotate([0,0,90])
clip_screw();
}
// clip holes
translate([clip_space,depth+clip_d/2,0])
clip_hole();
translate([clip_length_space,depth+clip_d/2,0])
clip_hole();
translate([clip_space,width-clip_d/2,0])
clip_hole();
translate([clip_length_space,width-clip_d/2,0])
clip_hole();
// right btn hole
translate([length/2-25+depth,(width+depth)/2,-depth/2])
cylinder(d=30,h=depth*2);
// left button hole
translate([length/2+25+depth,(width+depth)/2,-depth/2])
cylinder(d=30,h=depth*2);
// led holes
translate([length/2+depth,(width+depth)/2-16.666,-depth/2])
cylinder(d=2,h=depth*2);
translate([length/2+depth,(width+depth)/2,-depth/2])
cylinder(d=2,h=depth*2);
translate([length/2+depth,(width+depth)/2+16.666,-depth/2])
cylinder(d=2,h=depth*2);
}
}
luniebox_top();

View File

@ -0,0 +1,34 @@
include<luniebox-helper.scad>
use<luniebox-bottom.scad>
use<luniebox-front.scad>
use<luniebox-side.scad>
use<luniebox-top.scad>
use<luniebox-back.scad>
color(c=[1,0.1,0.1])
luniebox_bottom();
color(c=[0.2,1,0.2])
translate([0,width+depth,depth])
rotate([90,0,0])
luniebox_front();
color(c=[0.3,0.3,1])
translate([-depth,0,0])
rotate([90,0,90])
luniebox_side();
color(c=[1,1,0.4])
translate([length+depth,0,0])
rotate([90,0,90])
luniebox_side();
translate([0,width+depth,height+depth*3])
rotate([180,0,0])
color(c=[1,0.5,1])
luniebox_top();
color(c=[1,0.6,1])
translate([length+depth,0,depth])
rotate([90,0,180])
luniebox_back();

348
luniebox.sh Executable file
View File

@ -0,0 +1,348 @@
#!/usr/bin/env bash
AUTOMATIC=false
NO_UPDATES=false
NO_SPOTIFYDL=false
NO_SPOTIFY=false
NO_MPD=false
NO_AUDIO=false
MPU=false
AUDIO_DEVICE=false
AUDIO_DEVICES=("pimoroni" "adafruit")
DEVICE_NAME=false
help() {
local IFS="|"
printf "Usage: ${SCRIPTNAME} [-h] [-a] [--audio-device AUDIO_DEVICE] [--device-name DEVICE_NAME] [--no-updates] [--no-spotifydl] [--no-spotify] [--no-mpd] [--mpu] \n"
printf " -h\t\t\t\tshow this help\n"
printf " -a\t\t\t\tautomatic/non-interactive mode (without --audio-device, no Audio device is set up)\n"
printf " --audio-device [AUDIO_DEVICE]\tset Audio device (one of [${AUDIO_DEVICES[*]}])\n"
printf " --device-name [DEVICE_NAME]\tset luniebox device name (default: luniebox)\n"
printf " --no-updates\t\t\tskip system updates \n"
printf " --no-spotifydl\t\t\tskip installation of zspotify (no offline support for Spotify) \n"
printf " --no-mpd\t\t\tskip installation of Music Player Daemon (no offline support, also skips zspotify)\n"
printf " --no-spotify\t\t\tskip installation of spotifyd (no Spotify support, also skips zspotify)\n"
printf " --mpu\t\t\t\tsetup MPU9250 on I2C port (default is no MPU9250)\n"
exit 0
}
while [ -n "$1" ]; do
case "$1" in
"-h" | "--help")
help
;;
"-a" | "--automatic")
AUTOMATIC=true
;;
"--no-updates")
NO_UPDATES=true
;;
"--no-spotifydl")
NO_SPOTIFYDL=true
;;
"--no-spotify")
NO_SPOTIFY=true
;;
"--audio-device")
AUDIO_DEVICE="$2"
shift
;;
"--device-name")
DEVICE_NAME="$2"
shift
;;
"--mpu")
MPU=true
;;
*)
printf "\nargument '$1' not supported!\n\n"
help
;;
esac
shift
done
if [[ ${AUDIO_DEVICE} != "false" ]] && ! [[ ${AUDIO_DEVICES[*]} =~ "$AUDIO_DEVICE" ]]; then
NO_AUDIO=true
printf "Audio device '${AUDIO_DEVICE}' not supported, please set it up manually.\n"
fi
AUDIO_DEVICES+=("other")
start() {
if [[ ${AUTOMATIC} == "false" ]]; then
echo "##########################################
# _ _ _ #
# | |_ _ _ __ (_) ___| |__ _____ __ #
# | | | | | '_ \| |/ _ \ '_ \ / _ \ \/ / #
# | | |_| | | | | | __/ |_) | (_) > < #
# |_|\__,_|_| |_|_|\___|_.__/ \___/_/\_\ #
# #
##########################################
Welcome to luniebox setup. This script will perfom some interactive
steps to install and configure everything neccessary to convert this
device into a standalone spotify music box with RFID functionality.
If you want to run the script automatically, press n and restart script with -a option.
Run the script with -h to get more information about possible options to be applied for setup process.
"
read -rp "Do you want to setup luniebox now? [Y/n] " response
echo ""
case "$response" in
[nN][oO] | [nN])
echo "Bye...."
exit 0
;;
esac
fi
setup
}
setup() {
cd /home/pi
if [[ ${NO_UPDATES} == "false" ]]; then
update
fi
setup_software
set_device_name
setup_rfid
setup_mpu
if [[ ${NO_AUDIO} == "false" ]]; then
setup_audio
fi
finalize
}
update() {
if [[ ${AUTOMATIC} == "false" ]]; then
read -rp "Do you want to update system now? [Y/n] (recommended) " response
echo ""
case "$response" in
[nN][oO] | [nN])
return 0
;;
esac
fi
echo "Update system"
sudo apt update
sudo apt upgrade -y
}
setup_software() {
setup_app
if [[ ${NO_SPOTIFY} == "false" ]]; then
setup_spotify
fi
if [[ ${NO_MPD} == "false" ]]; then
setup_mpd
fi
if [[ ${NO_SPOTIFYDL} == "false" ]]; then
setup_zspotify
fi
}
setup_app() {
echo "Install required packages..."
sudo apt install -y git python3-venv python3-pip
echo "Fetch luniebox repository..."
git clone https://git.bstly.de/Lurkars/luniebox.git /home/pi/luniebox
cd /home/pi/luniebox/application
echo "Setup python venv..."
python -m venv venv
source venv/bin/activate
export CFLAGS=-fcommon
echo "Install pip requirements..."
pip install -r requirements.txt
deactivate
mkdir /home/pi/luniebox/config
echo "Setup config and systemd services..."
cp /home/pi/luniebox/contrib/config/luniebox.cfg /home/pi/luniebox/config/luniebox.cfg
sudo cp /home/pi/luniebox/contrib/service/luniebox-app.service /etc/systemd/system/
sudo cp /home/pi/luniebox/contrib/service/luniebox-daemon.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable luniebox-app luniebox-daemon
}
setup_spotify() {
if [[ ${AUTOMATIC} == "false" ]]; then
read -rp "Do you want to install spotifyd for Spofity support? [Y/n] " response
echo ""
case "$response" in
[nN][oO] | [nN])
NO_SPOTIFY=true
return 0
;;
esac
fi
mkdir /home/pi/luniebox/bin
echo "Download spotifyd..."
wget -c https://github.com/Spotifyd/spotifyd/releases/download/v0.3.3/spotifyd-linux-armv6-slim.tar.gz -O - | tar -xz -C /home/pi/luniebox/bin
echo "Setup config and systemd services..."
if [[ ${NO_AUDIO} == "true" ]]; then
echo "WARNING: you may need to adjust the file '/home/pi/luniebox/config/spotifyd.cfg' manually to setup your audio device!"
echo ""
fi
cp /home/pi/luniebox/contrib/config/spotifyd.cfg /home/pi/luniebox/config/spotifyd.cfg
sudo cp /home/pi/luniebox/contrib/service/spotifyd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable spotifyd
}
setup_mpd() {
if [[ ${AUTOMATIC} == "false" ]]; then
read -rp "Do you want to install mpd (Music Player Daemon) for offline file support? [Y/n] " response
echo ""
case "$response" in
[nN][oO] | [nN])
NO_MPD=true
return 0
;;
esac
fi
mkdir /home/pi/luniebox/library
echo "Install required packages..."
sudo apt install -y mpd
echo "Setup config..."
if [[ ${NO_AUDIO} == "true" ]]; then
echo "WARNING: you may need to adjust the file '/etc/mpd.conf' manually to setup your audio device!"
echo ""
fi
sudo cp /home/pi/luniebox/contrib/config/mpd.conf /etc/mpd.conf
}
setup_zspotify() {
if [[ ${NO_SPOTIFY} == "false" ]] && [[ ${NO_MPD} == "false" ]]; then
if [[ ${AUTOMATIC} == "false" ]]; then
read -rp "Do you want to install zspotify for downloading Spotify tracks for offline support? [Y/n] " response
echo ""
case "$response" in
[nN][oO] | [nN])
NO_MPD=true
return 0
;;
esac
fi
echo "Fetch clspotify repository..."
git clone https://github.com/agent255/clspotify.git /home/pi/clspotify
cd /home/pi/clspotify
echo "Setup python venv..."
python -m venv venv
source venv/bin/activate
echo "Install pip requirements..."
pip install -r requirements.txt
deactivate
echo "Setup config..."
sed -i "s/^zspotify_path =.*$/zspotify_path = \/home\/pi\/clspotify\//" /home/pi/luniebox/config/luniebox.cfg
fi
}
set_device_name() {
if [[ ${AUTOMATIC} == "false" ]] && [[ ${DEVICE_NAME} == "false" ]]; then
read -rp "Name for your Luniebox devices [default: luniebox]: " DEVICE_NAME
fi
if ! [[ $DEVICE_NAME ]] || [[ ${DEVICE_NAME} == "false" ]]; then
DEVICE_NAME="luniebox"
fi
echo "Set device name to '${DEVICE_NAME}'"
printf "${DEVICE_NAME}" | sudo tee /etc/hostname 1>/dev/null
sudo sed -i "/^127.0.0.1/s/$/ ${DEVICE_NAME}/g" /etc/hosts
if [[ NO_SPOTIFY == "false" ]]; then
sed -i "s/^device_name.*=.*$/device_name = \"${DEVICE_NAME}\"/" /home/pi/luniebox/config/spotifyd.cfg
fi
}
setup_rfid() {
echo "Enable SPI for RFID reader"
sudo sed -i "/dtparam=spi=on/s/^#//g" /boot/config.txt
}
setup_mpu() {
if [[ ${AUTOMATIC} == "false" ]] && [[ ${MPU} == "false" ]]; then
read -rp "Do you want to setup I2C for MPU9250 9-axis sensor? [y/N] " response
echo ""
case "$response" in
[yY][eE][sS] | [yY])
MPU=true
break
;;
*)
return 0
;;
esac
fi
if [[ ${MPU} == "true" ]]; then
echo "Install I2C software, enable i2c bus 4, enable mpu"
sudo apt install -y i2c-tools python3-smbus
sudo sed -i "/dtparam=i2c_arm=on/s/^#//g" /boot/config.txt
printf "dtoverlay=i2c-gpio,bus=4,i2c_gpio_delay_us=1,i2c_gpio_sda=23,i2c_gpio_scl=24" | sudo tee -a /boot/config.txt 1>/dev/null
sed -i "s/^mpu =.*$/mpu = True/" /home/pi/luniebox/config/luniebox.cfg
fi
}
setup_audio() {
if [[ ${AUTOMATIC} == "false" ]]; then
echo "Choose your audio hardware"
select response in "${AUDIO_DEVICES[@]}"; do
echo ""
AUDIO_DEVICE = "$response"
done
fi
case "$AUDIO_DEVICE" in
"pimoroni")
setup_pimoroni
;;
"adafruit")
setup_adafruit
;;
*)
NO_AUDIO=true
echo "WARNING: please setup your audio device manually!"
;;
esac
}
setup_pimoroni() {
echo "Setup Pimoroni Shim"
sudo sed -i "/dtparam=audio=on/s/^/#/g" /boot/config.txt
printf "dtoverlay=hifiberry-dac\ngpio=25=op,dh" | sudo tee -a /boot/config.txt 1>/dev/null
}
setup_adafruit() {
echo "Setup Adafruit Speaker Bonnet"
sudo sed -i "/dtparam=audio=on/s/^/#/g" /boot/config.txt
printf "dtoverlay=hifiberry-dac\ndtoverlay=i2s-mmap" | sudo tee -a /boot/config.txt 1>/dev/null
cat /home/pi/luniebox/contrib/config/asound.conf | sudo tee /etc/asound.conf 1>/dev/null
}
finalize() {
if [[ ${AUTOMATIC} == "false" ]]; then
read -rp "Setup finished and reboot required. Do you want to automatically reboot your luniebox now? [Y/n] " response
echo ""
case "$response" in
[nN][oO] | [nN])
echo "Bye...."
exit 0
;;
esac
fi
echo "Rebooting now...."
sleep 2
sudo reboot
exit
}
start