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