initialize
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
bin
|
||||
venv
|
||||
__pycache__
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "ExtendedMFRC522"
|
||||
|
||||
from mfrc522 import SimpleMFRC522
|
||||
|
||||
|
||||
class ExtendedMFRC522(SimpleMFRC522):
|
||||
|
||||
def __init__(self, start_section=1, sections=0, blocks_per_section=4, block_size=16, encoding='utf8'):
|
||||
self.START_SECTION = start_section
|
||||
self.SECTIONS = sections
|
||||
self.BLOCKS_PER_SECTIONS = blocks_per_section
|
||||
self.DATA_BLOCKS = self.BLOCKS_PER_SECTIONS - 1
|
||||
self.BLOCK_SIZE = block_size
|
||||
self.ENCODING = encoding
|
||||
if self.START_SECTION < 1:
|
||||
self.START_SECTION = 1
|
||||
super().__init__()
|
||||
|
||||
def read_no_block(self):
|
||||
(status, size) = self.READER.MFRC522_Request(self.READER.PICC_REQIDL)
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
(status, uid) = self.READER.MFRC522_Anticoll()
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
id = self.uid_to_num(uid)
|
||||
self.READER.MFRC522_SelectTag(uid)
|
||||
data = bytearray()
|
||||
|
||||
start_section = self.START_SECTION
|
||||
if start_section > size:
|
||||
start_section = size - 1
|
||||
|
||||
sections = size - start_section
|
||||
if self.SECTIONS > 0 and self.SECTIONS <= sections:
|
||||
sections = self.SECTIONS
|
||||
|
||||
for section in range(start_section, start_section + sections):
|
||||
trailer_block = section * self.BLOCKS_PER_SECTIONS + self.DATA_BLOCKS
|
||||
status = self.READER.MFRC522_Auth(
|
||||
self.READER.PICC_AUTHENT1A, trailer_block, self.KEY, uid)
|
||||
if status == self.READER.MI_OK:
|
||||
for i in range(self.DATA_BLOCKS):
|
||||
block_addr = section * self.BLOCKS_PER_SECTIONS + i
|
||||
block = self.READER.MFRC522_Read(block_addr)
|
||||
if block:
|
||||
data.extend(bytearray(block))
|
||||
else:
|
||||
return None, None
|
||||
|
||||
text_read = ''
|
||||
if data:
|
||||
while data and data[0] == 0:
|
||||
data.pop(0)
|
||||
while data and data[-1] == 0:
|
||||
data.pop()
|
||||
if data:
|
||||
text_read = data.decode(self.ENCODING)
|
||||
self.READER.MFRC522_StopCrypto1()
|
||||
return id, text_read
|
||||
|
||||
def write_no_block(self, text):
|
||||
(status, size) = self.READER.MFRC522_Request(self.READER.PICC_REQIDL)
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
(status, uid) = self.READER.MFRC522_Anticoll()
|
||||
if status != self.READER.MI_OK:
|
||||
return None, None
|
||||
id = self.uid_to_num(uid)
|
||||
self.READER.MFRC522_SelectTag(uid)
|
||||
|
||||
start_section = self.START_SECTION
|
||||
if start_section > size:
|
||||
start_section = size - 1
|
||||
|
||||
sections = size - start_section
|
||||
if self.SECTIONS > 0 and self.SECTIONS <= sections:
|
||||
sections = self.SECTIONS
|
||||
|
||||
data = text.strip().encode(self.ENCODING)
|
||||
data_sections = [data[i:i + self.BLOCK_SIZE * self.DATA_BLOCKS]
|
||||
for i in range(0, len(data), self.BLOCK_SIZE * self.DATA_BLOCKS)]
|
||||
|
||||
for section in range(start_section, start_section + sections):
|
||||
trailer_block = section * self.BLOCKS_PER_SECTIONS + self.DATA_BLOCKS
|
||||
status = self.READER.MFRC522_Auth(
|
||||
self.READER.PICC_AUTHENT1A, trailer_block, self.KEY, uid)
|
||||
self.READER.MFRC522_Read(trailer_block)
|
||||
|
||||
section_data = bytearray(self.DATA_BLOCKS * self.BLOCK_SIZE)
|
||||
if len(data_sections) > (section - start_section):
|
||||
section_data = bytearray(
|
||||
data_sections[section - start_section])
|
||||
section_data.extend(
|
||||
bytearray(self.DATA_BLOCKS * self.BLOCK_SIZE - len(section_data)))
|
||||
|
||||
if status == self.READER.MI_OK:
|
||||
for i in range(self.DATA_BLOCKS):
|
||||
block_addr = section * self.BLOCKS_PER_SECTIONS + i
|
||||
block_data = section_data[(
|
||||
i*self.BLOCK_SIZE):(i+1)*self.BLOCK_SIZE]
|
||||
self.READER.MFRC522_Write(
|
||||
block_addr, block_data)
|
||||
self.READER.MFRC522_StopCrypto1()
|
||||
return id, data[0:(len(self.BLOCK_ADDRS) * self.BLOCK_SIZE * sections)].decode(self.ENCODING)
|
||||
@@ -0,0 +1,176 @@
|
||||
from functools import wraps
|
||||
from flask import Blueprint, request, abort, redirect, url_for, jsonify
|
||||
from urllib.parse import urlencode
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
from luniebox import luniebox
|
||||
from spotifydl import SpotifyDLStatus
|
||||
import util
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
|
||||
|
||||
def api_key_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not request.headers.has_key('Authorization') or request.headers.get('Authorization') != luniebox.get_setting('API', 'API_KEY'):
|
||||
abort(401)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@api.route("/")
|
||||
@api_key_required
|
||||
def index():
|
||||
return jsonify("test")
|
||||
|
||||
|
||||
@api.route("/setup", methods=['POST'])
|
||||
def setup():
|
||||
spotify_username = request.form.get('spotify-username')
|
||||
spotify_password = request.form.get('spotify-password')
|
||||
spotify_client_id = request.form.get('spotify-client-id')
|
||||
spotify_client_secret = request.form.get('spotify-client-secret')
|
||||
spotify_redirect_uri = request.form.get('spotify-redirect-uri')
|
||||
spotify_device_name = request.form.get('spotify-device-name')
|
||||
luniebox.set_setting('luniebox', 'setup', True)
|
||||
luniebox.set_setting('spotify', 'client_id', spotify_client_id)
|
||||
luniebox.set_setting('spotify', 'client_secret', spotify_client_secret)
|
||||
luniebox.set_setting('spotify', 'redirect_uri', spotify_redirect_uri)
|
||||
|
||||
luniebox.spotify.set_setting('username', spotify_username)
|
||||
luniebox.spotify.set_setting('password', spotify_password)
|
||||
luniebox.spotify.set_setting('device_name', spotify_device_name)
|
||||
|
||||
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
|
||||
|
||||
return redirect(url_for('api.spotify_authorize'))
|
||||
|
||||
|
||||
spotify_authorize_state = False
|
||||
|
||||
|
||||
@api.route("/spotify/authorize", methods=['GET'])
|
||||
def spotify_authorize():
|
||||
client_id = luniebox.get_setting('spotify', 'client_id')
|
||||
redirect_uri = luniebox.get_setting('spotify', 'redirect_uri')
|
||||
global spotify_authorize_state
|
||||
spotify_authorize_state = util.randomString(16)
|
||||
scope = 'user-read-playback-state user-modify-playback-state user-read-private'
|
||||
return redirect('https://accounts.spotify.com/authorize?' +
|
||||
urlencode({
|
||||
'response_type': 'code',
|
||||
'client_id': client_id,
|
||||
'scope': scope,
|
||||
'redirect_uri': redirect_uri,
|
||||
'state': spotify_authorize_state
|
||||
}))
|
||||
|
||||
|
||||
@api.route("/spotify/callback", methods=['GET'])
|
||||
def spotify_callback():
|
||||
code = request.args['code']
|
||||
returnedState = request.args['state']
|
||||
global spotify_authorize_state
|
||||
if not returnedState or returnedState != spotify_authorize_state:
|
||||
abort(403)
|
||||
|
||||
luniebox.spotify.new_access_token(code)
|
||||
|
||||
return redirect(url_for('pages.index'))
|
||||
|
||||
|
||||
@api.route("/play", methods=['GET'])
|
||||
def play():
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
uri = request.args['uri']
|
||||
luniebox.play(uri)
|
||||
return redirect(url_for('pages.success', play=uri))
|
||||
|
||||
|
||||
@api.route("/spotify/dl", methods=['GET'])
|
||||
def spotify_dl():
|
||||
if luniebox.spotifydl_connect():
|
||||
uri = request.args['uri']
|
||||
dlStatus = luniebox.spotifydl.download(uri)
|
||||
return redirect(url_for('pages.spotifydl', status=dlStatus, uri=uri))
|
||||
else:
|
||||
return redirect(url_for('pages.spotifydl', status=SpotifyDLStatus.DISABLED))
|
||||
|
||||
|
||||
@api.route("/spotify/downloads", methods=['GET'])
|
||||
def spotify_downloads():
|
||||
if luniebox.spotifydl_connect():
|
||||
return jsonify(luniebox.spotifydl.getDownloads())
|
||||
else:
|
||||
return redirect(url_for('pages.error', error='spotifydownloads'))
|
||||
|
||||
|
||||
@api.route("/rfid/write", methods=['GET'])
|
||||
def rfid_write():
|
||||
value = request.args['value']
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
luniebox.rfid_write(value)
|
||||
logging.getLogger('luniebox').info("Write to RFID: " + value)
|
||||
return redirect(url_for('pages.success', rfid_write=value))
|
||||
|
||||
|
||||
@api.route("/rfid/read", methods=['GET'])
|
||||
def rfid_read():
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
value = luniebox.rfid_readOnce()
|
||||
return redirect(url_for('pages.success', rfid_read=value))
|
||||
|
||||
|
||||
@api.route("/rfid/play", methods=['GET'])
|
||||
def rfid_play():
|
||||
p = subprocess.Popen(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
p.communicate()
|
||||
value = luniebox.rfid_readOnce()
|
||||
if luniebox.play(value):
|
||||
return redirect(url_for('pages.success', rfid_play=value))
|
||||
else:
|
||||
return redirect(url_for('pages.error', error='play'))
|
||||
|
||||
|
||||
@api.route("/spotifyd/restart", methods=['GET'])
|
||||
def restart_spotifyd():
|
||||
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
|
||||
return redirect(url_for('pages.success', restart_spotifyd=True))
|
||||
|
||||
|
||||
@api.route("/mpd/restart", methods=['GET'])
|
||||
def restart_mpd():
|
||||
subprocess.run(["sudo", "systemctl", "restart", "mpd"])
|
||||
return redirect(url_for('pages.success', restart_mpd=True))
|
||||
|
||||
|
||||
@api.route("/mpd/list", methods=['GET'])
|
||||
def mpd_list():
|
||||
if 'path' in request.args:
|
||||
results = luniebox.mpd_list(request.args['path'])
|
||||
else:
|
||||
results = luniebox.mpd_list()
|
||||
return jsonify(results)
|
||||
|
||||
|
||||
@api.route("/daemon/start", methods=['GET'])
|
||||
def daemon_start():
|
||||
subprocess.run(["sudo", "systemctl", "start", "luniebox-daemon"])
|
||||
return redirect(url_for('pages.success', start_daemon=True))
|
||||
|
||||
|
||||
@api.route("/daemon/stop", methods=['GET'])
|
||||
def daemon_stop():
|
||||
subprocess.run(["sudo", "systemctl", "stop", "luniebox-daemon"])
|
||||
return redirect(url_for('pages.success', stop_daemon=True))
|
||||
|
||||
|
||||
@api.route("/restart", methods=['GET'])
|
||||
def restart_app():
|
||||
subprocess.run(["sudo", "systemctl", "restart", "luniebox-app"])
|
||||
return redirect(url_for('pages.success', restart_luniebox=True))
|
||||
@@ -0,0 +1,31 @@
|
||||
import sys
|
||||
from flask import Flask
|
||||
|
||||
import logging
|
||||
from luniebox import luniebox
|
||||
from api import api
|
||||
from pages import pages
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(api, url_prefix='/api')
|
||||
app.register_blueprint(pages)
|
||||
|
||||
loglevel = 'INFO'
|
||||
if luniebox.get_setting('logging', 'level'):
|
||||
loglevel = luniebox.get_setting('logging', 'level')
|
||||
|
||||
logger = logging.getLogger('luniebox')
|
||||
logger.setLevel(logging._nameToLevel[loglevel])
|
||||
logFormatter = logging.Formatter(
|
||||
style='{', datefmt='%Y-%m-%d %H:%M:%S', fmt='{asctime} {levelname}: {message}')
|
||||
logstdoutHandler = logging.StreamHandler(sys.stdout)
|
||||
logstdoutHandler.setFormatter(logFormatter)
|
||||
logger.addHandler(logstdoutHandler)
|
||||
|
||||
if __name__ != '__main__':
|
||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||
app.logger.handlers = gunicorn_logger.handlers
|
||||
app.logger.setLevel(gunicorn_logger.level)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0')
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import signal
|
||||
import time
|
||||
import sys
|
||||
import logging
|
||||
from luniebox import luniebox
|
||||
import RPi.GPIO as GPIO
|
||||
from mpu9250_jmdev.registers import *
|
||||
from mpu9250_jmdev.mpu_9250 import MPU9250
|
||||
|
||||
|
||||
class LunieboxDaemon(object):
|
||||
|
||||
def __init__(self, luniebox, input1=7, input2=8, skip_thresh=0.5, wind_thresh=0.3):
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
self.luniebox = luniebox
|
||||
self.input1 = input1
|
||||
self.input2 = input2
|
||||
self.skip_thresh = skip_thresh
|
||||
self.wind_thresh = wind_thresh
|
||||
GPIO.setup(self.input1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
GPIO.setup(self.input2, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
self.tolerance = self.luniebox.get_setting(
|
||||
'rfid', 'pause_tolerance', 4)
|
||||
self.reads = 0
|
||||
|
||||
if self.luniebox.get_setting('hardware', 'mpu') == 'True':
|
||||
self.mpu = MPU9250(
|
||||
address_ak=AK8963_ADDRESS,
|
||||
address_mpu_master=MPU9050_ADDRESS_68,
|
||||
address_mpu_slave=None,
|
||||
bus=4,
|
||||
gfs=GFS_1000,
|
||||
afs=AFS_8G,
|
||||
mfs=AK8963_BIT_16,
|
||||
mode=AK8963_MODE_C100HZ)
|
||||
self.mpu.configure()
|
||||
else:
|
||||
self.mpu = False
|
||||
|
||||
def run(self):
|
||||
logging.getLogger('luniebox').info("run luniebox")
|
||||
|
||||
while True:
|
||||
# mpu
|
||||
if self.mpu:
|
||||
acc = self.mpu.readAccelerometerMaster()
|
||||
rot_x = acc[0]
|
||||
rot_y = acc[1]
|
||||
|
||||
if rot_x > self.skip_thresh:
|
||||
self.luniebox.previous()
|
||||
time.sleep(1)
|
||||
elif rot_x < (self.skip_thresh * -1):
|
||||
self.luniebox.next()
|
||||
time.sleep(1)
|
||||
|
||||
if rot_y < (self.wind_thresh * -1):
|
||||
self.luniebox.fastforward()
|
||||
time.sleep(0.1)
|
||||
elif rot_y > self.wind_thresh:
|
||||
self.luniebox.rewind()
|
||||
time.sleep(0.1)
|
||||
|
||||
# buttons
|
||||
down_state = GPIO.input(self.input1)
|
||||
if down_state == False:
|
||||
self.luniebox.vol_down()
|
||||
time.sleep(0.1)
|
||||
|
||||
up_state = GPIO.input(self.input2)
|
||||
if up_state == False:
|
||||
self.luniebox.vol_up()
|
||||
time.sleep(0.1)
|
||||
|
||||
# rfid
|
||||
id, text = self.luniebox.reader.read_no_block()
|
||||
if text != None:
|
||||
text = text.strip()
|
||||
|
||||
if text == None:
|
||||
self.reads += 1
|
||||
if self.reads >= self.tolerance:
|
||||
self.reads = 0
|
||||
self.luniebox.pause()
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
self.reads = 0
|
||||
self.luniebox.play(text)
|
||||
time.sleep(0.1)
|
||||
|
||||
def signal_handler(self, signal, frame):
|
||||
logging.getLogger('luniebox').info(
|
||||
"Caught signal {}, exiting...".format(signal))
|
||||
luniebox.stop()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
daemon = LunieboxDaemon(luniebox=luniebox)
|
||||
signal.signal(signal.SIGINT, daemon.signal_handler)
|
||||
signal.signal(signal.SIGTERM, daemon.signal_handler)
|
||||
|
||||
daemon.run()
|
||||
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "Luniebox"
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
from enum import Enum
|
||||
from configparser import ConfigParser
|
||||
import RPi.GPIO as GPIO
|
||||
from spotifydl import SpotifyDL, SpotifyDLStatus
|
||||
from mpd import MPDClient
|
||||
from ExtendedMFRC522 import ExtendedMFRC522
|
||||
|
||||
from spotify import Spotify
|
||||
import util
|
||||
|
||||
|
||||
defaultConfigFilePath = '../config/luniebox.cfg'
|
||||
|
||||
|
||||
class PlayerService(Enum):
|
||||
NONE = 0
|
||||
SPOTIFY = 1
|
||||
MPD = 2
|
||||
|
||||
|
||||
class Luniebox(object):
|
||||
|
||||
def __init__(self, configFilePath=defaultConfigFilePath):
|
||||
self.configFilePath = configFilePath
|
||||
self.read_config()
|
||||
|
||||
GPIO.setwarnings(False)
|
||||
self.reader = ExtendedMFRC522(encoding="ascii")
|
||||
|
||||
loglevel = 'INFO'
|
||||
if self.get_setting('logging', 'level'):
|
||||
loglevel = self.get_setting('logging', 'level')
|
||||
|
||||
logger = logging.getLogger('luniebox')
|
||||
logger.setLevel(logging._nameToLevel[loglevel])
|
||||
logFormatter = logging.Formatter(
|
||||
style='{', datefmt='%Y-%m-%d %H:%M:%S', fmt='{asctime} {levelname}: {message}')
|
||||
logstdoutHandler = logging.StreamHandler(sys.stdout)
|
||||
logstdoutHandler.setFormatter(logFormatter)
|
||||
logger.addHandler(logstdoutHandler)
|
||||
|
||||
if self.get_setting('mpd', 'disabled', '') != 'True':
|
||||
self.mpd = MPDClient()
|
||||
self.mpd.host = "localhost"
|
||||
self.mpd.port = 6600
|
||||
self.mpd.timeout = 10
|
||||
if self.mpd_connect():
|
||||
logging.getLogger('luniebox').debug("connected to mpd")
|
||||
else:
|
||||
logging.getLogger('luniebox').info("mpd disabled")
|
||||
self.mpd = False
|
||||
|
||||
if self.get_setting('spotify', 'disabled', '') != 'True':
|
||||
self.spotify = Spotify(luniebox=self)
|
||||
if self.spotify_connect():
|
||||
logging.getLogger('luniebox').debug("connected to spotify")
|
||||
|
||||
self.zspotify_path = False
|
||||
self.spotifydl = False
|
||||
self.zspotify_path = self.get_setting('spotify', 'zspotify_path')
|
||||
if self.zspotify_path and self.mpd:
|
||||
self.spotifydl_connect()
|
||||
|
||||
else:
|
||||
logging.getLogger('luniebox').info("spotify disabled")
|
||||
self.spotify = False
|
||||
|
||||
if not self.config.has_option('api', 'api_key'):
|
||||
self.set_setting('api', 'api_key', util.randomString(64))
|
||||
|
||||
self.current = self.get_setting('luniebox', 'current')
|
||||
|
||||
self.service = PlayerService.NONE
|
||||
|
||||
if self.current != None:
|
||||
if self.current.startswith("spotify:"):
|
||||
self.service = PlayerService.SPOTIFY
|
||||
elif self.current.startswith("mpd:"):
|
||||
self.service = PlayerService.MPD
|
||||
|
||||
self.volume_max = int(self.get_setting('luniebox', 'volume_max', 100))
|
||||
self.volume = int(self.get_setting(
|
||||
'luniebox', 'volume', self.volume_max))
|
||||
self.volume_step = int(self.get_setting('luniebox', 'volume_step', 5))
|
||||
self.wind_step = int(self.get_setting('luniebox', 'wind_step', 10))
|
||||
self.resume = True
|
||||
|
||||
def mpd_connect(self):
|
||||
if not self.get_setting('luniebox', 'setup'):
|
||||
return False
|
||||
|
||||
if not self.mpd:
|
||||
logging.getLogger('luniebox').warn("mpd disabled!")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.mpd.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
tries = 0
|
||||
while tries < 5:
|
||||
try:
|
||||
self.mpd.connect(self.mpd.host, self.mpd.port)
|
||||
return True
|
||||
except:
|
||||
subprocess.call(["systemctl", "restart", "mpd"])
|
||||
time.sleep(1)
|
||||
tries += 1
|
||||
|
||||
logging.getLogger('luniebox').error(
|
||||
"Could not connect to MPD service!")
|
||||
return False
|
||||
|
||||
def spotify_connect(self, restart=False, max_tries=5):
|
||||
if not self.get_setting('luniebox', 'setup'):
|
||||
return False
|
||||
|
||||
if not self.spotify:
|
||||
logging.getLogger('luniebox').warn("spotify disabled!")
|
||||
return False
|
||||
|
||||
if restart:
|
||||
subprocess.run(["sudo", "systemctl", "restart", "spotifyd"])
|
||||
|
||||
tries = 0
|
||||
spotifyd_status = subprocess.call(
|
||||
["systemctl", "is-active", "--quiet", "spotifyd"])
|
||||
|
||||
while spotifyd_status != 0 and tries < max_tries:
|
||||
subprocess.call(["systemctl", "restart", "spotifyd"])
|
||||
time.sleep(1)
|
||||
tries += 1
|
||||
spotifyd_status = subprocess.call(
|
||||
["systemctl", "is-active", "--quiet", "spotifyd"])
|
||||
|
||||
if spotifyd_status == 0:
|
||||
return True
|
||||
|
||||
logging.getLogger('luniebox').error("spotifyd service not running!")
|
||||
return False
|
||||
|
||||
def spotifydl_connect(self):
|
||||
if not self.get_setting('luniebox', 'setup'):
|
||||
return False
|
||||
|
||||
if self.spotifydl:
|
||||
return True
|
||||
|
||||
if not self.zspotify_path or not self.mpd:
|
||||
logging.getLogger('luniebox').warn("spotifydl disabled!")
|
||||
return False
|
||||
|
||||
username = self.spotify.get_setting("username")
|
||||
password = self.spotify.get_setting("password")
|
||||
root = self.get_setting('mpd', 'library_path')
|
||||
try:
|
||||
self.spotifydl = SpotifyDL(
|
||||
self.zspotify_path, username, password, root)
|
||||
logging.getLogger('luniebox').info("spotifydl enabled!")
|
||||
return True
|
||||
except Exception as ex:
|
||||
logging.getLogger('luniebox').warning(
|
||||
"error on setup spotifydl: " + str(ex))
|
||||
return False
|
||||
|
||||
def read_config(self):
|
||||
configParser = ConfigParser()
|
||||
dataset = configParser.read(self.configFilePath)
|
||||
if len(dataset) != 1:
|
||||
raise ValueError(
|
||||
"Config file {} not found!".format(self.configFilePath))
|
||||
self.config = configParser
|
||||
|
||||
def get_setting(self, section, key, default=None):
|
||||
if self.config.has_option(section, key):
|
||||
return self.config[section][key]
|
||||
return default
|
||||
|
||||
def set_setting(self, section, key, value):
|
||||
if not self.config.has_section(section):
|
||||
self.config.add_section(section)
|
||||
self.config.set(section, key, str(value))
|
||||
|
||||
with open(self.configFilePath, 'w') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def rfid_write(self, data):
|
||||
self.reader.write(data.strip())
|
||||
|
||||
def rfid_readOnce(self):
|
||||
id, text = self.reader.read()
|
||||
logging.getLogger('luniebox').debug(
|
||||
"Once ID: %s Text: %s" % (id, text))
|
||||
return text.strip()
|
||||
|
||||
def vol_up(self):
|
||||
self.change_volume(self.volume_step)
|
||||
|
||||
def vol_down(self):
|
||||
self.change_volume(-1 * self.volume_step)
|
||||
|
||||
def change_volume(self, change):
|
||||
self.volume += change
|
||||
|
||||
if self.volume > self.volume_max:
|
||||
self.volume = self.volume_max
|
||||
|
||||
if self.volume < 0:
|
||||
self.volume = 0
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.volume(self.volume)
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.setvol(self.volume)
|
||||
|
||||
self.set_setting('luniebox', 'volume', str(self.volume))
|
||||
logging.getLogger('luniebox').debug("vol: " + str(self.volume))
|
||||
|
||||
def fastforward(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('seek ff')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.fastforward(self.wind_step)
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.seekcur(self.wind_step)
|
||||
|
||||
def rewind(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('seek rew')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.rewind(self.wind_step)
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.seekcur(-1 * self.wind_step)
|
||||
|
||||
def next(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('next')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.next()
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.next()
|
||||
|
||||
def previous(self):
|
||||
if not self.resume:
|
||||
logging.getLogger('luniebox').debug('prev')
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotify_connect():
|
||||
self.spotify.previous()
|
||||
elif self.service == PlayerService.MPD and self.mpd_connect():
|
||||
self.mpd.previous()
|
||||
|
||||
def pause(self):
|
||||
if not self.resume:
|
||||
if self.mpd_connect():
|
||||
self.mpd.pause(1)
|
||||
logging.getLogger('luniebox').debug("pause mpd")
|
||||
if self.spotify_connect(max_tries=2):
|
||||
self.spotify.pause()
|
||||
logging.getLogger('luniebox').debug("pause spotify")
|
||||
|
||||
self.resume = True
|
||||
|
||||
def play(self, text):
|
||||
if text != "":
|
||||
if text.startswith("spotify:"):
|
||||
self.service = PlayerService.SPOTIFY
|
||||
elif text.startswith("mpd:"):
|
||||
self.service = PlayerService.MPD
|
||||
|
||||
if self.service == PlayerService.SPOTIFY and self.spotifydl_connect():
|
||||
downloadStatus = self.spotifydl.downloadStatus(text)
|
||||
if downloadStatus == SpotifyDLStatus.FINISHED:
|
||||
self.mpd.update(text.replace('mpd:', ''))
|
||||
self.service = PlayerService.MPD
|
||||
elif self.get_setting('spotify', 'auto_download') == 'True' and downloadStatus == SpotifyDLStatus.NONE or downloadStatus == SpotifyDLStatus.ERROR:
|
||||
self.spotifydl.download(text)
|
||||
|
||||
if self.service == PlayerService.SPOTIFY:
|
||||
if text != self.current:
|
||||
if self.spotify_connect():
|
||||
self.spotify.volume(self.volume)
|
||||
if self.spotify.play(text):
|
||||
self.current = text
|
||||
self.set_setting(
|
||||
'luniebox', 'current', self.current)
|
||||
self.resume = False
|
||||
logging.getLogger('luniebox').debug(
|
||||
"play spotify: " + self.current)
|
||||
else:
|
||||
logging.getLogger('luniebox').warn(
|
||||
"cannot play spotify: " + self.current)
|
||||
elif self.resume and text == self.current:
|
||||
if self.spotify_connect():
|
||||
self.spotify.volume(self.volume)
|
||||
play = self.current
|
||||
if self.spotify.is_active():
|
||||
play = None
|
||||
if self.spotify.play(play):
|
||||
self.resume = False
|
||||
logging.getLogger('luniebox').debug(
|
||||
"resume spotify: " + self.current)
|
||||
else:
|
||||
logging.getLogger('luniebox').warn(
|
||||
"cannot resume spotify: " + self.current)
|
||||
elif self.service == PlayerService.MPD:
|
||||
if text != self.current:
|
||||
if self.mpd_connect():
|
||||
self.mpd.setvol(self.volume)
|
||||
self.mpd.clear()
|
||||
text = text.replace('mpd:', '')
|
||||
self.mpd.add(text)
|
||||
self.mpd.play()
|
||||
self.current = text
|
||||
self.set_setting('luniebox', 'current', self.current)
|
||||
self.resume = False
|
||||
if text.startswith('spotify:'):
|
||||
logging.getLogger('luniebox').debug(
|
||||
"play spotify from mpd: " + text)
|
||||
else:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"play mpd: " + self.current)
|
||||
elif self.resume and text == self.current:
|
||||
if self.mpd_connect():
|
||||
self.mpd.setvol(self.volume)
|
||||
self.mpd.play()
|
||||
self.resume = False
|
||||
if text.startswith('spotify:'):
|
||||
logging.getLogger('luniebox').debug(
|
||||
"resume spotify from mpd: " + text)
|
||||
else:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"resume mpd: " + self.current)
|
||||
|
||||
elif text != None:
|
||||
logging.getLogger('luniebox').info(
|
||||
"invalid value(?): " + str(text))
|
||||
|
||||
def stop(self):
|
||||
self.pause()
|
||||
|
||||
def sort_mpd(self, object):
|
||||
if 'directory' in object:
|
||||
return object['directory']
|
||||
elif 'file' in object:
|
||||
return object['file']
|
||||
return ''
|
||||
|
||||
def mpd_list(self, path=''):
|
||||
if self.mpd_connect():
|
||||
try:
|
||||
result = self.mpd.listfiles(path)
|
||||
return sorted(result, key=self.sort_mpd)
|
||||
except:
|
||||
return []
|
||||
return None
|
||||
|
||||
def mpd_status(self):
|
||||
if self.mpd_connect():
|
||||
status = self.mpd.status()
|
||||
status['song'] = self.mpd.currentsong()
|
||||
return status
|
||||
return None
|
||||
|
||||
|
||||
luniebox = Luniebox()
|
||||
@@ -0,0 +1,189 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request
|
||||
from functools import wraps
|
||||
import subprocess
|
||||
|
||||
from luniebox import luniebox
|
||||
|
||||
pages = Blueprint('pages', __name__)
|
||||
|
||||
|
||||
def setup_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not luniebox.get_setting('luniebox', 'setup'):
|
||||
return redirect(url_for('pages.setup'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@pages.route("/setup")
|
||||
def setup():
|
||||
model = {}
|
||||
|
||||
if luniebox.get_setting('luniebox', 'setup'):
|
||||
model['setup'] = True
|
||||
|
||||
model['client-id'] = luniebox.get_setting('spotify', 'client_id')
|
||||
model['client-secret'] = luniebox.get_setting('spotify', 'client_secret')
|
||||
model['redirect-uri'] = luniebox.get_setting('spotify', 'redirect_uri')
|
||||
|
||||
model['username'] = luniebox.spotify.get_setting('username', '')
|
||||
model['password'] = luniebox.spotify.get_setting('password', '')
|
||||
model['device-name'] = luniebox.spotify.get_setting('device_name', '')
|
||||
|
||||
model['host'] = request.host_url
|
||||
|
||||
if model['host'].endswith("/"):
|
||||
model['host'] = model['host'][:len(model['host']) - 1]
|
||||
|
||||
if not model['redirect-uri']:
|
||||
model['redirect-uri'] = model['host'] + url_for('api.spotify_callback')
|
||||
|
||||
return render_template('setup.html', model=model)
|
||||
|
||||
|
||||
@pages.route("/")
|
||||
@setup_required
|
||||
def index():
|
||||
devices = luniebox.spotify.devices()
|
||||
device_id = luniebox.get_setting('spotify', 'device_id')
|
||||
device_name = luniebox.spotify.get_setting('device_name')
|
||||
|
||||
mpdStatus = luniebox.mpd_status()
|
||||
|
||||
spotifyStatus = {
|
||||
'status': False,
|
||||
'device_name': device_name,
|
||||
'device_id': device_id,
|
||||
'errors': {'setup': True, 'not_running': True}
|
||||
}
|
||||
|
||||
if not device_id:
|
||||
spotifyStatus['errors']['not_running'] = False
|
||||
for device in devices['devices']:
|
||||
if device['name'] == device_name:
|
||||
device_id = device['id']
|
||||
luniebox.set_setting('spotify', 'device_id', device_id)
|
||||
spotifyStatus['errors']['setup'] = False
|
||||
else:
|
||||
spotifyStatus['errors']['setup'] = False
|
||||
for device in devices['devices']:
|
||||
if device['id'] == device_id:
|
||||
spotifyStatus['errors']['not_running'] = False
|
||||
|
||||
if spotifyStatus['errors']['not_running'] and luniebox.spotify.transfer_playback():
|
||||
spotifyStatus['errors']['not_running'] = False
|
||||
|
||||
if not spotifyStatus['errors']['setup'] and not spotifyStatus['errors']['not_running']:
|
||||
spotifyStatus['status'] = luniebox.spotify.playback_state()
|
||||
|
||||
daemon_error = False
|
||||
daemon_status = subprocess.call(
|
||||
["systemctl", "is-active", "--quiet", "luniebox-daemon"])
|
||||
|
||||
if daemon_status == 0:
|
||||
daemon_status = "Running"
|
||||
else:
|
||||
daemon_error = "Not running"
|
||||
daemon_status = False
|
||||
|
||||
return render_template('index.html', spotify=spotifyStatus, mpd=mpdStatus, daemon_status=daemon_status, daemon_error=daemon_error)
|
||||
|
||||
|
||||
@pages.route("/success")
|
||||
@setup_required
|
||||
def success():
|
||||
success = {}
|
||||
if 'play' in request.args:
|
||||
success['play'] = request.args['play']
|
||||
|
||||
if 'spotify_dl' in request.args:
|
||||
success['spotify_dl'] = request.args['spotify_dl']
|
||||
|
||||
if 'restart_spotifyd' in request.args:
|
||||
success['restart_spotifyd'] = request.args['restart_spotifyd']
|
||||
|
||||
if 'restart_mpd' in request.args:
|
||||
success['restart_mpd'] = request.args['restart_mpd']
|
||||
|
||||
if 'start_daemon' in request.args:
|
||||
success['start_daemon'] = request.args['start_daemon']
|
||||
|
||||
if 'stop_daemon' in request.args:
|
||||
success['stop_daemon'] = request.args['stop_daemon']
|
||||
|
||||
if 'rfid_play' in request.args:
|
||||
success['rfid_play'] = request.args['rfid_play']
|
||||
|
||||
if 'rfid_read' in request.args:
|
||||
success['rfid_read'] = request.args['rfid_read']
|
||||
|
||||
if 'rfid_write' in request.args:
|
||||
success['rfid_write'] = request.args['rfid_write']
|
||||
|
||||
return render_template('success.html', success=success)
|
||||
|
||||
|
||||
@pages.route("/error")
|
||||
@setup_required
|
||||
def error():
|
||||
error = False
|
||||
if 'error' in request.args:
|
||||
error = request.args['error']
|
||||
|
||||
return render_template('error.html', error=error)
|
||||
|
||||
|
||||
@pages.route("/spotify/search", methods=['GET', 'POST'])
|
||||
def spotify_search():
|
||||
query = '' if not 'q' in request.args else request.args['q']
|
||||
types = [
|
||||
'track', 'album'] if not 'type' in request.args else request.args.getlist('type')
|
||||
offset = 0 if not 'offset' in request.args else request.args['offset']
|
||||
|
||||
limit = 5
|
||||
if not 'limit' in request.args:
|
||||
if len(types) == 1:
|
||||
limit = 20
|
||||
if len(types) == 2:
|
||||
limit = 10
|
||||
else:
|
||||
limit = int(request.args['limit'])
|
||||
|
||||
results = False if not query else luniebox.spotify.search(
|
||||
query, types, limit, offset)
|
||||
|
||||
if results:
|
||||
for result in results.values():
|
||||
if 'items' in result:
|
||||
for item in result['items']:
|
||||
if luniebox.spotifydl_connect():
|
||||
item['spotifydl'] = str(
|
||||
luniebox.spotifydl.downloadStatus(item['uri']))
|
||||
|
||||
return render_template('spotify_search.html', results=results, query=query, types=types, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@pages.route("/spotify/download", methods=['GET'])
|
||||
def spotifydl():
|
||||
status = False
|
||||
if 'status' in request.args:
|
||||
status = request.args['status']
|
||||
uri = False
|
||||
if 'uri' in request.args:
|
||||
uri = request.args['uri']
|
||||
return render_template('spotifydl.html', status=status, uri=uri)
|
||||
|
||||
|
||||
@pages.route("/mpd/list", methods=['GET'])
|
||||
def mpd_list():
|
||||
path = ''
|
||||
spotify = False
|
||||
if 'path' in request.args:
|
||||
path = request.args['path']
|
||||
|
||||
if 'spotify' in request.args:
|
||||
spotify = True
|
||||
|
||||
results = luniebox.mpd_list(path)
|
||||
return render_template('mpd.html', results=results, path=path, spotify=spotify)
|
||||
@@ -0,0 +1,9 @@
|
||||
DateTime==4.3
|
||||
Flask==2.0.2
|
||||
gunicorn==20.1.0
|
||||
mfrc522==0.0.7
|
||||
mpu9250-jmdev==1.0.12
|
||||
python-dateutil==2.8.2
|
||||
python-mpd2==3.0.4
|
||||
requests==2.26.0
|
||||
smbus2==0.4.1
|
||||
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "Spotify"
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
from configparser import ConfigParser
|
||||
from dateutil import parser
|
||||
from urllib.parse import urlencode
|
||||
import requests
|
||||
import json
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
|
||||
defaultSpotifyConfigFilePath = '../config/spotifyd.cfg'
|
||||
|
||||
|
||||
class Spotify(object):
|
||||
|
||||
def __init__(self, luniebox, configFilePath=defaultSpotifyConfigFilePath):
|
||||
self.luniebox = luniebox
|
||||
self.configFilePath = configFilePath
|
||||
self.read_config()
|
||||
|
||||
def read_config(self):
|
||||
configParser = ConfigParser()
|
||||
dataset = configParser.read(self.configFilePath)
|
||||
if len(dataset) != 1:
|
||||
raise ValueError(
|
||||
"Config file {} not found!".format(self.configFilePath))
|
||||
self.config = configParser
|
||||
|
||||
def get_setting(self, key, default=None):
|
||||
if self.config.has_option('global', key):
|
||||
return self.config['global'][key].strip('\"')
|
||||
return default
|
||||
|
||||
def set_setting(self, key, value):
|
||||
if not self.config.has_section('global'):
|
||||
self.config.add_section('global')
|
||||
self.config.set('global', key, str('\"' + value + '\"'))
|
||||
|
||||
with open(self.configFilePath, 'w') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def get_status(self):
|
||||
devices = self.devices()
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
device_name = self.get_setting('device_name')
|
||||
status = {
|
||||
'status': False,
|
||||
'device_name': device_name,
|
||||
'device_id': device_id,
|
||||
'errors': {'setup': True, 'not_running': True}
|
||||
}
|
||||
|
||||
if not device_id:
|
||||
status['errors']['not_running'] = False
|
||||
for device in devices['devices']:
|
||||
if device['name'] == device_name:
|
||||
device_id = device['id']
|
||||
self.luniebox.get_setting(
|
||||
'spotify', 'device_id', device_id)
|
||||
status['errors']['setup'] = False
|
||||
else:
|
||||
status['errors']['setup'] = False
|
||||
for device in devices['devices']:
|
||||
if device['id'] == device_id:
|
||||
status['errors']['not_running'] = False
|
||||
|
||||
if status['errors']['not_running'] and self.transfer_playback():
|
||||
status['errors']['not_running'] = False
|
||||
|
||||
return status
|
||||
|
||||
def get_access_token(self):
|
||||
access_token = self.luniebox.get_setting('spotify', 'access_token')
|
||||
if not access_token:
|
||||
return False
|
||||
|
||||
token_expires = parser.parse(
|
||||
self.luniebox.get_setting('spotify', 'token_expires'))
|
||||
|
||||
if token_expires < datetime.datetime.now():
|
||||
self.refresh_access_token()
|
||||
return self.get_access_token()
|
||||
|
||||
return access_token
|
||||
|
||||
def new_access_token(self, code):
|
||||
clientId = self.luniebox.get_setting('spotify', 'client_id')
|
||||
clientSecret = self.luniebox.get_setting('spotify', 'client_secret')
|
||||
redirectUri = self.luniebox.get_setting('spotify', 'redirect_uri')
|
||||
|
||||
credentials = clientId + ":" + clientSecret
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + str(base64.b64encode(credentials.encode('utf-8')), 'utf-8')
|
||||
}
|
||||
|
||||
formData = {
|
||||
'code': code,
|
||||
'redirect_uri': redirectUri,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://accounts.spotify.com/api/token', headers=headers, data=formData)
|
||||
|
||||
tokenData = response.json()
|
||||
|
||||
self.luniebox.set_setting(
|
||||
'spotify', 'access_token', tokenData['access_token'])
|
||||
self.luniebox.set_setting('spotify', 'refresh_token',
|
||||
tokenData['refresh_token'])
|
||||
self.luniebox.set_setting('spotify', 'token_expires', str(datetime.datetime.now(
|
||||
) + datetime.timedelta(seconds=tokenData['expires_in'])))
|
||||
|
||||
def refresh_access_token(self):
|
||||
clientId = self.luniebox.get_setting('spotify', 'client_id')
|
||||
clientSecret = self.luniebox.get_setting('spotify', 'client_secret')
|
||||
refresh_token = self.luniebox.get_setting('spotify', 'refresh_token')
|
||||
|
||||
if not refresh_token:
|
||||
return False
|
||||
|
||||
credentials = clientId + ":" + clientSecret
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Basic ' + str(base64.b64encode(credentials.encode('utf-8')), 'utf-8')
|
||||
}
|
||||
|
||||
formData = {
|
||||
'refresh_token': refresh_token,
|
||||
'grant_type': 'refresh_token'
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://accounts.spotify.com/api/token', headers=headers, data=formData)
|
||||
|
||||
tokenData = response.json()
|
||||
|
||||
self.luniebox.set_setting(
|
||||
'spotify', 'access_token', tokenData['access_token'])
|
||||
self.luniebox.set_setting('spotify', 'token_expires', str(datetime.datetime.now(
|
||||
) + datetime.timedelta(seconds=tokenData['expires_in'])))
|
||||
|
||||
def api_call(self, url):
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
return requests.get(
|
||||
'https://api.spotify.com/v1' + url, headers=headers)
|
||||
|
||||
def search(self, query, types, limit, offset=0):
|
||||
if limit > 50:
|
||||
limit = 50
|
||||
response = self.api_call(
|
||||
'/search?' + urlencode({'q': query, 'type': ','.join(types), 'limit': limit, 'offset': offset}))
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.search(query, types, limit, offset)
|
||||
|
||||
return False
|
||||
|
||||
def playback_state(self):
|
||||
response = self.api_call('/me/player')
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
if response.status_code == 204:
|
||||
return {"not_active": True}
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.playback_state()
|
||||
|
||||
return False
|
||||
|
||||
def transfer_playback(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
data = {
|
||||
'device_ids': [device_id],
|
||||
'play': False
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player', headers=headers, data=json.dumps(data))
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.transfer_playback()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify transfer playback", response, response.text)
|
||||
return False
|
||||
|
||||
def is_active(self):
|
||||
self.transfer_playback()
|
||||
state = self.playback_state()
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
if not state:
|
||||
return False
|
||||
return 'device' in state and 'id' in state['device'] and state['device']['id'] == device_id
|
||||
|
||||
def devices(self):
|
||||
response = self.api_call('/me/player/devices')
|
||||
return response.json()
|
||||
|
||||
def play(self, uri):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
data = {}
|
||||
|
||||
if uri:
|
||||
if uri.startswith('spotify:track:'):
|
||||
data = {
|
||||
"uris": [uri]
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"context_uri": uri
|
||||
}
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/play?' +
|
||||
urlencode({'device_id': device_id}), headers=headers, data=json.dumps(data))
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.play(uri)
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify play", response, response.text)
|
||||
return False
|
||||
|
||||
def pause(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/pause?' +
|
||||
urlencode({'device_id': device_id}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.pause()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify pause", response, response.text)
|
||||
return False
|
||||
|
||||
def volume(self, value):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
if value < 0:
|
||||
value = 0
|
||||
if value > 100:
|
||||
value = 100
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/volume?' +
|
||||
urlencode({'device_id': device_id, 'volume_percent': value}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.volume(value)
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify volume", response, response.text)
|
||||
return False
|
||||
|
||||
def seek(self, position):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
if position < 0:
|
||||
position = 0
|
||||
|
||||
response = requests.put(
|
||||
'https://api.spotify.com/v1/me/player/seek?' +
|
||||
urlencode({'device_id': device_id, 'position_ms': position}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.seek(position)
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify seek", response, response.text)
|
||||
return False
|
||||
|
||||
def rewind(self, seconds):
|
||||
state = self.playback_state()
|
||||
if state and 'progress_ms' in state:
|
||||
progress = int(state['progress_ms'])
|
||||
progress -= seconds * 1000
|
||||
return self.seek(progress)
|
||||
|
||||
return False
|
||||
|
||||
def fastforward(self, seconds):
|
||||
state = self.playback_state()
|
||||
if state and 'progress_ms' in state:
|
||||
progress = int(state['progress_ms'])
|
||||
progress += seconds * 1000
|
||||
return self.seek(progress)
|
||||
|
||||
return False
|
||||
|
||||
def previous(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://api.spotify.com/v1/me/player/previous?' +
|
||||
urlencode({'device_id': device_id}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.previous()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify previous", response, response.text)
|
||||
return False
|
||||
|
||||
def next(self):
|
||||
device_id = self.luniebox.get_setting('spotify', 'device_id')
|
||||
access_token = self.get_access_token()
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://api.spotify.com/v1/me/player/next?' +
|
||||
urlencode({'device_id': device_id}), headers=headers)
|
||||
|
||||
if response.status_code == 204 or response.status_code == 202:
|
||||
return True
|
||||
elif response.status_code == 404 and self.luniebox.spotify_connect(restart=True):
|
||||
return self.next()
|
||||
|
||||
logging.getLogger('luniebox').warn(
|
||||
"error on spotify next", response, response.text)
|
||||
return False
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__name__ = "SpotifyDL"
|
||||
|
||||
import subprocess
|
||||
from configparser import ConfigParser
|
||||
import os.path
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
|
||||
defaultCredentialsLocation = '../config/zspotify.credentials'
|
||||
|
||||
|
||||
class SpotifyDLStatus(Enum):
|
||||
NONE = "none"
|
||||
RUNNING = "running"
|
||||
FINISHED = "finished"
|
||||
ERROR = "error"
|
||||
DISABLED = "disabled"
|
||||
|
||||
|
||||
class SpotifyDL():
|
||||
|
||||
def __init__(self, zspotify_path, username, password, root, credentialsLocation=defaultCredentialsLocation):
|
||||
if zspotify_path:
|
||||
self.zspotify_path = zspotify_path
|
||||
else:
|
||||
raise ValueError("No zspotify path provivded!")
|
||||
if credentialsLocation:
|
||||
self.credentialsLocation = credentialsLocation
|
||||
else:
|
||||
raise ValueError("No credentialsLocation provivded!")
|
||||
if not username:
|
||||
raise ValueError("No username provided!")
|
||||
if not password:
|
||||
raise ValueError("No password provided!")
|
||||
if root:
|
||||
if not root.endswith("/"):
|
||||
root = root + "/"
|
||||
self.root = root
|
||||
else:
|
||||
raise ValueError("No root provided!")
|
||||
|
||||
if not os.path.isfile(self.credentialsLocation):
|
||||
logging.getLogger('luniebox').info("initialize zspotify")
|
||||
p = subprocess.Popen([self.zspotify_path + 'venv/bin/python', self.zspotify_path + 'zspotify/__main__.py', '-s',
|
||||
'--credentials-location', self.credentialsLocation], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=False, group="pi", user="pi")
|
||||
input = username + '\n' + password + '\n'
|
||||
out, err = p.communicate(input=input.encode())
|
||||
logging.getLogger('luniebox').info(out)
|
||||
logging.getLogger('luniebox').warn(err)
|
||||
|
||||
self.downloads = {}
|
||||
|
||||
def download(self, uri):
|
||||
status = self.downloadStatus(uri)
|
||||
if status == SpotifyDLStatus.NONE or status == SpotifyDLStatus.ERROR:
|
||||
logging.getLogger('luniebox').info(
|
||||
"start download of '" + uri + "'")
|
||||
trackprefix = ""
|
||||
if uri.startswith('spotify:album:'):
|
||||
trackprefix = "{album_num}. "
|
||||
elif uri.startswith('spotify:playlist:'):
|
||||
trackprefix = "{playlist_num}. "
|
||||
p = subprocess.Popen([self.zspotify_path + 'venv/bin/python', self.zspotify_path + 'zspotify/__main__.py', "--root-path", self.root,
|
||||
'--credentials-location', self.credentialsLocation, "--download-real-time", "True", "--chunk-size", "50000", "--skip-existing-files", "True", "--skip-previously-downloaded", "True", "--output", uri + "/" + trackprefix + "{artist} - {song_name}.{ext}", uri], stdin=None,
|
||||
stdout=None, stderr=None, close_fds=True, shell=False, group="pi", user="pi")
|
||||
self.downloads[uri] = p
|
||||
return SpotifyDLStatus.RUNNING
|
||||
elif status == SpotifyDLStatus.RUNNING:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"download for '" + uri + "' still running")
|
||||
elif status == SpotifyDLStatus.FINISHED:
|
||||
logging.getLogger('luniebox').debug(
|
||||
"alreaded downloaded '" + uri + "'")
|
||||
return status
|
||||
|
||||
def downloadStatus(self, uri):
|
||||
doneFiledPath = self.root + uri + '/.spotifydl'
|
||||
if uri in self.downloads:
|
||||
if self.downloads[uri]:
|
||||
poll = self.downloads[uri].poll()
|
||||
if poll != None:
|
||||
self.downloads.pop(uri)
|
||||
if poll == 0:
|
||||
p = subprocess.Popen(["touch", doneFiledPath], stdin=None,
|
||||
stdout=None, stderr=None, close_fds=True, shell=False, group="pi", user="pi")
|
||||
p.communicate()
|
||||
return SpotifyDLStatus.FINISHED
|
||||
logging.getLogger('luniebox').warn(
|
||||
"download of '" + uri + "' exited with: " + str(poll))
|
||||
return SpotifyDLStatus.ERROR
|
||||
return SpotifyDLStatus.RUNNING
|
||||
|
||||
if os.path.exists(doneFiledPath):
|
||||
return SpotifyDLStatus.FINISHED
|
||||
|
||||
return SpotifyDLStatus.NONE
|
||||
|
||||
def getDownloads(self):
|
||||
downloads = {}
|
||||
for uri in self.downloads.keys():
|
||||
downloads[uri] = str(self.downloadStatus(uri))
|
||||
return downloads
|
||||
+1556
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
img.item-image {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
max-height: 100px;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if error == 'play': %}
|
||||
<div class="my-3 alert alert-danger">
|
||||
Error on playing from card.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,134 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1>luniebox</h1>
|
||||
<p>Welcome to luniebox web-interface. This is for configuring and control your luniebox device.</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Daemon</h5>
|
||||
<p class="card-text">
|
||||
{% if daemon_error: %}
|
||||
<div class="my-3 alert alert-danger">
|
||||
{{ daemon_error }}
|
||||
<a href="{{ url_for('api.daemon_start') }}" class="btn btn-secondary">Start daemon</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if daemon_status: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
{{ daemon_status }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Spotify Integration</h5>
|
||||
<p class="card-text">
|
||||
{% if spotify['errors']['setup']: %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
No luniebox device found! Please setup spotifyd propertly (device name must match name from setup, default is 'luniebox')!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spotify['errors']['not_running']: %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Spotifyd is not running. Please start spotifyd service on your luniebox!
|
||||
<a href="{{ url_for('api.restart_spotifyd') }}" class="btn btn-primary">Restart spotifyd</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spotify['status']['not_active']: %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
Spotify is available but currently inactive.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{% if spotify['status']['device']: %}
|
||||
<tr class="{{ 'table-success' if spotify['status']['device']['id'] == spotify['device_id'] else '' }}">
|
||||
<th scope="row">Current Device</th>
|
||||
<td>{{ spotify['status']['device']['name'] }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if spotify['status']['item']: %}
|
||||
{% with item = spotify['status']['item'] %}
|
||||
<tr class="{{ '' if spotify['status']['is_playing'] else 'table-active' }}">
|
||||
<th>{{ 'Currently playing' if spotify['status']['is_playing'] else 'Last played' }}</th>
|
||||
<td class="d-flex w-100">
|
||||
{% include "spotify/items/track.html" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Music Player Daemon Integration</h5>
|
||||
<p class="card-text">
|
||||
{% if not mpd: %}
|
||||
<div class="my-3 alert alert-danger">
|
||||
Music Player Daemon not connected
|
||||
<a href="{{ url_for('api.restart_mpd') }}" class="btn btn-primary">Restart mpd</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if mpd: %}
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="table-success">
|
||||
<th scope="row">Current state</th>
|
||||
<td>{{ mpd['state'] }}</td>
|
||||
</tr>
|
||||
{% if mpd['song']: %}
|
||||
{% with item = mpd['song'] %}
|
||||
<tr class="{{ '' if mpd['state'] == 'play' else 'table-active' }}">
|
||||
<th>{{ 'Currently playing' if mpd['state'] == 'play' else 'Last played' }}</th>
|
||||
<td class="d-flex w-100">
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['title'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['artist'] }}
|
||||
</p>
|
||||
<small class="text-muted">{{ item['album'] }}</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="alert alert-warning">This will stop daemon. You need to restart daemon afterwards!</p>
|
||||
<a href="{{ url_for('api.rfid_read') }}" class="btn btn-primary">Read card</a>
|
||||
<a href="{{ url_for('api.rfid_play') }}" class="btn btn-secondary">Play card</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="mpd-search-results">
|
||||
<div class="container">
|
||||
|
||||
{% if path: %}
|
||||
<h3>{{ path }}</h3>
|
||||
{% endif %}
|
||||
<div class="d-flex flex-column mt-3">
|
||||
<ul class="list-group mb-3 flex-fill">
|
||||
{% for item in results %}
|
||||
{% if item.directory and (spotify and item.directory.startswith('spotify:') or not spotify and not item.directory.startswith('spotify:')): %}
|
||||
<li href="#" class="list-group-item flex-fill">
|
||||
<div class="d-flex w-100">
|
||||
<h5 class="mb-1"><a href="{{ url_for('pages.mpd_list', path=item.directory) }}">{{ item.directory }}</a>
|
||||
</h5>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value='mpd:' + item.directory) }}"><i
|
||||
class="bi bi-pencil-square"></i>
|
||||
Write to card</a>
|
||||
<a class="btn btn-secondary" href="{{ url_for('api.play', uri='mpd:' + item.directory) }}"><i
|
||||
class="bi bi-play"></i> Play
|
||||
now</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if item.file and not item.file.startswith('.') and (spotify and item.file.startswith('spotify:') or not spotify and not item.file.startswith('spotify:')): %}
|
||||
<li href="#" class="list-group-item flex-fill">
|
||||
<div class="d-flex w-100">
|
||||
<h5 class="mb-1">{{ item.file }}</h5>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value='mpd:' + path + '/' + item.file) }}"><i
|
||||
class="bi bi-pencil-square"></i>
|
||||
Write to card</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if not spotify: %}
|
||||
<a href="{{ url_for('pages.mpd_list', spotify=true) }}">List Spotify Downloads</a>
|
||||
{% endif %}
|
||||
{% if spotify: %}
|
||||
<a href="{{ url_for('pages.mpd_list') }}">List MPD files</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1> Setup </h1>
|
||||
<p>Here you can setup the luniebox or restart services when having issues.</p>
|
||||
|
||||
<div class="row">
|
||||
{% if model['setup']: %}
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="{{ url_for('api.restart_spotifyd') }}" class="btn btn-primary">Restart spotifyd</a>
|
||||
<a href="{{ url_for('api.daemon_start') }}" class="btn btn-secondary">Start daemon</a>
|
||||
<a href="{{ url_for('api.daemon_stop') }}" class="btn btn-warning">Stop daemon</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col col-xs-12 col-md-6 col-lg-6 col-xl-6 mt-3">
|
||||
{% if model['setup']: %}
|
||||
<div class="my-3 alert alert-warning">
|
||||
Already set-up! On changes you may need to restart spotifyd service.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('api.setup') }}">
|
||||
<div class="mb-3">
|
||||
<label for="spotify-device-name" class="form-label">Spotify Device Name</label>
|
||||
<input type="text" class="form-control" id="spotify-device-name" name="spotify-device-name" required
|
||||
value="{{'' if not model['device-name'] else '' + model['device-name'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-username" class="form-label">Spotify Username</label>
|
||||
<input type="text" class="form-control" id="spotify-username" name="spotify-username" required
|
||||
value="{{'' if not model['username'] else '' + model['username'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-password" class="form-label">Spotify Password</label>
|
||||
<input type="password" class="form-control" id="spotify-password" name="spotify-password" required
|
||||
value="{{'' if not model['password'] else '' + model['password'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-client-id" class="form-label">Spotify Client Id</label>
|
||||
<input type="text" class="form-control" id="spotify-client-id" name="spotify-client-id" required
|
||||
value="{{'' if not model['client-id'] else '' + model['client-id'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-client-secret" class="form-label">Spotify Client Secret</label>
|
||||
<input type="text" class="form-control" id="spotify-client-secret" name="spotify-client-secret" required
|
||||
value="{{'' if not model['client-secret'] else '' + model['client-secret'] }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="spotify-redirect-uri" class="form-label">Spotify Redirect Uri</label>
|
||||
<input type="text" class="form-control" id="spotify-redirect-uri" name="spotify-redirect-uri"
|
||||
value="{{'' if not model['redirect-uri'] else '' + model['redirect-uri'] }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Setup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['artists'] | map(attribute='name') | join(', ') }}
|
||||
</p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1"></p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="dropdown">
|
||||
<img src="{{ images | map(attribute='url') | first }}" class="item-image rounded dropdown-toggle"
|
||||
data-bs-toggle="dropdown" data-bs-placement="top" title="Download Cover">
|
||||
<ul class="dropdown-menu">
|
||||
{% for image in images %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{image['url']}}" target="_blank">{{image['width']}}px</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1"></p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="d-flex w-100 mt-3 justify-content-between">
|
||||
<a class="btn btn-primary" href="{{ url_for('api.rfid_write', value=item.uri) }}"><i class="bi bi-pencil-square"></i>
|
||||
Write to card</a>
|
||||
<a class="btn btn-secondary" href="{{ url_for('api.play', uri=item.uri) }}"><i class="bi bi-play"></i> Play
|
||||
now</a>
|
||||
{% if 'spotifydl' in item: %}
|
||||
{% if item['spotifydl'] == 'SpotifyDLStatus.NONE': %}
|
||||
<a class="btn btn-warning" href="{{ url_for('api.spotify_dl', uri=item.uri) }}"><i class="bi bi-save"></i>
|
||||
Download</a>
|
||||
{% else: %}
|
||||
<p>
|
||||
{% if item['spotifydl'] == 'SpotifyDLStatus.FINISHED': %}
|
||||
<span class="badge rounded-pill bg-success">Downloaded</span>
|
||||
{% elif item['spotifydl'] == 'SpotifyDLStatus.RUNNING': %}
|
||||
<span class="badge rounded-pill bg-info">Running</span>
|
||||
{% elif item['spotifydl'] == 'SpotifyDLStatus.ERROR': %}
|
||||
<span class="badge rounded-pill bg-danger">Error</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['description'] }}
|
||||
</p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1"></p>
|
||||
<small class="text-muted"></small>
|
||||
</div>
|
||||
{%if item['images']: %}
|
||||
{% set images = item['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<h5 class="mb-1">{{ item['name'] }}</h5>
|
||||
<p class="mb-1">
|
||||
{{ item['artists'] | map(attribute='name') | join(', ') }}
|
||||
</p>
|
||||
<small class="text-muted">{{ item['album']['name'] }}</small>
|
||||
</div>
|
||||
{%if item['album']['images']: %}
|
||||
{% set images = item['album']['images'] %}
|
||||
{% include "spotify/items/cover.html" %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,117 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="mt-3">
|
||||
<form method="GET" action="{{ url_for('pages.spotify_search') }}">
|
||||
<div class="mb-3">
|
||||
<label for="search-query" class="form-label">Search</label>
|
||||
<input type="text" name="q" id="search-query" class="form-control" placeholder="Artists, songs, or podcasts"
|
||||
required value="{{ query }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="track" id="search-type-track" {{'checked'
|
||||
if 'track' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-track">
|
||||
Tracks
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="album" id="search-type-album" {{'checked'
|
||||
if 'album' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-album">
|
||||
Albums
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="artist" id="search-type-artist"
|
||||
{{'checked' if 'artist' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-artist">
|
||||
Artists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="playlist" id="search-type-playlist"
|
||||
{{'checked' if 'playlist' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-playlist">
|
||||
Playlists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="show" id="search-type-show" {{'checked'
|
||||
if 'show' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-show">
|
||||
Shows
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="type" value="episode" id="search-type-episode"
|
||||
{{'checked' if 'episode' in types else '' }}>
|
||||
<label class="form-check-label" for="search-type-episode">
|
||||
Episodes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if results: %}
|
||||
<div class="alert alert-warning mt-3">
|
||||
Writing to card will stop the daemon. You need to restart daemon afterwards!
|
||||
</div>
|
||||
<div class="spotify-search-results">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% for type in types %}
|
||||
{% if results[type + 's']: %}
|
||||
{% with result = results[type + 's'] %}
|
||||
<div
|
||||
class="d-flex flex-column col col-xs-12 col-md-6 col-lg-6 {{ 'col-xl-4' if results|length > 2 else 'col-xl-6' }} mt-3">
|
||||
<h3>{{ type | capitalize}}s</h3>
|
||||
|
||||
<ul class="list-group mb-3 flex-fill">
|
||||
{% for item in result['items'] %}
|
||||
<li href="#" class="list-group-item flex-fill">
|
||||
<div class="d-flex w-100">
|
||||
{% include "spotify/items/" + type + ".html" %}
|
||||
</div>
|
||||
|
||||
{% include "spotify/items/menu.html" %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="d-flex w-100 justify-content-between mt-auto">
|
||||
<a href="{{ url_for('pages.spotify_search',q=query, type=type, limit=result['limit'], offset=result['offset']-result['limit']) }}"
|
||||
class="btn btn-primary {{ 'disabled' if result['offset']==0 else '' }} " {{ 'disabled' if
|
||||
result['offset']==0 else '' }}>«</a>
|
||||
|
||||
<a href="{{ url_for('pages.spotify_search',q=query, type=type, limit=result['limit'], offset=result['offset']+result['limit']) }}"
|
||||
{{ 'disabled' if result['offset']>= result['total'] else ''}}
|
||||
class="btn btn-primary {{ 'disabled' if result['offset']>= result['total'] else ''}}">»</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if uri: %}
|
||||
<h3>{{ uri }}</h3>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ status }}</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
|
||||
{% if success['rfid_write']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Write card value: {{ success['rfid_write'] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['rfid_read']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Read card value: {{ success['rfid_read'] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['rfid_play']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Start playing from card.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['play']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Start playing {{ success['play'] }}.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['restart_spotifyd']: %}
|
||||
<div class="my-3 alert alert-secondary">
|
||||
Restartet spotifyd service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['restart_mpd']: %}
|
||||
<div class="my-3 alert alert-secondary">
|
||||
Restartet mpd service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['start_daemon']: %}
|
||||
<div class="my-3 alert alert-success">
|
||||
Started luniebox-daemon service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['stop_daemon']: %}
|
||||
<div class="my-3 alert alert-warning">
|
||||
Stopped luniebox-daemon service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success['restart_luniebox']: %}
|
||||
<div class="my-3 alert alert-secondary">
|
||||
Restartet luniebox-app service.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-primary" href="{{ url_for('pages.index') }}">Go back to status page</a>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>luniebox</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_for('pages.index') }}">luniebox</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.index') }}">Status</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.spotify_search') }}">Search Spotify</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.mpd_list') }}">MPD listing</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{{ url_for('pages.setup') }}">Setup</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container mt-3">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<footer class="mt-5">
|
||||
|
||||
</footer>
|
||||
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def randomString(len):
|
||||
return ''.join(random.SystemRandom().choice(
|
||||
string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(len))
|
||||
Reference in New Issue
Block a user