initialize
This commit is contained in:
commit
75749efa02
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.vscode
|
||||
.local
|
||||
.cache
|
||||
library/*
|
||||
config/*
|
||||
bin/*
|
244
README.md
Normal file
244
README.md
Normal file
@ -0,0 +1,244 @@
|
||||
# luniebox
|
||||
|
||||
lunibox is a RFID jukebox based on a Raspberry Pi. It is similar to the [Phoniebox](https://www.phoniebox.de) \[[https://github.com/MiczFlor/RPi-Jukebox-RFID](GitHub)\] and an upgrade of the [TonUINO (de)](https://www.voss.earth/tonuino/) which both are DIY versions of the popular Toniebox©. The main focus for now is to play Spotify© content, playing local files or other sources will be integrated later.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need to be familiar with ssh (Putty on Windows). For the usage of Spotify© a premium account is required.
|
||||
|
||||
### Hardware
|
||||
- Raspberry Pi (tested with Raspberry Pi 3 Model B, Zero W, Zero 2 W) \[starting at ~14€\]
|
||||
- power supply (5V ~2.5A) \[starting at ~6€\]
|
||||
- Micro SD card (at least 4GB) \[starting at ~4€\]
|
||||
- RC522 RFID Reader connected to [TODO] \[starting at ~1.50€\]
|
||||
- RFID (MiFare) cards or chips \[starting at ~2€\]
|
||||
- a Audio Card:
|
||||
- Pimoroni Audio Amp SHIM (3W Mono Amp) \[starting at ~11€\] with passive speaker \[starting at ~5€\]
|
||||
- Pimoroni Audio DAC SHIM \[starting at ~14€\] with active speaker
|
||||
- Adafruit Speaker Bonnet for Raspberry Pi
|
||||
- ...something else (use custom setup!)
|
||||
- (optional) MPU9250 9-axis sensor [TODO] \[starting at ~1.50€\]
|
||||
- (optional) Waveshare UPS HAT + 2x 18650 18650 Li battery \[starting at ~30€\]
|
||||
- some wires or dupont connectors \[starting at ~2€\]
|
||||
- case for all above
|
||||
- depending on hardware: soldering equipment
|
||||
|
||||
A minimal setup (Raspberry Zero 2 W, power supply, Micro SD, RC522, Cards, Audio Amp) should be about ~40€ plus case materials.
|
||||
|
||||
### Software
|
||||
- latest Raspberry Pi OS blank installation on Micro SD card (Instructions: [raspberrypi.com/software/](https://www.raspberrypi.com/software/))
|
||||
- WiFi connection
|
||||
- ssh enabled
|
||||
|
||||
### Optional: Headless installation
|
||||
|
||||
If you're using a Raspberry Pi Zero or have missing peripherals to setup WiFi and ssh please perform the following steps:
|
||||
- insert Micro SD card with Raspberry Pi OS into a computer
|
||||
- place an empty file called `ssh` into `/boot` folder/partioon
|
||||
- place a filed called `wpa_supplicant.conf` into `/boot` folder/partition with following content
|
||||
```
|
||||
country=$COUNTRY_CODE
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
|
||||
network={
|
||||
ssid="$WIFI_SSID"
|
||||
psk="$WIFI_PASSWORD"
|
||||
}
|
||||
```
|
||||
- replace `$COUNTRY_CODE` with upper-case country code (eg. GB or DE) and `$WIFI_SSID` and `$WIFI_PASSWORD` with your WiFi credentials
|
||||
- plug card back into your Pi and connect power supply
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Establish a ssh connection to your Pi:
|
||||
- the Pi should be reachable under `raspberrypi` or `raspberrypi.local` in your network, if not try to find out it's IP address from your router.
|
||||
- default username is `pi` and default password is `raspberry`
|
||||
- Example: `ssh pi@raspberrypi.local` or `ssh pi@192.168.2.100`
|
||||
> ⚠️ Warning: you should change the default passwort by executing `passwd` after login
|
||||
|
||||
### Automatic setup
|
||||
|
||||
> ⚠️ Warning: executing scripts from the internet without checking is bad. This is only done to get things done fast. In any doubts you can perform the [Manual setup](#manual-setup) and execute all commands step-by-setop to understand what's going on.
|
||||
|
||||
- download and excecute setup script (you will be prompted to confirm certain steps beforehand anyway)
|
||||
> `curl https://git.bstly.de/Lurkars/luniebox/raw/branch/main/luniebox.sh -o luniebox.sh`
|
||||
>
|
||||
> `chmod +x luniebox.sh`
|
||||
>
|
||||
> `./luniebox.sh`
|
||||
|
||||
### Manual setup
|
||||
|
||||
#### Software setup
|
||||
|
||||
go to home directory
|
||||
> `cd /home/pi`
|
||||
|
||||
install `git`, `python3-venv` and `python3-pip`
|
||||
> `sudo apt install -y git python3-venv python3-pip`
|
||||
|
||||
clone repository `https://git.bstly.de/Lurkars/luniebox.git` with sources and config:
|
||||
> `git clone https://git.bstly.de/Lurkars/luniebox.git luniebox`
|
||||
|
||||
setup application
|
||||
> `cd /home/pi/luniebox/application`
|
||||
>
|
||||
> `python -m venv venv`
|
||||
>
|
||||
> `source venv/bin/activate`
|
||||
>
|
||||
> `export CFLAGS=-fcommon`
|
||||
>
|
||||
> `pip install -r requirements.txt`
|
||||
>
|
||||
> `deactivate`
|
||||
>
|
||||
> `mkdir /home/pi/luniebox/config`
|
||||
>
|
||||
> `cp /home/pi/luniebox/contrib/config/luniebox.cfg /home/pi/luniebox/config/luniebox.cfg`
|
||||
>
|
||||
> `sudo cp /home/pi/luniebox/contrib/luniebox-app.service /etc/systemd/system/`
|
||||
>
|
||||
> `sudo cp /home/pi/luniebox/contrib/luniebox-daemon.service /etc/systemd/system/`
|
||||
>
|
||||
> `sudo systemctl daemon-reload`
|
||||
>
|
||||
> `sudo systemctl enable luniebox-app luniebox-daemon`
|
||||
|
||||
|
||||
setup spotifyd
|
||||
> `mkdir /home/pi/luniebox/bin`
|
||||
>
|
||||
> `wget -c https://github.com/Spotifyd/spotifyd/releases/download/v0.3.3/spotifyd-linux-armv6-slim.tar.gz -O - | tar -xz -C /home/pi/luniebox/bin`
|
||||
>
|
||||
> `cp /home/pi/luniebox/contrib/config/spotifyd.cfg /home/pi/luniebox/config/spotifyd.cfg` (if you use other audio hardware, you may need to adjust the `backend` and `device` properties to your needs!)
|
||||
>
|
||||
> `sudo cp /home/pi/luniebox/contrib/spotifyd.service /etc/systemd/system/`
|
||||
>
|
||||
> `sudo systemctl daemon-reload`
|
||||
>
|
||||
> `sudo systemctl enable spotifyd`
|
||||
|
||||
setup mpd
|
||||
> `mkdir /home/pi/luniebox/library`
|
||||
>
|
||||
> `sudo apt install -y mpd`
|
||||
>
|
||||
> `sudo cp /home/pi/luniebox/contrib/config/mpd.conf /etc/mpd.conf` (if you use other audio hardware, you may need to adjust the `audio_output` section to your needs!)
|
||||
>
|
||||
|
||||
start/restart all services
|
||||
> `sudo systemctl restart mpd spotifyd luniebox-daemon luniebox-app`
|
||||
|
||||
setup ClSpotify
|
||||
> `git clone https://github.com/agent255/clspotify.git /home/pi/clspotify`
|
||||
>
|
||||
> `cd /home/pi/clspotify`
|
||||
>
|
||||
> `python -m venv venv`
|
||||
>
|
||||
> `source venv/bin/activate`
|
||||
>
|
||||
> `pip install -r requirements.txt`
|
||||
>
|
||||
> `deactivate`
|
||||
>
|
||||
> `sed -i -i 's/^zspotify_path =.*$/zspotify_path = \/home\/pi\/clspotify\//' /home/pi/luniebox/config/luniebox.cfg`
|
||||
|
||||
#### Hardware Setup
|
||||
|
||||
##### enable SPI for RFID Reader
|
||||
|
||||
uncomment `dtparam=spi=on` in `/boot/config.txt`
|
||||
> `sudo sed -i '/dtparam=spi=on/s/^#//g' /boot/config.txt`
|
||||
|
||||
##### enable I2C for MPU9250 9-axis sensor
|
||||
|
||||
install `i2c-tools` and `python3-smbus`
|
||||
> `sudo apt install -y i2c-tools python3-smbus`
|
||||
|
||||
uncomment `dtparam=i2c_arm=on` in `/boot/config.txt`
|
||||
> `sudo sed -i '/dtparam=i2c_arm=on/s/^#//g' /boot/config.txt`
|
||||
|
||||
|
||||
add `dtoverlay=i2c-gpio,bus=4,i2c_gpio_delay_us=1,i2c_gpio_sda=23,i2c_gpio_scl=24` to `/boot/config.txt`
|
||||
> `printf "dtoverlay=i2c-gpio,bus=4,i2c_gpio_delay_us=1,i2c_gpio_sda=23,i2c_gpio_scl=24" | sudo tee -a /boot/config.txt`
|
||||
|
||||
#### Setup Audio
|
||||
|
||||
##### for Pimoroni Amp or DAC
|
||||
|
||||
disable onboard audio comment out `dtparam=audio=on` in `/boot/config.txt`
|
||||
> `sudo sed -i '/dtparam=audio=on/s/^/#/g' /boot/config.txt`
|
||||
|
||||
setup hifiberry-dac by adding
|
||||
```
|
||||
dtoverlay=hifiberry-dac
|
||||
gpio=25=op,dh
|
||||
```
|
||||
to `/boot/config.txt`
|
||||
> `printf "dtoverlay=hifiberry-dac\ngpio=25=op,dh" | sudo tee -a /boot/config.txt`
|
||||
|
||||
##### for Adafruit Speaker Bonnet for Raspberry Pi
|
||||
disable onboard audio comment out `dtparam=audio=on` in `/boot/config.txt`
|
||||
> `sudo sed -i '/dtparam=audio=on/s/^/#/g' /boot/config.txt`
|
||||
|
||||
setup hifiberry-dac and i2s by adding
|
||||
```
|
||||
dtoverlay=hifiberry-dac
|
||||
dtoverlay=i2s-mmap
|
||||
```
|
||||
to `/boot/config.txt`
|
||||
> `printf "dtoverlay=hifiberry-dac\ndtoverlay=i2s-mmap" | sudo tee -a /boot/config.txt`
|
||||
|
||||
create `/etc/asound.conf` file with following content:
|
||||
```
|
||||
pcm.speakerbonnet {
|
||||
type hw card 0
|
||||
}
|
||||
|
||||
pcm.dmixer {
|
||||
type dmix
|
||||
ipc_key 1024
|
||||
ipc_perm 0666
|
||||
slave {
|
||||
pcm "speakerbonnet"
|
||||
period_time 0
|
||||
period_size 1024
|
||||
buffer_size 8192
|
||||
rate 44100
|
||||
channels 2
|
||||
}
|
||||
}
|
||||
|
||||
ctl.dmixer {
|
||||
type hw card 0
|
||||
}
|
||||
|
||||
pcm.softvol {
|
||||
type softvol
|
||||
slave.pcm "dmixer"
|
||||
control.name "PCM"
|
||||
control.card 0
|
||||
}
|
||||
|
||||
ctl.softvol {
|
||||
type hw card 0
|
||||
}
|
||||
|
||||
pcm.!default {
|
||||
type plug
|
||||
slave.pcm "softvol"
|
||||
}
|
||||
```
|
||||
|
||||
After setup, reboot system.
|
||||
> `sudo reboot`
|
||||
|
||||
## Planned features
|
||||
|
||||
- status LEDs
|
||||
- indicators for UPS HAT
|
||||
- WiFi Hotspot (https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection)
|
3
application/.gitignore
vendored
Normal file
3
application/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
bin
|
||||
venv
|
||||
__pycache__
|
108
application/ExtendedMFRC522.py
Normal file
108
application/ExtendedMFRC522.py
Normal file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "ExtendedMFRC522"
|
||||
|
||||
from mfrc522 import SimpleMFRC522
|
||||
|
||||
|
||||
class ExtendedMFRC522(SimpleMFRC522):
|
||||
|
||||
def __init__(self, start_section=1, sections=0, blocks_per_section=4, block_size=16, encoding='utf8'):
|
||||
self.START_SECTION = start_section
|
||||
self.SECTIONS = sections
|
||||
self.BLOCKS_PER_SECTIONS = blocks_per_section
|
||||
self.DATA_BLOCKS = self.BLOCKS_PER_SECTIONS - 1
|
||||
self.BLOCK_SIZE = block_size
|
||||
self.ENCODING = encoding
|
||||
if self.START_SECTION < 1:
|
||||
self.START_SECTION = 1
|
||||
super().__init__()
|
||||
|
||||
def read_no_block(self):
|
||||
(status, size) = self.READER.MFRC522_Request(self.READER.PICC_REQIDL)
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
(status, uid) = self.READER.MFRC522_Anticoll()
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
id = self.uid_to_num(uid)
|
||||
self.READER.MFRC522_SelectTag(uid)
|
||||
data = bytearray()
|
||||
|
||||
start_section = self.START_SECTION
|
||||
if start_section > size:
|
||||
start_section = size - 1
|
||||
|
||||
sections = size - start_section
|
||||
if self.SECTIONS > 0 and self.SECTIONS <= sections:
|
||||
sections = self.SECTIONS
|
||||
|
||||
for section in range(start_section, start_section + sections):
|
||||
trailer_block = section * self.BLOCKS_PER_SECTIONS + self.DATA_BLOCKS
|
||||
status = self.READER.MFRC522_Auth(
|
||||
self.READER.PICC_AUTHENT1A, trailer_block, self.KEY, uid)
|
||||
if status == self.READER.MI_OK:
|
||||
for i in range(self.DATA_BLOCKS):
|
||||
block_addr = section * self.BLOCKS_PER_SECTIONS + i
|
||||
block = self.READER.MFRC522_Read(block_addr)
|
||||
if block:
|
||||
data.extend(bytearray(block))
|
||||
else:
|
||||
return None, None
|
||||
|
||||
text_read = ''
|
||||
if data:
|
||||
while data and data[0] == 0:
|
||||
data.pop(0)
|
||||
while data and data[-1] == 0:
|
||||
data.pop()
|
||||
if data:
|
||||
text_read = data.decode(self.ENCODING)
|
||||
self.READER.MFRC522_StopCrypto1()
|
||||
return id, text_read
|
||||
|
||||
def write_no_block(self, text):
|
||||
(status, size) = self.READER.MFRC522_Request(self.READER.PICC_REQIDL)
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
(status, uid) = self.READER.MFRC522_Anticoll()
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
id = self.uid_to_num(uid)
|
||||
self.READER.MFRC522_SelectTag(uid)
|
||||
|
||||
start_section = self.START_SECTION
|
||||
if start_section > size:
|
||||
start_section = size - 1
|
||||
|
||||
sections = size - start_section
|
||||
if self.SECTIONS > 0 and self.SECTIONS <= sections:
|
||||
sections = self.SECTIONS
|
||||
|
||||
data = text.strip().encode(self.ENCODING)
|
||||
data_sections = [data[i:i + self.BLOCK_SIZE * self.DATA_BLOCKS]
|
||||
for i in range(0, len(data), self.BLOCK_SIZE * self.DATA_BLOCKS)]
|
||||
|
||||
for section in range(start_section, start_section + sections):
|
||||
trailer_block = section * self.BLOCKS_PER_SECTIONS + self.DATA_BLOCKS
|
||||
status = self.READER.MFRC522_Auth(
|
||||
self.READER.PICC_AUTHENT1A, trailer_block, self.KEY, uid)
|
||||
self.READER.MFRC522_Read(trailer_block)
|
||||
|
||||
section_data = bytearray(self.DATA_BLOCKS * self.BLOCK_SIZE)
|
||||
if len(data_sections) > (section - start_section):
|
||||
section_data = bytearray(
|
||||
data_sections[section - start_section])
|
||||
section_data.extend(
|
||||
bytearray(self.DATA_BLOCKS * self.BLOCK_SIZE - len(section_data)))
|
||||
|
||||
if status == self.READER.MI_OK:
|
||||
for i in range(self.DATA_BLOCKS):
|
||||
block_addr = section * self.BLOCKS_PER_SECTIONS + i
|
||||
block_data = section_data[(
|
||||
i*self.BLOCK_SIZE):(i+1)*self.BLOCK_SIZE]
|
||||
self.READER.MFRC522_Write(
|
||||
block_addr, block_data)
|
||||
self.READER.MFRC522_StopCrypto1()
|
||||
return id, data[0:(len(self.BLOCK_ADDRS) * self.BLOCK_SIZE * sections)].decode(self.ENCODING)
|
176
application/api.py
Normal file
176
application/api.py
Normal file
@ -0,0 +1,176 @@
|
||||
from functools import wraps
|
||||
from flask import Blueprint, request, abort, redirect, url_for, jsonify
|
||||
from urllib.parse import urlencode
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
from luniebox import luniebox
|
||||
from spotifydl import SpotifyDLStatus
|
||||
import util
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
|
||||
|
||||
def api_key_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not request.headers.has_key('Authorization') or request.headers.get('Authorization') != luniebox.get_setting('API', 'API_KEY'):
|
||||
abort(401)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@api.route("/")
|
||||
@api_key_required
|
||||
def index():
|
||||
return jsonify("test")
|
||||
|
||||
|
||||
@api.route("/setup", methods=['POST'])
|
||||
def setup():
|
||||
spotify_username = request.form.get('spotify-username')
|
||||
spotify_password = request.form.get('spotify-password')
|
||||
spotify_client_id = request.form.get('spotify-client-id')
|
||||
spotify_client_secret = request.form.get('spotify-client-secret')
|
||||
spotify_redirect_uri = request.form.get('spotify-redirect-uri')
|
||||
spotify_device_name = request.form.get('spotify-device-name')
|
||||
luniebox.set_setting('luniebox', 'setup', True)
|
||||
luniebox.set_setting('spotify', 'client_id', spotify_client_id)
|
||||
luniebox.set_setting('spotify', 'client_secret', spotify_client_secret)
|
||||
luniebox.set_setting('spotify', 'redirect_uri', spotify_redirect_uri)
|
||||
|
||||
luniebox.spotify.set_setting('username', spotify_username)
|
||||
luniebox.spotify.set_setting('password', spotify_password)
|
||||
luniebox.spotify.set_setting('device_name', spotify_device_name)
|
||||
|
||||
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
|
||||
|
||||
return redirect(url_for('api.spotify_authorize'))
|
||||
|
||||
|
||||
spotify_authorize_state = False
|
||||
|
||||
|
||||
@api.route("/spotify/authorize", methods=['GET'])
|
||||
def spotify_authorize():
|
||||
client_id = luniebox.get_setting('spotify', 'client_id')
|
||||
redirect_uri = luniebox.get_setting('spotify', 'redirect_uri')
|
||||
global spotify_authorize_state
|
||||
spotify_authorize_state = util.randomString(16)
|
||||
scope = 'user-read-playback-state user-modify-playback-state user-read-private'
|
||||
return redirect('https://accounts.spotify.com/authorize?' +
|
||||
urlencode({
|
||||
'response_type': 'code',
|
||||
'client_id': client_id,
|
||||
'scope': scope,
|
||||
'redirect_uri': redirect_uri,
|
||||
'state': spotify_authorize_state
|
||||
}))
|
||||
|
||||
|
||||
@api.route("/spotify/callback", methods=['GET'])
|
||||
def spotify_callback():
|
||||
code = request.args['code']
|
||||
returnedState = request.args['state']
|
||||
global spotify_authorize_state
|
||||
if not returnedState or returnedState != spotify_authorize_state:
|
||||
abort(403)
|
||||
|
||||
luniebox.spotify.new_access_token(code)
|
||||
|
||||
return redirect(url_for('pages.index'))
|
||||
|
||||
|
||||
@api.route("/play", methods=['GET'])
|
||||
def play():
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
uri = request.args['uri']
|
||||
luniebox.play(uri)
|
||||
return redirect(url_for('pages.success', play=uri))
|
||||
|
||||
|
||||
@api.route("/spotify/dl", methods=['GET'])
|
||||
def spotify_dl():
|
||||
if luniebox.spotifydl_connect():
|
||||
uri = request.args['uri']
|
||||
dlStatus = luniebox.spotifydl.download(uri)
|
||||
return redirect(url_for('pages.spotifydl', status=dlStatus, uri=uri))
|
||||
else:
|
||||
return redirect(url_for('pages.spotifydl', status=SpotifyDLStatus.DISABLED))
|
||||
|
||||
|
||||
@api.route("/spotify/downloads", methods=['GET'])
|
||||
def spotify_downloads():
|
||||
if luniebox.spotifydl_connect():
|
||||
return jsonify(luniebox.spotifydl.getDownloads())
|
||||
else:
|
||||
return redirect(url_for('pages.error', error='spotifydownloads'))
|
||||
|
||||
|
||||
@api.route("/rfid/write", methods=['GET'])
|
||||
def rfid_write():
|
||||
value = request.args['value']
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
luniebox.rfid_write(value)
|
||||
logging.getLogger('luniebox').info("Write to RFID: " + value)
|
||||
return redirect(url_for('pages.success', rfid_write=value))
|
||||
|
||||
|
||||
@api.route("/rfid/read", methods=['GET'])
|
||||
def rfid_read():
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
value = luniebox.rfid_readOnce()
|
||||
return redirect(url_for('pages.success', rfid_read=value))
|
||||
|
||||
|
||||
@api.route("/rfid/play", methods=['GET'])
|
||||
def rfid_play():
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
value = luniebox.rfid_readOnce()
|
||||
if luniebox.play(value):
|
||||
return redirect(url_for('pages.success', rfid_play=value))
|
||||
else:
|
||||
return redirect(url_for('pages.error', error='play'))
|
||||
|
||||
|
||||
@api.route("/spotifyd/restart", methods=['GET'])
|
||||
def restart_spotifyd():
|
||||
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
|
||||
return redirect(url_for('pages.success', restart_spotifyd=True))
|
||||
|
||||
|
||||
@api.route("/mpd/restart", methods=['GET'])
|
||||
def restart_mpd():
|
||||
subprocess.run(["sudo", "systemctl", "restart", "mpd"])
|
||||
return redirect(url_for('pages.success', restart_mpd=True))
|
||||
|
||||
|
||||
@api.route("/mpd/list", methods=['GET'])
|
||||
def mpd_list():
|
||||
if 'path' in request.args:
|
||||
results = luniebox.mpd_list(request.args['path'])
|
||||
else:
|
||||
results = luniebox.mpd_list()
|
||||
return jsonify(results)
|
||||
|
||||
|
||||
@api.route("/daemon/start", methods=['GET'])
|
||||
def daemon_start():
|
||||
subprocess.run(["sudo", "systemctl", "start", "luniebox-daemon"])
|
||||
return redirect(url_for('pages.success', start_daemon=True))
|
||||
|
||||
|
||||
@api.route("/daemon/stop", methods=['GET'])
|
||||
def daemon_stop():
|
||||
subprocess.run(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
return redirect(url_for('pages.success', stop_daemon=True))
|
||||
|
||||
|
||||
@api.route("/restart", methods=['GET'])
|
||||
def restart_app():
|
||||
subprocess.run(["sudo", "systemctl", "restart", "luniebox-app"])
|
||||
return redirect(url_for('pages.success', restart_luniebox=True))
|
31
application/app.py
Normal file
31
application/app.py
Normal file
@ -0,0 +1,31 @@
|
||||
import sys
|
||||
from flask import Flask
|
||||
|
||||
import logging
|
||||
from luniebox import luniebox
|
||||
from api import api
|
||||
from pages import pages
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(api, url_prefix='/api')
|
||||
app.register_blueprint(pages)
|
||||
|
||||
loglevel = 'INFO'
|
||||
if luniebox.get_setting('logging', 'level'):
|
||||
loglevel = luniebox.get_setting('logging', 'level')
|
||||
|
||||
logger = logging.getLogger('luniebox')
|
||||
logger.setLevel(logging._nameToLevel[loglevel])
|
||||
logFormatter = logging.Formatter(
|
||||
style='{', datefmt='%Y-%m-%d %H:%M:%S', fmt='{asctime} {levelname}: {message}')
|
||||
logstdoutHandler = logging.StreamHandler(sys.stdout)
|
||||
logstdoutHandler.setFormatter(logFormatter)
|
||||
logger.addHandler(logstdoutHandler)
|
||||
|
||||
if __name__ != '__main__':
|
||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||
app.logger.handlers = gunicorn_logger.handlers
|
||||
app.logger.setLevel(gunicorn_logger.level)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0')
|
107
application/daemon.py
Normal file
107
application/daemon.py
Normal file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import signal
|
||||
import time
|
||||
import sys
|
||||
import logging
|
||||
from luniebox import luniebox
|
||||
import RPi.GPIO as GPIO
|
||||
from mpu9250_jmdev.registers import *
|
||||
from mpu9250_jmdev.mpu_9250 import MPU9250
|
||||
|
||||
|
||||
class LunieboxDaemon(object):
|
||||
|
||||
def __init__(self, luniebox, input1=7, input2=8, skip_thresh=0.5, wind_thresh=0.3):
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
self.luniebox = luniebox
|
||||
self.input1 = input1
|
||||
self.input2 = input2
|
||||
self.skip_thresh = skip_thresh
|
||||
self.wind_thresh = wind_thresh
|
||||
GPIO.setup(self.input1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
GPIO.setup(self.input2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
self.tolerance = self.luniebox.get_setting(
|
||||
'rfid', 'pause_tolerance', 4)
|
||||
self.reads = 0
|
||||
|
||||
if self.luniebox.get_setting('hardware', 'mpu') == 'True':
|
||||
self.mpu = MPU9250(
|
||||
address_ak=AK8963_ADDRESS,
|
||||
address_mpu_master=MPU9050_ADDRESS_68,
|
||||
address_mpu_slave=None,
|
||||
bus=4,
|
||||
gfs=GFS_1000,
|
||||
afs=AFS_8G,
|
||||
mfs=AK8963_BIT_16,
|
||||
mode=AK8963_MODE_C100HZ)
|
||||
self.mpu.configure()
|
||||
else:
|
||||
self.mpu = False
|
||||
|
||||
def run(self):
|
||||
logging.getLogger('luniebox').info("run luniebox")
|
||||
|
||||
while True:
|
||||
# mpu
|
||||
if self.mpu:
|
||||
acc = self.mpu.readAccelerometerMaster()
|
||||
rot_x = acc[0]
|
||||
rot_y = acc[1]
|
||||
|
||||
if rot_x > self.skip_thresh:
|
||||
self.luniebox.previous()
|
||||
time.sleep(1)
|
||||
elif rot_x < (self.skip_thresh * -1):
|
||||
self.luniebox.next()
|
||||
time.sleep(1)
|
||||
|
||||
if rot_y < (self.wind_thresh * -1):
|
||||
self.luniebox.fastforward()
|
||||
time.sleep(0.1)
|
||||
elif rot_y > self.wind_thresh:
|
||||
self.luniebox.rewind()
|
||||
time.sleep(0.1)
|
||||
|
||||
# buttons
|
||||
down_state = GPIO.input(self.input1)
|
||||
if down_state == False:
|
||||
self.luniebox.vol_down()
|
||||
time.sleep(0.1)
|
||||
|
||||
up_state = GPIO.input(self.input2)
|
||||
if up_state == False:
|
||||
self.luniebox.vol_up()
|
||||
time.sleep(0.1)
|
||||
|
||||
# rfid
|
||||
id, text = self.luniebox.reader.read_no_block()
|
||||
if text != None:
|
||||
text = text.strip()
|
||||
|
||||
if text == None:
|
||||
self.reads += 1
|
||||
if self.reads >= self.tolerance:
|
||||
self.reads = 0
|
||||
self.luniebox.pause()
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
self.reads = 0
|
||||
self.luniebox.play(text)
|
||||
time.sleep(0.1)
|
||||
|
||||
def signal_handler(self, signal, frame):
|
||||
logging.getLogger('luniebox').info(
|
||||
"Caught signal {}, exiting...".format(signal))
|
||||
luniebox.stop()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
daemon = LunieboxDaemon(luniebox=luniebox)
|
||||
signal.signal(signal.SIGINT, daemon.signal_handler)
|
||||
signal.signal(signal.SIGTERM, daemon.signal_handler)
|
||||
|
||||
daemon.run()
|
378
application/luniebox.py
Normal file
378
application/luniebox.py
Normal file
@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "Luniebox"
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
from enum import Enum
|
||||
from configparser import ConfigParser
|
||||
import RPi.GPIO as GPIO
|
||||
from spotifydl import SpotifyDL, SpotifyDLStatus
|
||||
from mpd import MPDClient
|
||||
from ExtendedMFRC522 import ExtendedMFRC522
|
||||
|
||||
from spotify import Spotify
|
||||
import util
|
||||
|
||||
|
||||
defaultConfigFilePath = '../config/luniebox.cfg'
|
||||
|
||||
|
||||
class PlayerService(Enum):
|
||||
NONE = 0
|
||||
SPOTIFY = 1
|
||||
MPD = 2
|
||||
|
||||
|
||||
class Luniebox(object):
|
||||
|
||||
def __init__(self, configFilePath=defaultConfigFilePath):
|
||||
self.configFilePath = configFilePath
|
||||
self.read_config()
|
||||
|
||||
GPIO.setwarnings(False)
|
||||
self.reader = ExtendedMFRC522(encoding="ascii")
|
||||
|
||||
loglevel = 'INFO'
|
||||
if self.get_setting('logging', 'level'):
|
||||
loglevel = self.get_setting('logging', 'level')
|
||||
|
||||
logger = logging.getLogger('luniebox')
|
||||
logger.setLevel(logging._nameToLevel[loglevel])
|
||||
logFormatter = logging.Formatter(
|
||||
style='{', datefmt='%Y-%m-%d %H:%M:%S', fmt='{asctime} {levelname}: {message}')
|
||||
logstdoutHandler = logging.StreamHandler(sys.stdout)
|
||||
logstdoutHandler.setFormatter(logFormatter)
|
||||
logger.addHandler(logstdoutHandler)
|
||||
|
||||
if self.get_setting('mpd', 'disabled', '') != 'True':
|
||||
self.mpd = MPDClient()
|
||||
self.mpd.host = "localhost"
|
||||
self.mpd.port = 6600
|
||||
self.mpd.timeout = 10
|
||||
if self.mpd_connect():
|
||||
logging.getLogger('luniebox').debug("connected to mpd")
|
||||
else:
|
||||
logging.getLogger('luniebox').info("mpd disabled")
|
||||
self.mpd = False
|
||||
|
||||
if self.get_setting('spotify', 'disabled', '') != 'True':
|
||||
self.spotify = Spotify(luniebox=self)
|
||||
if self.spotify_connect():
|
||||
logging.getLogger('luniebox').debug("connected to spotify")
|
||||
|
||||
self.zspotify_path = False
|
||||
self.spotifydl = False
|
||||
self.zspotify_path = self.get_setting('spotify', 'zspotify_path')
|
||||
if self.zspotify_path and self.mpd:
|
||||
self.spotifydl_connect()
|
||||
|
||||
else:
|
||||
logging.getLogger('luniebox').info("spotify disabled")
|
||||
self.spotify = False
|
||||
|
||||
if not self.config.has_option('api', 'api_key'):
|
||||
self.set_setting('api', 'api_key', util.randomString(64))
|
||||
|
||||
self.current = self.get_setting('luniebox', 'current')
|
||||
|
||||
self.service = PlayerService.NONE
|
||||
|
||||
if self.current != None:
|
||||
if self.current.startswith("spotify:"):
|
||||
self.service = PlayerService.SPOTIFY
|
||||
elif self.current.startswith("mpd:"):
|
||||
self.service = PlayerService.MPD
|
||||
|
||||
self.volume_max = int(self.get_setting('luniebox', 'volume_max', 100))
|
||||
self.volume = int(self.get_setting(
|
||||
'luniebox', 'volume', self.volume_max))
|
||||
self.volume_step = int(self.get_setting('luniebox', 'volume_step', 5))
|
||||
self.wind_step = int(self.get_setting('luniebox', 'wind_step', 10))
|
||||
self.resume = True
|
||||
|
||||
def mpd_connect(self):
|
||||
if not self.get_setting('luniebox', 'setup'):
|
||||
return False
|
||||
|
||||
if not self.mpd:
|
||||
logging.getLogger('luniebox').warn("mpd disabled!")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.mpd.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
tries = 0
|
||||
while tries < 5:
|
||||
try:
|
||||
self.mpd.connect(self.mpd.host, self.mpd.port)
|
||||
return True
|
||||
except:
|
||||
subprocess.call(["systemctl", "restart", "mpd"])
|
||||
time.sleep(1)
|
||||
tries += 1
|
||||
|
||||
logging.getLogger('luniebox').error(
|
||||
"Could not connect to MPD service!")
|
||||
return False
|
||||
|
||||
def spotify_connect(self, restart=False, max_tries=5):
|
||||
if not self.get_setting('luniebox', 'setup'):
|
||||
return False
|
||||
|
||||
if not self.spotify:
|
||||
logging.getLogger('luniebox').warn("spotify disabled!")
|
||||
return False
|
||||
|
||||
if restart:
|
||||
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
|
||||
|
||||
tries = 0
|
||||
spotifyd_status = subprocess.call(
|
||||
["systemctl", "is-active", "--quiet", "spotifyd"])
|
||||
|
||||
while spotifyd_status != 0 and tries < max_tries:
|
||||
subprocess.call(["systemctl", "restart", "spotifyd"])
|
||||
time.sleep(1)
|
||||
tries += 1
|
||||
spotifyd_status = subprocess.call(
|
||||
["systemctl", "is-active", "--quiet", "spotifyd"])
|
||||
|
||||
if spotifyd_status == 0:
|
||||
return True
|
||||
|
||||
logging.getLogger('luniebox').error("spotifyd service not running!")
|
||||
return False
|
||||
|
||||
def spotifydl_connect(self):
|
||||
if not self.get_setting('luniebox', 'setup'):
|
||||
return False
|
||||
|
||||
if self.spotifydl:
|
||||
return True
|
||||
|
||||
if not self.zspotify_path or not self.mpd:
|
||||
logging.getLogger('luniebox').warn("spotifydl disabled!")
|
||||
return False
|
||||
|
||||
username = self.spotify.get_setting("username")
|
||||
password = self.spotify.get_setting("password")
|
||||
root = self.get_setting('mpd', 'library_path')
|
||||
try:
|
||||
self.spotifydl = SpotifyDL(
|
||||
self.zspotify_path, username, password, root)
|
||||
logging.getLogger('luniebox').info("spotifydl enabled!")
|
||||
return True
|
||||
except Exception as ex:
|
||||
logging.getLogger('luniebox').warning(
|
||||
"error on setup spotifydl: " + str(ex))
|
||||
return False
|
||||
|
||||
def read_config(self):
|
||||
configParser = ConfigParser()
|
||||
dataset = configParser.read(self.configFilePath)
|
||||
if len(dataset) != 1:
|
||||
raise ValueError(
|
||||
"Config file {} not found!".format(self.configFilePath))
|
||||
self.config = configParser
|
||||
|
||||
def get_setting(self, section, key, default=None):
|
||||
if self.config.has_option(section, key):
|
||||
return self.config[section][key]
|
||||
return default
|
||||
|
||||
def set_setting(self, section, key, value):
|
||||
if not self.config.has_section(section):
|
||||
self.config.add_section(section)
|
||||
self.config.set(section, key, str(value))
|
||||
|
||||
with open(self.configFilePath, 'w') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def rfid_write(self, data):
|
||||
self.reader.write(data.strip())
|
||||
|
||||
def rfid_readOnce(self):
|
||||
id, text = self.reader.read()
|
||||
logging.getLogger('luniebox').debug(
|
||||
"Once ID: %s Text: %s" % (id, text))
|
||||
return text.strip()
|
||||
|
||||
def vol_up(self):
|
||||
self.change_volume(self.volume_step)
|
||||
|
||||
def vol_down(self):
|
||||
self.change_volume(-1 * self.volume_step)
|
||||
|
||||
def change_volume(self, change):
|
||||
self.volume += change
|
||||
|
||||
if self.volume > self.volume_max:
|
||||
self.volume = self.volume_max
|
||||
|
||||
if self.volume < 0:
|
||||
self.volume = 0
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.volume(self.volume)
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.setvol(self.volume)
|
||||
|
||||
self.set_setting('luniebox', 'volume', str(self.volume))
|
||||
logging.getLogger('luniebox').debug("vol: " + str(self.volume))
|
||||
|
||||
def fastforward(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('seek ff')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.fastforward(self.wind_step)
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.seekcur(self.wind_step)
|
||||
|
||||
def rewind(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('seek rew')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.rewind(self.wind_step)
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.seekcur(-1 * self.wind_step)
|
||||
|
||||
def next(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('next')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.next()
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.next()
|
||||
|
||||
def previous(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('prev')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.previous()
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.previous()
|
||||
|
||||
def pause(self):
|
||||
if not self.resume:
|
||||
if self.mpd_connect():
|
||||
self.mpd.pause(1)
|
||||
logging.getLogger('luniebox').debug("pause mpd")
|
||||
if self.spotify_connect(max_tries=2):
|
||||
self.spotify.pause()
|
||||
logging.getLogger('luniebox').debug("pause spotify")
|
||||
|
||||
self.resume = True
|
||||
|
||||
def play(self, text):
|
||||
if text != "":
|
||||
if text.startswith("spotify:"):
|
||||
self.service = PlayerService.SPOTIFY
|
||||
elif text.startswith("mpd:"):
|
||||
self.service = PlayerService.MPD
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotifydl_connect():
|
||||
downloadStatus = self.spotifydl.downloadStatus(text)
|
||||
if downloadStatus == SpotifyDLStatus.FINISHED:
|
||||
self.mpd.update(text.replace('mpd:', ''))
|
||||
self.service = PlayerService.MPD
|
||||
elif self.get_setting('spotify', 'auto_download') == 'True' and downloadStatus == SpotifyDLStatus.NONE or downloadStatus == SpotifyDLStatus.ERROR:
|
||||
self.spotifydl.download(text)
|
||||
|
||||
if self.service == PlayerService.SPOTIFY:
|
||||
if text != self.current:
|
||||
if self.spotify_connect():
|
||||
self.spotify.volume(self.volume)
|
||||
if self.spotify.play(text):
|
||||
self.current = text
|
||||
self.set_setting(
|
||||
'luniebox', 'current', self.current)
|
||||
self.resume = False
|
||||
logging.getLogger('luniebox').debug(
|
||||
"play spotify: " + self.current)
|
||||
else:
|
||||
logging.getLogger('luniebox').warn(
|
||||
"cannot play spotify: " + self.current)
|
||||
elif self.resume and text == self.current:
|
||||
if self.spotify_connect():
|
||||
self.spotify.volume(self.volume)
|
||||
play = self.current
|
||||
if self.spotify.is_active():
|
||||
play = None
|
||||
if self.spotify.play(play):
|
||||
self.resume = False
|
||||
logging.getLogger('luniebox').debug(
|
||||
"resume spotify: " + self.current)
|
||||
else:
|
||||
logging.getLogger('luniebox').warn(
|
||||
"cannot resume spotify: " + self.current)
|
||||
elif self.service == PlayerService.MPD:
|
||||
if text != self.current:
|
||||
if self.mpd_connect():
|
||||
self.mpd.setvol(self.volume)
|
||||
self.mpd.clear()
|
||||
text = text.replace('mpd:', '')
|
||||
self.mpd.add(text)
|
||||
self.mpd.play()
|
||||
self.current = text
|
||||
self.set_setting('luniebox', 'current', self.current)
|
||||
self.resume = False
|
||||
if text.startswith('spotify:'):
|
||||
logging.getLogger('luniebox').debug(
|
||||
"play spotify from mpd: " + text)
|
||||
else:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"play mpd: " + self.current)
|
||||
elif self.resume and text == self.current:
|
||||
if self.mpd_connect():
|
||||
self.mpd.setvol(self.volume)
|
||||
self.mpd.play()
|
||||
self.resume = False
|
||||
if text.startswith('spotify:'):
|
||||
logging.getLogger('luniebox').debug(
|
||||
"resume spotify from mpd: " + text)
|
||||
else:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"resume mpd: " + self.current)
|
||||
|
||||
elif text != None:
|
||||
logging.getLogger('luniebox').info(
|
||||
"invalid value(?): " + str(text))
|
||||
|
||||
def stop(self):
|
||||
self.pause()
|
||||
|
||||
def sort_mpd(self, object):
|
||||
if 'directory' in object:
|
||||
return object['directory']
|
||||
elif 'file' in object:
|
||||
return object['file']
|
||||
return ''
|
||||
|
||||
def mpd_list(self, path=''):
|
||||
if self.mpd_connect():
|
||||
try:
|
||||
result = self.mpd.listfiles(path)
|
||||
return sorted(result, key=self.sort_mpd)
|
||||
except:
|
||||
return []
|
||||
return None
|
||||
|
||||
def mpd_status(self):
|
||||
if self.mpd_connect():
|
||||
status = self.mpd.status()
|
||||
status['song'] = self.mpd.currentsong()
|
||||
return status
|
||||
return None
|
||||
|
||||
|
||||
luniebox = Luniebox()
|
189
application/pages.py
Normal file
189
application/pages.py
Normal file
@ -0,0 +1,189 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request
|
||||
from functools import wraps
|
||||
import subprocess
|
||||
|
||||
from luniebox import luniebox
|
||||
|
||||
pages = Blueprint('pages', __name__)
|
||||
|
||||
|
||||
def setup_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not luniebox.get_setting('luniebox', 'setup'):
|
||||
return redirect(url_for('pages.setup'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@pages.route("/setup")
|
||||
def setup():
|
||||
model = {}
|
||||
|
||||
if luniebox.get_setting('luniebox', 'setup'):
|
||||
model['setup'] = True
|
||||
|
||||
model['client-id'] = luniebox.get_setting('spotify', 'client_id')
|
||||
model['client-secret'] = luniebox.get_setting('spotify', 'client_secret')
|
||||
model['redirect-uri'] = luniebox.get_setting('spotify', 'redirect_uri')
|
||||
|
||||
model['username'] = luniebox.spotify.get_setting('username', '')
|
||||
model['password'] = luniebox.spotify.get_setting('password', '')
|
||||
model['device-name'] = luniebox.spotify.get_setting('device_name', '')
|
||||
|
||||
model['host'] = request.host_url
|
||||
|
||||
if model['host'].endswith("/"):
|
||||
model['host'] = model['host'][:len(model['host']) - 1]
|
||||
|
||||
if not model['redirect-uri']:
|
||||
model['redirect-uri'] = model['host'] + url_for('api.spotify_callback')
|
||||
|
||||
return render_template('setup.html', model=model)
|
||||
|
||||
|
||||
@pages.route("/")
|
||||
@setup_required
|
||||
def index():
|
||||
devices = luniebox.spotify.devices()
|
||||
device_id = luniebox.get_setting('spotify', 'device_id')
|
||||
device_name = luniebox.spotify.get_setting('device_name')
|
||||
|
||||
mpdStatus = luniebox.mpd_status()
|
||||
|
||||
spotifyStatus = {
|
||||
'status': False,
|
||||
'device_name': device_name,
|
||||
'device_id': device_id,
|
||||
'errors': {'setup': True, 'not_running': True}
|
||||
}
|
||||
|
||||
if not device_id:
|
||||
spotifyStatus['errors']['not_running'] = False
|
||||
for device in devices['devices']:
|
||||
if device['name'] == device_name:
|
||||
device_id = device['id']
|
||||
luniebox.set_setting('spotify', 'device_id', device_id)
|
||||
spotifyStatus['errors']['setup'] = False
|
||||
else:
|
||||
spotifyStatus['errors']['setup'] = False
|
||||
for device in devices['devices']:
|
||||
if device['id'] == device_id:
|
||||
spotifyStatus['errors']['not_running'] = False
|
||||
|
||||
if spotifyStatus['errors']['not_running'] and luniebox.spotify.transfer_playback():
|
||||
spotifyStatus['errors']['not_running'] = False
|
||||
|
||||
if not spotifyStatus['errors']['setup'] and not spotifyStatus['errors']['not_running']:
|
||||
spotifyStatus['status'] = luniebox.spotify.playback_state()
|
||||
|
||||
daemon_error = False
|
||||
daemon_status = subprocess.call(
|
||||
["systemctl", "is-active", "--quiet", "luniebox-daemon"])
|
||||
|
||||
if daemon_status == 0:
|
||||
daemon_status = "Running"
|
||||
else:
|
||||
daemon_error = "Not running"
|
||||
daemon_status = False
|
||||
|
||||
return render_template('index.html', spotify=spotifyStatus, mpd=mpdStatus, daemon_status=daemon_status, daemon_error=daemon_error)
|
||||
|
||||
|
||||
@pages.route("/success")
|
||||
@setup_required
|
||||
def success():
|
||||
success = {}
|
||||
if 'play' in request.args:
|
||||
success['play'] = request.args['play']
|
||||
|
||||
if 'spotify_dl' in request.args:
|
||||
success['spotify_dl'] = request.args['spotify_dl']
|
||||
|
||||
if 'restart_spotifyd' in request.args:
|
||||
success['restart_spotifyd'] = request.args['restart_spotifyd']
|
||||
|
||||
if 'restart_mpd' in request.args:
|
||||
success['restart_mpd'] = request.args['restart_mpd']
|
||||
|
||||
if 'start_daemon' in request.args:
|
||||
success['start_daemon'] = request.args['start_daemon']
|
||||
|
||||
if 'stop_daemon' in request.args:
|
||||
success['stop_daemon'] = request.args['stop_daemon']
|
||||
|
||||
if 'rfid_play' in request.args:
|
||||
success['rfid_play'] = request.args['rfid_play']
|
||||
|
||||
if 'rfid_read' in request.args:
|
||||
success['rfid_read'] = request.args['rfid_read']
|
||||
|
||||
if 'rfid_write' in request.args:
|
||||
success['rfid_write'] = request.args['rfid_write']
|
||||
|
||||
return render_template('success.html', success=success)
|
||||
|
||||
|
||||
@pages.route("/error")
|
||||
@setup_required
|
||||
def error():
|
||||
error = False
|
||||
if 'error' in request.args:
|
||||
error = request.args['error']
|
||||
|
||||
return render_template('error.html', error=error)
|
||||
|
||||
|
||||
@pages.route("/spotify/search", methods=['GET', 'POST'])
|
||||
def spotify_search():
|
||||
query = '' if not 'q' in request.args else request.args['q']
|
||||
types = [
|
||||
'track', 'album'] if not 'type' in request.args else request.args.getlist('type')
|
||||
offset = 0 if not 'offset' in request.args else request.args['offset']
|
||||
|
||||
limit = 5
|
||||
if not 'limit' in request.args:
|
||||
if len(types) == 1:
|
||||
limit = 20
|
||||
if len(types) == 2:
|
||||
limit = 10
|
||||
else:
|
||||
limit = int(request.args['limit'])
|
||||
|
||||
results = False if not query else luniebox.spotify.search(
|
||||
query, types, limit, offset)
|
||||
|
||||
if results:
|
||||
for result in results.values():
|
||||
if 'items' in result:
|
||||
for item in result['items']:
|
||||
if luniebox.spotifydl_connect():
|
||||
item['spotifydl'] = str(
|
||||
luniebox.spotifydl.downloadStatus(item['uri']))
|
||||
|
||||
return render_template('spotify_search.html', results=results, query=query, types=types, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@pages.route("/spotify/download", methods=['GET'])
|
||||
def spotifydl():
|
||||
status = False
|
||||
if 'status' in request.args:
|
||||
status = request.args['status']
|
||||
uri = False
|
||||
if 'uri' in request.args:
|
||||
uri = request.args['uri']
|
||||
return render_template('spotifydl.html', status=status, uri=uri)
|
||||
|
||||
|
||||
@pages.route("/mpd/list", methods=['GET'])
|
||||
def mpd_list():
|
||||
path = ''
|
||||
spotify = False
|
||||
if 'path' in request.args:
|
||||
path = request.args['path']
|
||||
|
||||
if 'spotify' in request.args:
|
||||
spotify = True
|
||||
|
||||
results = luniebox.mpd_list(path)
|
||||
return render_template('mpd.html', results=results, path=path, spotify=spotify)
|
9
application/requirements.txt
Normal file
9
application/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
||||
DateTime==4.3
|
||||
Flask==2.0.2
|
||||
gunicorn==20.1.0
|
||||
mfrc522==0.0.7
|
||||
mpu9250-jmdev==1.0.12
|
||||
python-dateutil==2.8.2
|
||||
python-mpd2==3.0.4
|
||||
requests==2.26.0
|
||||
smbus2==0.4.1
|
385
application/spotify.py
Normal file
385
application/spotify.py
Normal file
@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "Spotify"
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
from configparser import ConfigParser
|
||||
from dateutil import parser
|
||||
from urllib.parse import urlencode
|
||||
import requests
|
||||
import json
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
|
||||
defaultSpotifyConfigFilePath = '../config/spotifyd.cfg'
|
||||
|
||||
|
||||
class Spotify(object):
|
||||
|
||||
def __init__(self, luniebox, configFilePath=defaultSpotifyConfigFilePath):
|
||||
self.luniebox = luniebox
|
||||
self.configFilePath = configFilePath
|
||||
self.read_config()
|
||||
|
||||
def read_config(self):
|
||||
configParser = ConfigParser()
|
||||
dataset = configParser.read(self.configFilePath)
|
||||
if len(dataset) != 1:
|
||||
raise ValueError(
|
||||
"Config file {} not found!".format(self.configFilePath))
|
||||
self.config = configParser
|
||||
|
||||
def get_setting(self, key, default=None):
|
||||
if self.config.has_option('global', key):
|
||||
return self.config['global'][key].strip('\"')
|
||||
return default
|
||||
|
||||
def set_setting(self, key, value):
|
||||
if not self.config.has_section('global'):
|
||||
self.config.add_section('global')
|
||||
self.config.set('global', key, str('\"' + value + '\"'))
|
||||
|
||||
with open(self.configFilePath, 'w') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def get_status(self):
|
||||
devices = self.devices()
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
device_name = self.get_setting('device_name')
|
||||
status = {
|
||||
'status': False,
|
||||
'device_name': device_name,
|
||||
'device_id': device_id,
|
||||
'errors': {'setup': True, 'not_running': True}
|
||||
}
|
||||
|
||||
if not device_id:
|
||||
status['errors']['not_running'] = False
|
||||
for device in devices['devices']:
|
||||
if device['name'] == device_name:
|
||||
device_id = device['id']
|
||||
self.luniebox.get_setting(
|
||||
'spotify', 'device_id', device_id)
|
||||
status['errors']['setup'] = False
|
||||
else:
|
||||
status['errors']['setup'] = False
|
||||
for device in devices['devices']:
|
||||
if device['id'] == device_id:
|
||||
status['errors']['not_running'] = False
|
||||
|
||||
if status['errors']['not_running'] and self.transfer_playback():
|
||||
status['errors']['not_running'] = False
|
||||
|
||||
return status
|
||||
|
||||
def get_access_token(self):
|
||||
access_token = self.luniebox.get_setting('spotify', 'access_token')
|
||||
if not access_token:
|
||||
return False
|
||||
|
||||
token_expires = parser.parse(
|
||||
self.luniebox.get_setting('spotify', 'token_expires'))
|
||||
|
||||
if token_expires < datetime.datetime.now():
|
||||
self.refresh_access_token()
|
||||
return self.get_access_token()
|
||||
|
||||
return access_token
|
||||
|
||||
def new_access_token(self, code):
|
||||
clientId = self.luniebox.get_setting('spotify', 'client_id')
|
||||
clientSecret = self.luniebox.get_setting('spotify', 'client_secret')
|
||||
redirectUri = self.luniebox.get_setting('spotify', 'redirect_uri')
|
||||
|
||||
credentials = clientId + ":" + clientSecret
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + str(base64.b64encode(credentials.encode('utf-8')), 'utf-8')
|
||||
}
|
||||
|
||||
formData = {
|
||||
'code': code,
|
||||
'redirect_uri': redirectUri,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://accounts.spotify.com/api/token', headers=headers, data=formData)
|
||||
|
||||
tokenData = response.json()
|
||||
|
||||
self.luniebox.set_setting(
|
||||
'spotify', 'access_token', tokenData['access_token'])
|
||||
self.luniebox.set_setting('spotify', 'refresh_token',
|
||||
tokenData['refresh_token'])
|
||||
self.luniebox.set_setting('spotify', 'token_expires', str(datetime.datetime.now(
|
||||
) + datetime.timedelta(seconds=tokenData['expires_in'])))
|
||||
|
||||
def refresh_access_token(self):
|
||||
clientId = self.luniebox.get_setting('spotify', 'client_id')
|
||||
clientSecret = self.luniebox.get_setting('spotify', 'client_secret')
|
||||
refresh_token = self.luniebox.get_setting('spotify', 'refresh_token')
|
||||
|
||||
if not refresh_token:
|
||||
return False
|
||||
|
||||
credentials = clientId + ":" + clientSecret
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + str(base64.b64encode(credentials.encode('utf-8')), 'utf-8')
|
||||
}
|
||||
|
||||
formData = {
|
||||
'refresh_token': refresh_token,
|
||||
'grant_type': 'refresh_token'
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://accounts.spotify.com/api/token', headers=headers, data=formData)
|
||||
|
||||
tokenData = response.json()
|
||||
|
||||
self.luniebox.set_setting(
|
||||
'spotify', 'access_token', tokenData['access_token'])
|
||||
self.luniebox.set_setting('spotify', 'token_expires', str(datetime.datetime.now(
|
||||
) + datetime.timedelta(seconds=tokenData['expires_in'])))
|
||||
|
||||
def api_call(self, url):
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
return requests.get(
|
||||
'https://api.spotify.com/v1' + url, headers=headers)
|
||||
|
||||
def search(self, query, types, limit, offset=0):
|
||||
if limit > 50:
|
||||
limit = 50
|
||||
response = self.api_call(
|
||||
'/search?' + urlencode({'q': query, 'type': ','.join(types), 'limit': limit, 'offset': offset}))
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.search(query, types, limit, offset)
|
||||
|
||||
return False
|
||||
|
||||
def playback_state(self):
|
||||
response = self.api_call('/me/player')
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
if response.status_code == 204:
|
||||
return {"not_active": True}
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.playback_state()
|
||||
|
||||
return False
|
||||
|
||||
def transfer_playback(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
data = {
|
||||
'device_ids': [device_id],
|
||||
'play': False
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player', headers=headers, data=json.dumps(data))
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.transfer_playback()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify transfer playback", response, response.text)
|
||||
return False
|
||||
|
||||
def is_active(self):
|
||||
self.transfer_playback()
|
||||
state = self.playback_state()
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
if not state:
|
||||
return False
|
||||
return 'device' in state and 'id' in state['device'] and state['device']['id'] == device_id
|
||||
|
||||
def devices(self):
|
||||
response = self.api_call('/me/player/devices')
|
||||
return response.json()
|
||||
|
||||
def play(self, uri):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
data = {}
|
||||
|
||||
if uri:
|
||||
if uri.startswith('spotify:track:'):
|
||||
data = {
|
||||
"uris": [uri]
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"context_uri": uri
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/play?' +
|
||||
urlencode({'device_id': device_id}), headers=headers, data=json.dumps(data))
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.play(uri)
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify play", response, response.text)
|
||||
return False
|
||||
|
||||
def pause(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/pause?' +
|
||||
urlencode({'device_id': device_id}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.pause()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify pause", response, response.text)
|
||||
return False
|
||||
|
||||
def volume(self, value):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
if value < 0:
|
||||
value = 0
|
||||
if value > 100:
|
||||
value = 100
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/volume?' +
|
||||
urlencode({'device_id': device_id, 'volume_percent': value}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.volume(value)
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify volume", response, response.text)
|
||||
return False
|
||||
|
||||
def seek(self, position):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
if position < 0:
|
||||
position = 0
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/seek?' +
|
||||
urlencode({'device_id': device_id, 'position_ms': position}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.seek(position)
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify seek", response, response.text)
|
||||
return False
|
||||
|
||||
def rewind(self, seconds):
|
||||
state = self.playback_state()
|
||||
if state and 'progress_ms' in state:
|
||||
progress = int(state['progress_ms'])
|
||||
progress -= seconds * 1000
|
||||
return self.seek(progress)
|
||||
|
||||
return False
|
||||
|
||||
def fastforward(self, seconds):
|
||||
state = self.playback_state()
|
||||
if state and 'progress_ms' in state:
|
||||
progress = int(state['progress_ms'])
|
||||
progress += seconds * 1000
|
||||
return self.seek(progress)
|
||||
|
||||
return False
|
||||
|
||||
def previous(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://api.spotify.com/v1/me/player/previous?' +
|
||||
urlencode({'device_id': device_id}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.previous()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify previous", response, response.text)
|
||||
return False
|
||||
|
||||
def next(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://api.spotify.com/v1/me/player/next?' +
|
||||
urlencode({'device_id': device_id}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.next()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify next", response, response.text)
|
||||
return False
|
106
application/spotifydl.py
Normal file
106
application/spotifydl.py
Normal file
@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "SpotifyDL"
|
||||
|
||||
import subprocess
|
||||
from configparser import ConfigParser
|
||||
import os.path
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
|
||||
defaultCredentialsLocation = '../config/zspotify.credentials'
|
||||
|
||||
|
||||
class SpotifyDLStatus(Enum):
|
||||
NONE = "none"
|
||||
RUNNING = "running"
|
||||
FINISHED = "finished"
|
||||
ERROR = "error"
|
||||
DISABLED = "disabled"
|
||||
|
||||
|
||||
class SpotifyDL():
|
||||
|
||||
def __init__(self, zspotify_path, username, password, root, credentialsLocation=defaultCredentialsLocation):
|
||||
if zspotify_path:
|
||||
self.zspotify_path = zspotify_path
|
||||
else:
|
||||
raise ValueError("No zspotify path provivded!")
|
||||
if credentialsLocation:
|
||||
self.credentialsLocation = credentialsLocation
|
||||
else:
|
||||
raise ValueError("No credentialsLocation provivded!")
|
||||
if not username:
|
||||
raise ValueError("No username provided!")
|
||||
if not password:
|
||||
raise ValueError("No password provided!")
|
||||
if root:
|
||||
if not root.endswith("/"):
|
||||
root = root + "/"
|
||||
self.root = root
|
||||
else:
|
||||
raise ValueError("No root provided!")
|
||||
|
||||
if not os.path.isfile(self.credentialsLocation):
|
||||
logging.getLogger('luniebox').info("initialize zspotify")
|
||||
p = subprocess.Popen([self.zspotify_path + 'venv/bin/python', self.zspotify_path + 'zspotify/__main__.py', '-s',
|
||||
'--credentials-location', self.credentialsLocation], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=False, group="pi", user="pi")
|
||||
input = username + '\n' + password + '\n'
|
||||
out, err = p.communicate(input=input.encode())
|
||||
logging.getLogger('luniebox').info(out)
|
||||
logging.getLogger('luniebox').warn(err)
|
||||
|
||||
self.downloads = {}
|
||||
|
||||
def download(self, uri):
|
||||
status = self.downloadStatus(uri)
|
||||
if status == SpotifyDLStatus.NONE or status == SpotifyDLStatus.ERROR:
|
||||
logging.getLogger('luniebox').info(
|
||||
"start download of '" + uri + "'")
|
||||
trackprefix = ""
|
||||
if uri.startswith('spotify:album:'):
|
||||
trackprefix = "{album_num}. "
|
||||
elif uri.startswith('spotify:playlist:'):
|
||||
trackprefix = "{playlist_num}. "
|
||||
p = subprocess.Popen([self.zspotify_path + 'venv/bin/python', self.zspotify_path + 'zspotify/__main__.py', "--root-path", self.root,
|
||||
'--credentials-location', self.credentialsLocation, "--download-real-time", "True", "--chunk-size", "50000", "--skip-existing-files", "True", "--skip-previously-downloaded", "True", "--output", uri + "/" + trackprefix + "{artist} - {song_name}.{ext}", uri], stdin=None,
|
||||
stdout=None, stderr=None, close_fds=True, shell=False, group="pi", user="pi")
|
||||
self.downloads[uri] = p
|
||||
return SpotifyDLStatus.RUNNING
|
||||
elif status == SpotifyDLStatus.RUNNING:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"download for '" + uri + "' still running")
|
||||
elif status == SpotifyDLStatus.FINISHED:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"alreaded downloaded '" + uri + "'")
|
||||
return status
|
||||
|
||||
def downloadStatus(self, uri):
|
||||
doneFiledPath = self.root + uri + '/.spotifydl'
|
||||
if uri in self.downloads:
|
||||
if self.downloads[uri]:
|
||||
poll = self.downloads[uri].poll()
|
||||
if poll != None:
|
||||
self.downloads.pop(uri)
|
||||
if poll == 0:
|
||||
p = subprocess.Popen(["touch", doneFiledPath], stdin=None,
|
||||
stdout=None, stderr=None, close_fds=True, shell=False, group="pi", user="pi")
|
||||
p.communicate()
|
||||
return SpotifyDLStatus.FINISHED
|
||||
logging.getLogger('luniebox').warn(
|
||||
"download of '" + uri + "' exited with: " + str(poll))
|
||||
return SpotifyDLStatus.ERROR
|
||||
return SpotifyDLStatus.RUNNING
|
||||
|
||||
if os.path.exists(doneFiledPath):
|
||||
return SpotifyDLStatus.FINISHED
|
||||
|
||||
return SpotifyDLStatus.NONE
|
||||
|
||||
def getDownloads(self):
|
||||
downloads = {}
|
||||
for uri in self.downloads.keys():
|
||||
downloads[uri] = str(self.downloadStatus(uri))
|
||||
return downloads
|
1556
application/static/css/bootstrap-icons.css
vendored
Normal file
1556
application/static/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
application/static/css/bootstrap.min.css
vendored
Normal file
7
application/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
application/static/css/bootstrap.min.css.map
Normal file
1
application/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
application/static/css/fonts/bootstrap-icons.woff
Normal file
BIN
application/static/css/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
application/static/css/fonts/bootstrap-icons.woff2
Normal file
BIN
application/static/css/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
5
application/static/css/style.css
Normal file
5
application/static/css/style.css
Normal file
@ -0,0 +1,5 @@
|
||||
img.item-image {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
max-height: 100px;
|
||||
}
|
7
application/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
application/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
application/static/js/bootstrap.bundle.min.js.map
Normal file
1
application/static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
10
application/templates/error.html
Normal file
10
application/templates/error.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if error == 'play': %}
|
||||
<div class="my-3 alert alert-danger">
|
||||
Error on playing from card.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
134
application/templates/index.html
Normal file
134
application/templates/index.html
Normal file
@ -0,0 +1,134 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1>luniebox</h1>
|
||||
<p>Welcome to luniebox web-interface. This is for configuring and control your luniebox device.</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Daemon</h5>
|
||||
<p class="card-text">
|
||||
{% if daemon_error: %}
|
||||
<div class="my-3 alert alert-danger">
|
||||
{{ daemon_error }}
|
||||
<a href="{{ url_for('api.daemon_start') }}" class="btn btn-secondary">Start daemon</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if daemon_status: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
{{ daemon_status }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Spotify Integration</h5>
|
||||
<p class="card-text">
|
||||
{% if spotify['errors']['setup']: %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
No luniebox device found! Please setup spotifyd propertly (device name must match name from setup, default is 'luniebox')!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spotify['errors']['not_running']: %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Spotifyd is not running. Please start spotifyd service on your luniebox!
|
||||
<a href="{{ url_for('api.restart_spotifyd') }}" class="btn btn-primary">Restart spotifyd</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spotify['status']['not_active']: %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
Spotify is available but currently inactive.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% if spotify['status']['device']: %}
|
||||
<tr class="{{ 'table-success' if spotify['status']['device']['id'] == spotify['device_id'] else '' }}">
|
||||
<th scope="row">Current Device</th>
|
||||
<td>{{ spotify['status']['device']['name'] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if spotify['status']['item']: %}
|
||||
{% with item = spotify['status']['item'] %}
|
||||
<tr class="{{ '' if spotify['status']['is_playing'] else 'table-active' }}">
|
||||
<th>{{ 'Currently playing' if spotify['status']['is_playing'] else 'Last played' }}</th>
|
||||
<td class="d-flex w-100">
|
||||
{% include "spotify/items/track.html" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Music Player Daemon Integration</h5>
|
||||
<p class="card-text">
|
||||
{% if not mpd: %}
|
||||
<div class="my-3 alert alert-danger">
|
||||
Music Player Daemon not connected
|
||||
<a href="{{ url_for('api.restart_mpd') }}" class="btn btn-primary">Restart mpd</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if mpd: %}
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="table-success">
|
||||
<th scope="row">Current state</th>
|
||||
<td>{{ mpd['state'] }}</td>
|
||||
</tr>
|
||||
{% if mpd['song']: %}
|
||||
{% with item = mpd['song'] %}
|
||||
<tr class="{{ '' if mpd['state'] == 'play' else 'table-active' }}">
|
||||
<th>{{ 'Currently playing' if mpd['state'] == 'play' else 'Last played' }}</th>
|
||||
<td class="d-flex w-100">
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['title'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['artist'] }}
|
||||
</p>
|
||||
<small class="text-muted">{{ item['album'] }}</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="alert alert-warning">This will stop daemon. You need to restart daemon afterwards!</p>
|
||||
<a href="{{ url_for('api.rfid_read') }}" class="btn btn-primary">Read card</a>
|
||||
<a href="{{ url_for('api.rfid_play') }}" class="btn btn-secondary">Play card</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
50
application/templates/mpd.html
Normal file
50
application/templates/mpd.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="mpd-search-results">
|
||||
<div class="container">
|
||||
|
||||
{% if path: %}
|
||||
<h3>{{ path }}</h3>
|
||||
{% endif %}
|
||||
<div class="d-flex flex-column mt-3">
|
||||
<ul class="list-group mb-3 flex-fill">
|
||||
{% for item in results %}
|
||||
{% if item.directory and (spotify and item.directory.startswith('spotify:') or not spotify and not item.directory.startswith('spotify:')): %}
|
||||
<li href="#" class="list-group-item flex-fill">
|
||||
<div class="d-flex w-100">
|
||||
<h5 class="mb-1"><a href="{{ url_for('pages.mpd_list', path=item.directory) }}">{{ item.directory }}</a>
|
||||
</h5>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value='mpd:' + item.directory) }}"><i
|
||||
class="bi bi-pencil-square"></i>
|
||||
Write to card</a>
|
||||
<a class="btn btn-secondary" href="{{ url_for('api.play', uri='mpd:' + item.directory) }}"><i
|
||||
class="bi bi-play"></i> Play
|
||||
now</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if item.file and not item.file.startswith('.') and (spotify and item.file.startswith('spotify:') or not spotify and not item.file.startswith('spotify:')): %}
|
||||
<li href="#" class="list-group-item flex-fill">
|
||||
<div class="d-flex w-100">
|
||||
<h5 class="mb-1">{{ item.file }}</h5>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value='mpd:' + path + '/' + item.file) }}"><i
|
||||
class="bi bi-pencil-square"></i>
|
||||
Write to card</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if not spotify: %}
|
||||
<a href="{{ url_for('pages.mpd_list', spotify=true) }}">List Spotify Downloads</a>
|
||||
{% endif %}
|
||||
{% if spotify: %}
|
||||
<a href="{{ url_for('pages.mpd_list') }}">List MPD files</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
60
application/templates/setup.html
Normal file
60
application/templates/setup.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1> Setup </h1>
|
||||
<p>Here you can setup the luniebox or restart services when having issues.</p>
|
||||
|
||||
<div class="row">
|
||||
{% if model['setup']: %}
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="{{ url_for('api.restart_spotifyd') }}" class="btn btn-primary">Restart spotifyd</a>
|
||||
<a href="{{ url_for('api.daemon_start') }}" class="btn btn-secondary">Start daemon</a>
|
||||
<a href="{{ url_for('api.daemon_stop') }}" class="btn btn-warning">Stop daemon</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
{% if model['setup']: %}
|
||||
<div class="my-3 alert alert-warning">
|
||||
Already set-up! On changes you may need to restart spotifyd service.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('api.setup') }}">
|
||||
<div class="mb-3">
|
||||
<label for="spotify-device-name" class="form-label">Spotify Device Name</label>
|
||||
<input type="text" class="form-control" id="spotify-device-name" name="spotify-device-name" required
|
||||
value="{{'' if not model['device-name'] else '' + model['device-name'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-username" class="form-label">Spotify Username</label>
|
||||
<input type="text" class="form-control" id="spotify-username" name="spotify-username" required
|
||||
value="{{'' if not model['username'] else '' + model['username'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-password" class="form-label">Spotify Password</label>
|
||||
<input type="password" class="form-control" id="spotify-password" name="spotify-password" required
|
||||
value="{{'' if not model['password'] else '' + model['password'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-client-id" class="form-label">Spotify Client Id</label>
|
||||
<input type="text" class="form-control" id="spotify-client-id" name="spotify-client-id" required
|
||||
value="{{'' if not model['client-id'] else '' + model['client-id'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-client-secret" class="form-label">Spotify Client Secret</label>
|
||||
<input type="text" class="form-control" id="spotify-client-secret" name="spotify-client-secret" required
|
||||
value="{{'' if not model['client-secret'] else '' + model['client-secret'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-redirect-uri" class="form-label">Spotify Redirect Uri</label>
|
||||
<input type="text" class="form-control" id="spotify-redirect-uri" name="spotify-redirect-uri"
|
||||
value="{{'' if not model['redirect-uri'] else '' + model['redirect-uri'] }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Setup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
11
application/templates/spotify/items/album.html
Normal file
11
application/templates/spotify/items/album.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['artists'] | map(attribute='name') | join(', ') }}
|
||||
</p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
9
application/templates/spotify/items/artist.html
Normal file
9
application/templates/spotify/items/artist.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1"></p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
11
application/templates/spotify/items/cover.html
Normal file
11
application/templates/spotify/items/cover.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="dropdown">
|
||||
<img src="{{ images | map(attribute='url') | first }}" class="item-image rounded dropdown-toggle"
|
||||
data-bs-toggle="dropdown" data-bs-placement="top" title="Download Cover">
|
||||
<ul class="dropdown-menu">
|
||||
{% for image in images %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{image['url']}}" target="_blank">{{image['width']}}px</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
9
application/templates/spotify/items/episode.html
Normal file
9
application/templates/spotify/items/episode.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1"></p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
22
application/templates/spotify/items/menu.html
Normal file
22
application/templates/spotify/items/menu.html
Normal file
@ -0,0 +1,22 @@
|
||||
<div class="d-flex w-100 mt-3 justify-content-between">
|
||||
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value=item.uri) }}"><i class="bi bi-pencil-square"></i>
|
||||
Write to card</a>
|
||||
<a class="btn btn-secondary" href="{{ url_for('api.play', uri=item.uri) }}"><i class="bi bi-play"></i> Play
|
||||
now</a>
|
||||
{% if 'spotifydl' in item: %}
|
||||
{% if item['spotifydl'] == 'SpotifyDLStatus.NONE': %}
|
||||
<a class="btn btn-warning" href="{{ url_for('api.spotify_dl', uri=item.uri) }}"><i class="bi bi-save"></i>
|
||||
Download</a>
|
||||
{% else: %}
|
||||
<p>
|
||||
{% if item['spotifydl'] == 'SpotifyDLStatus.FINISHED': %}
|
||||
<span class="badge rounded-pill bg-success">Downloaded</span>
|
||||
{% elif item['spotifydl'] == 'SpotifyDLStatus.RUNNING': %}
|
||||
<span class="badge rounded-pill bg-info">Running</span>
|
||||
{% elif item['spotifydl'] == 'SpotifyDLStatus.ERROR': %}
|
||||
<span class="badge rounded-pill bg-danger">Error</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
11
application/templates/spotify/items/playlist.html
Normal file
11
application/templates/spotify/items/playlist.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['description'] }}
|
||||
</p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
9
application/templates/spotify/items/show.html
Normal file
9
application/templates/spotify/items/show.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1"></p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
11
application/templates/spotify/items/track.html
Normal file
11
application/templates/spotify/items/track.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['artists'] | map(attribute='name') | join(', ') }}
|
||||
</p>
|
||||
<small class="text-muted">{{ item['album']['name'] }}</small>
|
||||
</div>
|
||||
{%if item['album']['images']: %}
|
||||
{% set images = item['album']['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
117
application/templates/spotify_search.html
Normal file
117
application/templates/spotify_search.html
Normal file
@ -0,0 +1,117 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="mt-3">
|
||||
<form method="GET" action="{{ url_for('pages.spotify_search') }}">
|
||||
<div class="mb-3">
|
||||
<label for="search-query" class="form-label">Search</label>
|
||||
<input type="text" name="q" id="search-query" class="form-control" placeholder="Artists, songs, or podcasts"
|
||||
required value="{{ query }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="track" id="search-type-track" {{'checked'
|
||||
if 'track' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-track">
|
||||
Tracks
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="album" id="search-type-album" {{'checked'
|
||||
if 'album' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-album">
|
||||
Albums
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="artist" id="search-type-artist"
|
||||
{{'checked' if 'artist' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-artist">
|
||||
Artists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="playlist" id="search-type-playlist"
|
||||
{{'checked' if 'playlist' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-playlist">
|
||||
Playlists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="show" id="search-type-show" {{'checked'
|
||||
if 'show' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-show">
|
||||
Shows
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="episode" id="search-type-episode"
|
||||
{{'checked' if 'episode' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-episode">
|
||||
Episodes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if results: %}
|
||||
<div class="alert alert-warning mt-3">
|
||||
Writing to card will stop the daemon. You need to restart daemon afterwards!
|
||||
</div>
|
||||
<div class="spotify-search-results">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% for type in types %}
|
||||
{% if results[type + 's']: %}
|
||||
{% with result = results[type + 's'] %}
|
||||
<div
|
||||
class="d-flex flex-column col col-xs-12 col-md-6 col-lg-6 {{ 'col-xl-4' if results|length > 2 else 'col-xl-6' }} mt-3">
|
||||
<h3>{{ type | capitalize}}s</h3>
|
||||
|
||||
<ul class="list-group mb-3 flex-fill">
|
||||
{% for item in result['items'] %}
|
||||
<li href="#" class="list-group-item flex-fill">
|
||||
<div class="d-flex w-100">
|
||||
{% include "spotify/items/" + type + ".html" %}
|
||||
</div>
|
||||
|
||||
{% include "spotify/items/menu.html" %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="d-flex w-100 justify-content-between mt-auto">
|
||||
<a href="{{ url_for('pages.spotify_search',q=query, type=type, limit=result['limit'], offset=result['offset']-result['limit']) }}"
|
||||
class="btn btn-primary {{ 'disabled' if result['offset']==0 else '' }} " {{ 'disabled' if
|
||||
result['offset']==0 else '' }}>«</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 %}
|
10
application/templates/spotifydl.html
Normal file
10
application/templates/spotifydl.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if uri: %}
|
||||
<h3>{{ uri }}</h3>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ status }}</p>
|
||||
|
||||
{% endblock %}
|
60
application/templates/success.html
Normal file
60
application/templates/success.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if success['rfid_write']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Write card value: {{ success['rfid_write'] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['rfid_read']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Read card value: {{ success['rfid_read'] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['rfid_play']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Start playing from card.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['play']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Start playing {{ success['play'] }}.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['restart_spotifyd']: %}
|
||||
<div class="my-3 alert alert-secondary">
|
||||
Restartet spotifyd service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['restart_mpd']: %}
|
||||
<div class="my-3 alert alert-secondary">
|
||||
Restartet mpd service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['start_daemon']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Started luniebox-daemon service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['stop_daemon']: %}
|
||||
<div class="my-3 alert alert-warning">
|
||||
Stopped luniebox-daemon service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['restart_luniebox']: %}
|
||||
<div class="my-3 alert alert-secondary">
|
||||
Restartet luniebox-app service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-primary" href="{{ url_for('pages.index') }}">Go back to status page</a>
|
||||
|
||||
{% endblock %}
|
52
application/templates/template.html
Normal file
52
application/templates/template.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>luniebox</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('pages.index') }}">luniebox</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.index') }}">Status</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.spotify_search') }}">Search Spotify</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.mpd_list') }}">MPD listing</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.setup') }}">Setup</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container mt-3">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<footer class="mt-5">
|
||||
|
||||
</footer>
|
||||
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
7
application/util.py
Normal file
7
application/util.py
Normal file
@ -0,0 +1,7 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def randomString(len):
|
||||
return ''.join(random.SystemRandom().choice(
|
||||
string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(len))
|
38
contrib/config/asound.conf
Normal file
38
contrib/config/asound.conf
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
pcm.speakerbonnet {
|
||||
type hw card 0
|
||||
}
|
||||
|
||||
pcm.dmixer {
|
||||
type dmix
|
||||
ipc_key 1024
|
||||
ipc_perm 0666
|
||||
slave {
|
||||
pcm "speakerbonnet"
|
||||
period_time 0
|
||||
period_size 1024
|
||||
buffer_size 8192
|
||||
rate 44100
|
||||
channels 2
|
||||
}
|
||||
}
|
||||
|
||||
ctl.dmixer {
|
||||
type hw card 0
|
||||
}
|
||||
|
||||
pcm.softvol {
|
||||
type softvol
|
||||
slave.pcm "dmixer"
|
||||
control.name "PCM"
|
||||
control.card 0
|
||||
}
|
||||
|
||||
ctl.softvol {
|
||||
type hw card 0
|
||||
}
|
||||
|
||||
pcm.!default {
|
||||
type plug
|
||||
slave.pcm "softvol"
|
||||
}
|
17
contrib/config/luniebox.cfg
Normal file
17
contrib/config/luniebox.cfg
Normal file
@ -0,0 +1,17 @@
|
||||
[api]
|
||||
|
||||
[hardware]
|
||||
mpu = False
|
||||
led = False
|
||||
|
||||
[logging]
|
||||
level = DEBUG
|
||||
|
||||
[luniebox]
|
||||
|
||||
[mpd]
|
||||
library_path = /home/pi/luniebox/library
|
||||
|
||||
[spotify]
|
||||
auto_download = False
|
||||
zspotify_path =
|
21
contrib/config/mpd.conf
Normal file
21
contrib/config/mpd.conf
Normal file
@ -0,0 +1,21 @@
|
||||
music_directory "/home/pi/luniebox/library"
|
||||
playlist_directory "/var/lib/mpd/playlists"
|
||||
db_file "/var/lib/mpd/tag_cache"
|
||||
restore_paused "yes"
|
||||
log_file "/var/log/mpd/mpd.log"
|
||||
pid_file "/run/mpd/pid"
|
||||
state_file "/var/lib/mpd/state"
|
||||
sticker_file "/var/lib/mpd/sticker.sql"
|
||||
user "mpd"
|
||||
bind_to_address "localhost"
|
||||
audio_output {
|
||||
type "alsa"
|
||||
name "ALSA Device"
|
||||
device "hw:CARD=sndrpihifiberry"
|
||||
mixer_type "software"
|
||||
mixer_device "default"
|
||||
mixer_control "PCM"
|
||||
mixer_index "0"
|
||||
}
|
||||
volume_normalization "yes"
|
||||
filesystem_charset "UTF-8"
|
11
contrib/config/spotifyd.cfg
Normal file
11
contrib/config/spotifyd.cfg
Normal file
@ -0,0 +1,11 @@
|
||||
[global]
|
||||
username = ""
|
||||
password = ""
|
||||
backend = "alsa"
|
||||
device = "hw:CARD=sndrpihifiberry"
|
||||
device_name = "luniebox"
|
||||
device_type = "computer"
|
||||
bitrate = 160
|
||||
volume-normalisation = true
|
||||
normalisation-pregain = 0
|
||||
cache_path = "/home/pi/luniebox/.cache/spotify"
|
12
contrib/service/luniebox-app.service
Normal file
12
contrib/service/luniebox-app.service
Normal file
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Luniebox Application
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/luniebox/application
|
||||
ExecStart=/home/pi/luniebox/application/venv/bin/gunicorn -b 0.0.0.0:80 app:app
|
||||
Restart=always
|
||||
RestartSec=12
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
12
contrib/service/luniebox-daemon.service
Normal file
12
contrib/service/luniebox-daemon.service
Normal file
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Luniebox Daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/luniebox/application
|
||||
ExecStart=/home/pi/luniebox/application/venv/bin/python daemon.py
|
||||
Restart=always
|
||||
RestartSec=12
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
16
contrib/service/spotifyd.service
Normal file
16
contrib/service/spotifyd.service
Normal file
@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=the spotify playing daemon
|
||||
Wants=sound.target
|
||||
After=sound.target
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
User=pi
|
||||
Group=pi
|
||||
ExecStart=/home/pi/luniebox/bin/spotifyd --config-path /home/pi/luniebox/config/spotifyd.cfg --no-daemon
|
||||
Restart=always
|
||||
RestartSec=12
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
48
hardware/case/luniebox-back.scad
Normal file
48
hardware/case/luniebox-back.scad
Normal file
@ -0,0 +1,48 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_back() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,height+depth,depth]);
|
||||
// screws RFID MC522
|
||||
translate([length/2-22,height*0.6-13.5,depth])
|
||||
screw();
|
||||
translate([length/2-22,height*0.6+13.5,depth])
|
||||
screw();
|
||||
translate([length/2+16.5,height*0.6-18,depth])
|
||||
screw();
|
||||
translate([length/2+16.5,height*0.6+18,depth])
|
||||
screw();
|
||||
// clips
|
||||
translate([clip_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
}
|
||||
// USB port hole
|
||||
translate([54+depth,14+depth,0])
|
||||
rounded_cube(s=[10,6,depth],r=1.5);
|
||||
translate([38,20+depth,-render_limiter])
|
||||
cube([10,1.5,depth+render_limiter*2]);
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_back();
|
||||
|
||||
|
48
hardware/case/luniebox-bottom.scad
Normal file
48
hardware/case/luniebox-bottom.scad
Normal file
@ -0,0 +1,48 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_bottom() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,width+depth,depth]);
|
||||
// clips
|
||||
translate([6,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([6,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
// screws Raspi Zero
|
||||
translate([length-10,5+depth,depth])
|
||||
screw();
|
||||
translate([length-68,5+depth,depth])
|
||||
screw();
|
||||
translate([length-10,28+depth,depth])
|
||||
screw();
|
||||
translate([length-68,28+depth,depth])
|
||||
screw();
|
||||
// screws MCP
|
||||
translate([length/2-7.5,width/2-7+depth,depth])
|
||||
screw();
|
||||
translate([length/2+7.5,width/2-7+depth,depth])
|
||||
screw();
|
||||
}
|
||||
// clip holes
|
||||
translate([clip_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_bottom();
|
||||
|
10
hardware/case/luniebox-cardholder.scad
Normal file
10
hardware/case/luniebox-cardholder.scad
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
difference() {
|
||||
cube([94,50,3]);
|
||||
translate([6,-0.001,-0.001])
|
||||
cube([82,44.002,1.502]);
|
||||
translate([3,-0.001,1.499])
|
||||
cube([88,47.002,1.502]);
|
||||
}
|
||||
|
||||
|
76
hardware/case/luniebox-front.scad
Normal file
76
hardware/case/luniebox-front.scad
Normal file
@ -0,0 +1,76 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_front() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,height+depth,depth]);
|
||||
// screws speaker right
|
||||
translate([height/2-22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([height/2+22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([height/2-22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
translate([height/2+22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
// screws speaker left
|
||||
translate([length-height/2-22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([length-height/2+22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([length-height/2-22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
translate([length-height/2+22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
// clips
|
||||
translate([clip_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
}
|
||||
// speaker hole right
|
||||
translate([height/2,height/2,-depth / 2])
|
||||
cylinder(d=50,h=depth*2);
|
||||
// speaker hole left
|
||||
translate([length-height/2,height/2,-depth / 2])
|
||||
cylinder(d=50,h=depth * 2);
|
||||
// screw holes speaker right
|
||||
translate([height/2-22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([height/2+22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([height/2-22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([height/2+22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
// screw holes speaker left
|
||||
translate([length-height/2-22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([length-height/2+22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([length-height/2-22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([length-height/2+22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_front();
|
||||
|
||||
|
74
hardware/case/luniebox-helper.scad
Normal file
74
hardware/case/luniebox-helper.scad
Normal file
@ -0,0 +1,74 @@
|
||||
$fa = 1;
|
||||
$fs = 0.05;
|
||||
|
||||
render_limiter = 0.001;
|
||||
|
||||
depth = 2;
|
||||
length = 190;
|
||||
width = 70;
|
||||
height = 70;
|
||||
|
||||
|
||||
clip_space=20;
|
||||
clip_d=5;
|
||||
clip_h=6;
|
||||
clip_length_space=length - clip_space + depth;
|
||||
clip_width_space=width - clip_space + depth;
|
||||
clip_height_space=height - clip_space + depth;
|
||||
|
||||
|
||||
module clip_screw() {
|
||||
d=clip_d;
|
||||
h=clip_h;
|
||||
translate([0,0,d/2+depth])
|
||||
rotate([-90,0,0])
|
||||
difference() {
|
||||
union() {
|
||||
cylinder(d=d,h=h);
|
||||
translate([-d/2,0,0])
|
||||
cube([d,d/2,h]);
|
||||
}
|
||||
translate([0,0,-render_limiter])
|
||||
cylinder(d=d/2,h=h+render_limiter*2);
|
||||
}
|
||||
}
|
||||
|
||||
module clip_hole(h=0) {
|
||||
d=clip_d;
|
||||
translate([0,0,-render_limiter])
|
||||
cylinder(d=d/2,h=h+depth+render_limiter * 2);
|
||||
}
|
||||
|
||||
module clip(width=15) {
|
||||
translate([-render_limiter,-render_limiter,-render_limiter])
|
||||
cube([width +render_limiter * 2,depth + render_limiter * 2,depth + render_limiter * 2]);
|
||||
}
|
||||
|
||||
module screw_hole(depth=0,d=clip_d/2) {
|
||||
z= -depth - render_limiter;
|
||||
h=7+depth;
|
||||
translate([0,0,z])
|
||||
cylinder(d=d,h=h);
|
||||
}
|
||||
|
||||
module screw(d=clip_d,h=clip_h) {
|
||||
difference() {
|
||||
cylinder(d=d,h=h);
|
||||
screw_hole(d=d/2);
|
||||
}
|
||||
}
|
||||
|
||||
module rounded_cube(s=[1,1,1],r=0.5) {
|
||||
x = s[0];
|
||||
y = s[1];
|
||||
z = s[2];
|
||||
mx = x - 2*r;
|
||||
my = y - 2*r;
|
||||
mz = z / 2;
|
||||
|
||||
minkowski() {
|
||||
cube([mx,my,mz]);
|
||||
translate([r,r,-render_limiter])
|
||||
cylinder(r=r,h=mz+render_limiter*2);
|
||||
}
|
||||
}
|
31
hardware/case/luniebox-side.scad
Normal file
31
hardware/case/luniebox-side.scad
Normal file
@ -0,0 +1,31 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_side() {
|
||||
difference() {
|
||||
cube([width+depth,height+depth*3,depth]);
|
||||
// clip holes
|
||||
translate([clip_space,clip_d/2+depth,0])
|
||||
clip_hole();
|
||||
translate([clip_width_space,clip_d/2+depth,0])
|
||||
clip_hole();
|
||||
translate([clip_space,height-clip_d/2+depth*2,0])
|
||||
clip_hole();
|
||||
translate([clip_width_space,height-clip_d/2+depth*2,0])
|
||||
clip_hole();
|
||||
|
||||
translate([clip_d/2+depth,clip_space+depth,0])
|
||||
clip_hole();
|
||||
translate([clip_d/2+depth,clip_height_space+depth,0])
|
||||
clip_hole();
|
||||
|
||||
translate([width-clip_d/2,clip_space+depth,0])
|
||||
clip_hole();
|
||||
translate([width-clip_d/2,clip_height_space+depth,0])
|
||||
clip_hole();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_side();
|
||||
|
||||
|
46
hardware/case/luniebox-top.scad
Normal file
46
hardware/case/luniebox-top.scad
Normal file
@ -0,0 +1,46 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_top() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,width+depth,depth]);
|
||||
// clips
|
||||
translate([clip_h,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
}
|
||||
// clip holes
|
||||
translate([clip_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
// right btn hole
|
||||
translate([length/2-25+depth,(width+depth)/2,-depth/2])
|
||||
cylinder(d=30,h=depth*2);
|
||||
// left button hole
|
||||
translate([length/2+25+depth,(width+depth)/2,-depth/2])
|
||||
cylinder(d=30,h=depth*2);
|
||||
// led holes
|
||||
translate([length/2+depth,(width+depth)/2-16.666,-depth/2])
|
||||
cylinder(d=2,h=depth*2);
|
||||
translate([length/2+depth,(width+depth)/2,-depth/2])
|
||||
cylinder(d=2,h=depth*2);
|
||||
translate([length/2+depth,(width+depth)/2+16.666,-depth/2])
|
||||
cylinder(d=2,h=depth*2);
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_top();
|
34
hardware/case/luniebox.scad
Normal file
34
hardware/case/luniebox.scad
Normal file
@ -0,0 +1,34 @@
|
||||
include<luniebox-helper.scad>
|
||||
use<luniebox-bottom.scad>
|
||||
use<luniebox-front.scad>
|
||||
use<luniebox-side.scad>
|
||||
use<luniebox-top.scad>
|
||||
use<luniebox-back.scad>
|
||||
|
||||
color(c=[1,0.1,0.1])
|
||||
luniebox_bottom();
|
||||
|
||||
color(c=[0.2,1,0.2])
|
||||
translate([0,width+depth,depth])
|
||||
rotate([90,0,0])
|
||||
luniebox_front();
|
||||
|
||||
color(c=[0.3,0.3,1])
|
||||
translate([-depth,0,0])
|
||||
rotate([90,0,90])
|
||||
luniebox_side();
|
||||
|
||||
color(c=[1,1,0.4])
|
||||
translate([length+depth,0,0])
|
||||
rotate([90,0,90])
|
||||
luniebox_side();
|
||||
|
||||
translate([0,width+depth,height+depth*3])
|
||||
rotate([180,0,0])
|
||||
color(c=[1,0.5,1])
|
||||
luniebox_top();
|
||||
|
||||
color(c=[1,0.6,1])
|
||||
translate([length+depth,0,depth])
|
||||
rotate([90,0,180])
|
||||
luniebox_back();
|
48
hardware/case/square/luniebox-back.scad
Normal file
48
hardware/case/square/luniebox-back.scad
Normal file
@ -0,0 +1,48 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_back() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,height+depth,depth]);
|
||||
// screws RFID MC522
|
||||
translate([length/2-22,height*0.6-13.5,depth])
|
||||
screw();
|
||||
translate([length/2-22,height*0.6+13.5,depth])
|
||||
screw();
|
||||
translate([length/2+16.5,height*0.6-18,depth])
|
||||
screw();
|
||||
translate([length/2+16.5,height*0.6+18,depth])
|
||||
screw();
|
||||
// clips
|
||||
translate([clip_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
}
|
||||
// USB port hole
|
||||
translate([54+depth,14+depth,0])
|
||||
rounded_cube(s=[10,6,depth],r=1.5);
|
||||
translate([38,20+depth,-render_limiter])
|
||||
cube([10,1.5,depth+render_limiter*2]);
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_back();
|
||||
|
||||
|
48
hardware/case/square/luniebox-bottom.scad
Normal file
48
hardware/case/square/luniebox-bottom.scad
Normal file
@ -0,0 +1,48 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_bottom() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,width+depth,depth]);
|
||||
// clips
|
||||
translate([6,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([6,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
// screws Raspi Zero
|
||||
translate([length-10,5+depth,depth])
|
||||
screw();
|
||||
translate([length-68,5+depth,depth])
|
||||
screw();
|
||||
translate([length-10,28+depth,depth])
|
||||
screw();
|
||||
translate([length-68,28+depth,depth])
|
||||
screw();
|
||||
// screws MCP
|
||||
translate([length/2-7.5,width/2-7+depth,depth])
|
||||
screw();
|
||||
translate([length/2+7.5,width/2-7+depth,depth])
|
||||
screw();
|
||||
}
|
||||
// clip holes
|
||||
translate([clip_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_bottom();
|
||||
|
10
hardware/case/square/luniebox-cardholder.scad
Normal file
10
hardware/case/square/luniebox-cardholder.scad
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
difference() {
|
||||
cube([94,50,3]);
|
||||
translate([6,-0.001,-0.001])
|
||||
cube([82,44.002,1.502]);
|
||||
translate([3,-0.001,1.499])
|
||||
cube([88,47.002,1.502]);
|
||||
}
|
||||
|
||||
|
76
hardware/case/square/luniebox-front.scad
Normal file
76
hardware/case/square/luniebox-front.scad
Normal file
@ -0,0 +1,76 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_front() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,height+depth,depth]);
|
||||
// screws speaker right
|
||||
translate([height/2-22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([height/2+22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([height/2-22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
translate([height/2+22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
// screws speaker left
|
||||
translate([length-height/2-22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([length-height/2+22.5,height/2-22.5,depth])
|
||||
screw();
|
||||
translate([length-height/2-22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
translate([length-height/2+22.5,height/2+22.5,depth])
|
||||
screw();
|
||||
// clips
|
||||
translate([clip_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,height-clip_h+depth,0])
|
||||
clip_screw();
|
||||
translate([clip_length_space,0,0])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_height_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
}
|
||||
// speaker hole right
|
||||
translate([height/2,height/2,-depth / 2])
|
||||
cylinder(d=50,h=depth*2);
|
||||
// speaker hole left
|
||||
translate([length-height/2,height/2,-depth / 2])
|
||||
cylinder(d=50,h=depth * 2);
|
||||
// screw holes speaker right
|
||||
translate([height/2-22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([height/2+22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([height/2-22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([height/2+22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
// screw holes speaker left
|
||||
translate([length-height/2-22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([length-height/2+22.5,height/2-22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([length-height/2-22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
translate([length-height/2+22.5,height/2+22.5,depth])
|
||||
screw_hole(depth);
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_front();
|
||||
|
||||
|
74
hardware/case/square/luniebox-helper.scad
Normal file
74
hardware/case/square/luniebox-helper.scad
Normal file
@ -0,0 +1,74 @@
|
||||
$fa = 1;
|
||||
$fs = 0.05;
|
||||
|
||||
render_limiter = 0.001;
|
||||
|
||||
depth = 2;
|
||||
length = 100;
|
||||
width = 100;
|
||||
height = 100;
|
||||
|
||||
|
||||
clip_space=20;
|
||||
clip_d=5;
|
||||
clip_h=6;
|
||||
clip_length_space=length - clip_space + depth;
|
||||
clip_width_space=width - clip_space + depth;
|
||||
clip_height_space=height - clip_space + depth;
|
||||
|
||||
|
||||
module clip_screw() {
|
||||
d=clip_d;
|
||||
h=clip_h;
|
||||
translate([0,0,d/2+depth])
|
||||
rotate([-90,0,0])
|
||||
difference() {
|
||||
union() {
|
||||
cylinder(d=d,h=h);
|
||||
translate([-d/2,0,0])
|
||||
cube([d,d/2,h]);
|
||||
}
|
||||
translate([0,0,-render_limiter])
|
||||
cylinder(d=d/2,h=h+render_limiter*2);
|
||||
}
|
||||
}
|
||||
|
||||
module clip_hole(h=0) {
|
||||
d=clip_d;
|
||||
translate([0,0,-render_limiter])
|
||||
cylinder(d=d/2,h=h+depth+render_limiter * 2);
|
||||
}
|
||||
|
||||
module clip(width=15) {
|
||||
translate([-render_limiter,-render_limiter,-render_limiter])
|
||||
cube([width +render_limiter * 2,depth + render_limiter * 2,depth + render_limiter * 2]);
|
||||
}
|
||||
|
||||
module screw_hole(depth=0,d=clip_d/2) {
|
||||
z= -depth - render_limiter;
|
||||
h=7+depth;
|
||||
translate([0,0,z])
|
||||
cylinder(d=d,h=h);
|
||||
}
|
||||
|
||||
module screw(d=clip_d,h=clip_h) {
|
||||
difference() {
|
||||
cylinder(d=d,h=h);
|
||||
screw_hole(d=d/2);
|
||||
}
|
||||
}
|
||||
|
||||
module rounded_cube(s=[1,1,1],r=0.5) {
|
||||
x = s[0];
|
||||
y = s[1];
|
||||
z = s[2];
|
||||
mx = x - 2*r;
|
||||
my = y - 2*r;
|
||||
mz = z / 2;
|
||||
|
||||
minkowski() {
|
||||
cube([mx,my,mz]);
|
||||
translate([r,r,-render_limiter])
|
||||
cylinder(r=r,h=mz+render_limiter*2);
|
||||
}
|
||||
}
|
31
hardware/case/square/luniebox-side.scad
Normal file
31
hardware/case/square/luniebox-side.scad
Normal file
@ -0,0 +1,31 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_side() {
|
||||
difference() {
|
||||
cube([width+depth,height+depth*3,depth]);
|
||||
// clip holes
|
||||
translate([clip_space,clip_d/2+depth,0])
|
||||
clip_hole();
|
||||
translate([clip_width_space,clip_d/2+depth,0])
|
||||
clip_hole();
|
||||
translate([clip_space,height-clip_d/2+depth*2,0])
|
||||
clip_hole();
|
||||
translate([clip_width_space,height-clip_d/2+depth*2,0])
|
||||
clip_hole();
|
||||
|
||||
translate([clip_d/2+depth,clip_space+depth,0])
|
||||
clip_hole();
|
||||
translate([clip_d/2+depth,clip_height_space+depth,0])
|
||||
clip_hole();
|
||||
|
||||
translate([width-clip_d/2,clip_space+depth,0])
|
||||
clip_hole();
|
||||
translate([width-clip_d/2,clip_height_space+depth,0])
|
||||
clip_hole();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_side();
|
||||
|
||||
|
46
hardware/case/square/luniebox-top.scad
Normal file
46
hardware/case/square/luniebox-top.scad
Normal file
@ -0,0 +1,46 @@
|
||||
include<luniebox-helper.scad>
|
||||
|
||||
module luniebox_top() {
|
||||
difference() {
|
||||
union() {
|
||||
cube([length+depth,width+depth,depth]);
|
||||
// clips
|
||||
translate([clip_h,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([clip_h,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
translate([length+depth,clip_width_space,0])
|
||||
rotate([0,0,90])
|
||||
clip_screw();
|
||||
}
|
||||
// clip holes
|
||||
translate([clip_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,depth+clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
translate([clip_length_space,width-clip_d/2,0])
|
||||
clip_hole();
|
||||
// right btn hole
|
||||
translate([length/2-25+depth,(width+depth)/2,-depth/2])
|
||||
cylinder(d=30,h=depth*2);
|
||||
// left button hole
|
||||
translate([length/2+25+depth,(width+depth)/2,-depth/2])
|
||||
cylinder(d=30,h=depth*2);
|
||||
// led holes
|
||||
translate([length/2+depth,(width+depth)/2-16.666,-depth/2])
|
||||
cylinder(d=2,h=depth*2);
|
||||
translate([length/2+depth,(width+depth)/2,-depth/2])
|
||||
cylinder(d=2,h=depth*2);
|
||||
translate([length/2+depth,(width+depth)/2+16.666,-depth/2])
|
||||
cylinder(d=2,h=depth*2);
|
||||
}
|
||||
}
|
||||
|
||||
luniebox_top();
|
34
hardware/case/square/luniebox.scad
Normal file
34
hardware/case/square/luniebox.scad
Normal file
@ -0,0 +1,34 @@
|
||||
include<luniebox-helper.scad>
|
||||
use<luniebox-bottom.scad>
|
||||
use<luniebox-front.scad>
|
||||
use<luniebox-side.scad>
|
||||
use<luniebox-top.scad>
|
||||
use<luniebox-back.scad>
|
||||
|
||||
color(c=[1,0.1,0.1])
|
||||
luniebox_bottom();
|
||||
|
||||
color(c=[0.2,1,0.2])
|
||||
translate([0,width+depth,depth])
|
||||
rotate([90,0,0])
|
||||
luniebox_front();
|
||||
|
||||
color(c=[0.3,0.3,1])
|
||||
translate([-depth,0,0])
|
||||
rotate([90,0,90])
|
||||
luniebox_side();
|
||||
|
||||
color(c=[1,1,0.4])
|
||||
translate([length+depth,0,0])
|
||||
rotate([90,0,90])
|
||||
luniebox_side();
|
||||
|
||||
translate([0,width+depth,height+depth*3])
|
||||
rotate([180,0,0])
|
||||
color(c=[1,0.5,1])
|
||||
luniebox_top();
|
||||
|
||||
color(c=[1,0.6,1])
|
||||
translate([length+depth,0,depth])
|
||||
rotate([90,0,180])
|
||||
luniebox_back();
|
348
luniebox.sh
Executable file
348
luniebox.sh
Executable file
@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
AUTOMATIC=false
|
||||
NO_UPDATES=false
|
||||
NO_SPOTIFYDL=false
|
||||
NO_SPOTIFY=false
|
||||
NO_MPD=false
|
||||
NO_AUDIO=false
|
||||
MPU=false
|
||||
AUDIO_DEVICE=false
|
||||
AUDIO_DEVICES=("pimoroni" "adafruit")
|
||||
DEVICE_NAME=false
|
||||
|
||||
help() {
|
||||
local IFS="|"
|
||||
printf "Usage: ${SCRIPTNAME} [-h] [-a] [--audio-device AUDIO_DEVICE] [--device-name DEVICE_NAME] [--no-updates] [--no-spotifydl] [--no-spotify] [--no-mpd] [--mpu] \n"
|
||||
printf " -h\t\t\t\tshow this help\n"
|
||||
printf " -a\t\t\t\tautomatic/non-interactive mode (without --audio-device, no Audio device is set up)\n"
|
||||
printf " --audio-device [AUDIO_DEVICE]\tset Audio device (one of [${AUDIO_DEVICES[*]}])\n"
|
||||
printf " --device-name [DEVICE_NAME]\tset luniebox device name (default: luniebox)\n"
|
||||
printf " --no-updates\t\t\tskip system updates \n"
|
||||
printf " --no-spotifydl\t\t\tskip installation of zspotify (no offline support for Spotify) \n"
|
||||
printf " --no-mpd\t\t\tskip installation of Music Player Daemon (no offline support, also skips zspotify)\n"
|
||||
printf " --no-spotify\t\t\tskip installation of spotifyd (no Spotify support, also skips zspotify)\n"
|
||||
printf " --mpu\t\t\t\tsetup MPU9250 on I2C port (default is no MPU9250)\n"
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
case "$1" in
|
||||
"-h" | "--help")
|
||||
help
|
||||
;;
|
||||
"-a" | "--automatic")
|
||||
AUTOMATIC=true
|
||||
;;
|
||||
"--no-updates")
|
||||
NO_UPDATES=true
|
||||
;;
|
||||
"--no-spotifydl")
|
||||
NO_SPOTIFYDL=true
|
||||
;;
|
||||
"--no-spotify")
|
||||
NO_SPOTIFY=true
|
||||
;;
|
||||
"--audio-device")
|
||||
AUDIO_DEVICE="$2"
|
||||
shift
|
||||
;;
|
||||
"--device-name")
|
||||
DEVICE_NAME="$2"
|
||||
shift
|
||||
;;
|
||||
"--mpu")
|
||||
MPU=true
|
||||
;;
|
||||
*)
|
||||
printf "\nargument '$1' not supported!\n\n"
|
||||
help
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ ${AUDIO_DEVICE} != "false" ]] && ! [[ ${AUDIO_DEVICES[*]} =~ "$AUDIO_DEVICE" ]]; then
|
||||
NO_AUDIO=true
|
||||
printf "Audio device '${AUDIO_DEVICE}' not supported, please set it up manually.\n"
|
||||
fi
|
||||
|
||||
AUDIO_DEVICES+=("other")
|
||||
|
||||
start() {
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
echo "##########################################
|
||||
# _ _ _ #
|
||||
# | |_ _ _ __ (_) ___| |__ _____ __ #
|
||||
# | | | | | '_ \| |/ _ \ '_ \ / _ \ \/ / #
|
||||
# | | |_| | | | | | __/ |_) | (_) > < #
|
||||
# |_|\__,_|_| |_|_|\___|_.__/ \___/_/\_\ #
|
||||
# #
|
||||
##########################################
|
||||
|
||||
Welcome to luniebox setup. This script will perfom some interactive
|
||||
steps to install and configure everything neccessary to convert this
|
||||
device into a standalone spotify music box with RFID functionality.
|
||||
|
||||
If you want to run the script automatically, press n and restart script with -a option.
|
||||
|
||||
Run the script with -h to get more information about possible options to be applied for setup process.
|
||||
"
|
||||
read -rp "Do you want to setup luniebox now? [Y/n] " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[nN][oO] | [nN])
|
||||
echo "Bye...."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
setup
|
||||
}
|
||||
|
||||
setup() {
|
||||
cd /home/pi
|
||||
if [[ ${NO_UPDATES} == "false" ]]; then
|
||||
update
|
||||
fi
|
||||
setup_software
|
||||
set_device_name
|
||||
setup_rfid
|
||||
setup_mpu
|
||||
if [[ ${NO_AUDIO} == "false" ]]; then
|
||||
setup_audio
|
||||
fi
|
||||
finalize
|
||||
}
|
||||
|
||||
update() {
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
read -rp "Do you want to update system now? [Y/n] (recommended) " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[nN][oO] | [nN])
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "Update system"
|
||||
sudo apt update
|
||||
sudo apt upgrade -y
|
||||
}
|
||||
|
||||
setup_software() {
|
||||
setup_app
|
||||
if [[ ${NO_SPOTIFY} == "false" ]]; then
|
||||
setup_spotify
|
||||
fi
|
||||
|
||||
if [[ ${NO_MPD} == "false" ]]; then
|
||||
setup_mpd
|
||||
fi
|
||||
if [[ ${NO_SPOTIFYDL} == "false" ]]; then
|
||||
setup_zspotify
|
||||
fi
|
||||
}
|
||||
|
||||
setup_app() {
|
||||
echo "Install required packages..."
|
||||
sudo apt install -y git python3-venv python3-pip
|
||||
echo "Fetch luniebox repository..."
|
||||
git clone https://git.bstly.de/Lurkars/luniebox.git /home/pi/luniebox
|
||||
cd /home/pi/luniebox/application
|
||||
echo "Setup python venv..."
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
export CFLAGS=-fcommon
|
||||
echo "Install pip requirements..."
|
||||
pip install -r requirements.txt
|
||||
deactivate
|
||||
mkdir /home/pi/luniebox/config
|
||||
echo "Setup config and systemd services..."
|
||||
cp /home/pi/luniebox/contrib/config/luniebox.cfg /home/pi/luniebox/config/luniebox.cfg
|
||||
sudo cp /home/pi/luniebox/contrib/service/luniebox-app.service /etc/systemd/system/
|
||||
sudo cp /home/pi/luniebox/contrib/service/luniebox-daemon.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable luniebox-app luniebox-daemon
|
||||
}
|
||||
|
||||
setup_spotify() {
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
read -rp "Do you want to install spotifyd for Spofity support? [Y/n] " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[nN][oO] | [nN])
|
||||
NO_SPOTIFY=true
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
mkdir /home/pi/luniebox/bin
|
||||
echo "Download spotifyd..."
|
||||
wget -c https://github.com/Spotifyd/spotifyd/releases/download/v0.3.3/spotifyd-linux-armv6-slim.tar.gz -O - | tar -xz -C /home/pi/luniebox/bin
|
||||
echo "Setup config and systemd services..."
|
||||
if [[ ${NO_AUDIO} == "true" ]]; then
|
||||
echo "WARNING: you may need to adjust the file '/home/pi/luniebox/config/spotifyd.cfg' manually to setup your audio device!"
|
||||
echo ""
|
||||
fi
|
||||
cp /home/pi/luniebox/contrib/config/spotifyd.cfg /home/pi/luniebox/config/spotifyd.cfg
|
||||
sudo cp /home/pi/luniebox/contrib/service/spotifyd.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable spotifyd
|
||||
}
|
||||
|
||||
setup_mpd() {
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
read -rp "Do you want to install mpd (Music Player Daemon) for offline file support? [Y/n] " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[nN][oO] | [nN])
|
||||
NO_MPD=true
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
mkdir /home/pi/luniebox/library
|
||||
echo "Install required packages..."
|
||||
sudo apt install -y mpd
|
||||
echo "Setup config..."
|
||||
if [[ ${NO_AUDIO} == "true" ]]; then
|
||||
echo "WARNING: you may need to adjust the file '/etc/mpd.conf' manually to setup your audio device!"
|
||||
echo ""
|
||||
fi
|
||||
sudo cp /home/pi/luniebox/contrib/config/mpd.conf /etc/mpd.conf
|
||||
}
|
||||
|
||||
setup_zspotify() {
|
||||
if [[ ${NO_SPOTIFY} == "false" ]] && [[ ${NO_MPD} == "false" ]]; then
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
read -rp "Do you want to install zspotify for downloading Spotify tracks for offline support? [Y/n] " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[nN][oO] | [nN])
|
||||
NO_MPD=true
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "Fetch clspotify repository..."
|
||||
git clone https://github.com/agent255/clspotify.git /home/pi/clspotify
|
||||
cd /home/pi/clspotify
|
||||
echo "Setup python venv..."
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
echo "Install pip requirements..."
|
||||
pip install -r requirements.txt
|
||||
deactivate
|
||||
echo "Setup config..."
|
||||
sed -i "s/^zspotify_path =.*$/zspotify_path = \/home\/pi\/clspotify\//" /home/pi/luniebox/config/luniebox.cfg
|
||||
fi
|
||||
}
|
||||
|
||||
set_device_name() {
|
||||
if [[ ${AUTOMATIC} == "false" ]] && [[ ${DEVICE_NAME} == "false" ]]; then
|
||||
read -rp "Name for your Luniebox devices [default: luniebox]: " DEVICE_NAME
|
||||
fi
|
||||
|
||||
if ! [[ $DEVICE_NAME ]] || [[ ${DEVICE_NAME} == "false" ]]; then
|
||||
DEVICE_NAME="luniebox"
|
||||
fi
|
||||
|
||||
echo "Set device name to '${DEVICE_NAME}'"
|
||||
|
||||
printf "${DEVICE_NAME}" | sudo tee /etc/hostname 1>/dev/null
|
||||
sudo sed -i "/^127.0.0.1/s/$/ ${DEVICE_NAME}/g" /etc/hosts
|
||||
|
||||
if [[ NO_SPOTIFY == "false" ]]; then
|
||||
sed -i "s/^device_name.*=.*$/device_name = \"${DEVICE_NAME}\"/" /home/pi/luniebox/config/spotifyd.cfg
|
||||
fi
|
||||
}
|
||||
|
||||
setup_rfid() {
|
||||
echo "Enable SPI for RFID reader"
|
||||
sudo sed -i "/dtparam=spi=on/s/^#//g" /boot/config.txt
|
||||
}
|
||||
|
||||
setup_mpu() {
|
||||
if [[ ${AUTOMATIC} == "false" ]] && [[ ${MPU} == "false" ]]; then
|
||||
read -rp "Do you want to setup I2C for MPU9250 9-axis sensor? [y/N] " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[yY][eE][sS] | [yY])
|
||||
MPU=true
|
||||
break
|
||||
;;
|
||||
*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ ${MPU} == "true" ]]; then
|
||||
echo "Install I2C software, enable i2c bus 4, enable mpu"
|
||||
sudo apt install -y i2c-tools python3-smbus
|
||||
sudo sed -i "/dtparam=i2c_arm=on/s/^#//g" /boot/config.txt
|
||||
printf "dtoverlay=i2c-gpio,bus=4,i2c_gpio_delay_us=1,i2c_gpio_sda=23,i2c_gpio_scl=24" | sudo tee -a /boot/config.txt 1>/dev/null
|
||||
sed -i "s/^mpu =.*$/mpu = True/" /home/pi/luniebox/config/luniebox.cfg
|
||||
fi
|
||||
}
|
||||
|
||||
setup_audio() {
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
echo "Choose your audio hardware"
|
||||
select response in "${AUDIO_DEVICES[@]}"; do
|
||||
echo ""
|
||||
AUDIO_DEVICE = "$response"
|
||||
done
|
||||
fi
|
||||
|
||||
case "$AUDIO_DEVICE" in
|
||||
"pimoroni")
|
||||
setup_pimoroni
|
||||
;;
|
||||
"adafruit")
|
||||
setup_adafruit
|
||||
;;
|
||||
*)
|
||||
NO_AUDIO=true
|
||||
echo "WARNING: please setup your audio device manually!"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
setup_pimoroni() {
|
||||
echo "Setup Pimoroni Shim"
|
||||
sudo sed -i "/dtparam=audio=on/s/^/#/g" /boot/config.txt
|
||||
printf "dtoverlay=hifiberry-dac\ngpio=25=op,dh" | sudo tee -a /boot/config.txt 1>/dev/null
|
||||
}
|
||||
|
||||
setup_adafruit() {
|
||||
echo "Setup Adafruit Speaker Bonnet"
|
||||
sudo sed -i "/dtparam=audio=on/s/^/#/g" /boot/config.txt
|
||||
printf "dtoverlay=hifiberry-dac\ndtoverlay=i2s-mmap" | sudo tee -a /boot/config.txt 1>/dev/null
|
||||
cat /home/pi/luniebox/contrib/config/asound.conf | sudo tee /etc/asound.conf 1>/dev/null
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if [[ ${AUTOMATIC} == "false" ]]; then
|
||||
read -rp "Setup finished and reboot required. Do you want to automatically reboot your luniebox now? [Y/n] " response
|
||||
echo ""
|
||||
case "$response" in
|
||||
[nN][oO] | [nN])
|
||||
echo "Bye...."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "Rebooting now...."
|
||||
sleep 2
|
||||
sudo reboot
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
Loading…
Reference in New Issue
Block a user