initialize
This commit is contained in:
commit
75749efa02
|
@ -0,0 +1,6 @@
|
|||
.vscode
|
||||
.local
|
||||
.cache
|
||||
library/*
|
||||
config/*
|
||||
bin/*
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
|||
bin
|
||||
venv
|
||||
__pycache__
|
|
@ -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)
|
|
@ -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))
|
|
@ -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')
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
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.
|
@ -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
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 '' }}>«</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 ''}}">»</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if uri: %}
|
||||
<h3>{{ uri }}</h3>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ status }}</p>
|
||||
|
||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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))
|
|
@ -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"
|
||||
}
|
|
@ -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 =
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
@ -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();
|
|
@ -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();
|
|
@ -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
|
Loading…
Reference in New Issue