Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Spotifyd instead of vollibrespot to get latest librespot changes #366

Merged
merged 16 commits into from
Nov 3, 2022
Merged
Prev Previous commit
Next Next commit
WIP
  • Loading branch information
klay2000 authored and linknum23 committed Nov 3, 2022
commit a08c553836dcf958162a42cdeff896a441cfc156
File renamed without changes.
84 changes: 37 additions & 47 deletions amplipi/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import amplipi.models as models
import amplipi.utils as utils
from amplipi.mpris import MPRIS

def write_config_file(filename, config):
""" Write a simple config file (@filename) with key=value pairs given by @config """
Expand Down Expand Up @@ -217,9 +218,10 @@ class Spotify(BaseStream):
""" A Spotify Stream """
def __init__(self, name, mock=False):
super().__init__('spotify', name, mock)
self.proc2 = None
self.metaport = None
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

self.connect_port = None
self.mpris = None

self.supported_cmds = {
'play': [0x05],
'pause': [0x04],
Expand Down Expand Up @@ -260,80 +262,68 @@ def connect(self, src):
os.system(f'cp {toml_template} {toml_useful}')

# Input the proper values
self.metaport = 5030 + 2*src
self.connect_port = 4070 + 2*src
with open(toml_useful, 'r') as TOML:
data = TOML.read()
data = data.replace('AmpliPi_TEMPLATE', f'{self.name}')
data = data.replace("device = 'ch'", f"device = 'ch{src}'")
data = data.replace('5030', f'{self.metaport}')
data = data.replace('device_name_in_spotify_connect', f'{self.name}')
data = data.replace("alsa_audio_device", f"ch{src}")
data = data.replace('1234', f'{self.connect_port}')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be awesome to expose bitrate and autoplay to the user if possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we support higher bit rates?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this in the future when we are able to better characterize utilization. We still need a 4th Spotify account!

with open(toml_useful, 'w') as TOML:
TOML.write(data)

# PROCESS
meta_args = ['python3', f'{utils.get_folder("streams")}/spot_meta.py', f'{self.metaport}', f'{src_config_folder}']
spotify_args = [f'{utils.get_folder("streams")}/vollibrespot']
spotify_args = [f'{utils.get_folder("streams")}/spotifyd', '--config-path', './config.toml']

try:
self.proc = subprocess.Popen(args=meta_args, preexec_fn=os.setpgrp)
self.proc2 = subprocess.Popen(args=spotify_args, cwd=f'{src_config_folder}')
self.proc = subprocess.Popen(args=spotify_args, cwd=f'{src_config_folder}')
time.sleep(0.1) # Delay a bit

self.mpris = MPRIS(f'spotifyd_{self.name}')

self._connect(src)
except Exception as exc:
print(f'error starting spotify: {exc}')

def disconnect(self):
if self._is_running():
os.killpg(os.getpgid(self.proc.pid), signal.SIGKILL)
self.proc2.kill()
self.proc.kill()
self._disconnect()
self.proc = None
self.proc2 = None

def info(self) -> models.SourceInfo:
src_config_folder = f'{utils.get_folder("config")}/srcs/{self.src}'
loc = f'{src_config_folder}/currentSong'
source = models.SourceInfo(
name=self.full_name(),
state=self.state,
img_url='static/imgs/spotify.png'
)

try:
with open(loc, 'r') as file:
d = {}
for line in file.readlines():
try:
d = ast.literal_eval(line)
except Exception as exc:
print(f'Error parsing currentSong: {exc}')
if d['state'] and d['state'] != 'stopped':
source.state = d['state']
source.artist = ', '.join(d['artist'])
source.track = d['track']
source.album = d['album']
source.supported_cmds=list(self.supported_cmds.keys())
else:
source.track = f'connect to {self.name}'
if d['img_url']: # report generic spotify image in place of unspecified album art
source.img_url = d['img_url']
md = self.mpris.metadata()

source.state = 'playing' if self.mpris.is_playing() else 'paused'
source.artist = md.artist
source.track = md.title
source.album = md.album
source.supported_cmds=list(self.supported_cmds.keys())
if md.art_url: # report generic spotify image in place of unspecified album art
source.img_url = md.art_url
except Exception:
pass

return source

def send_cmd(self, cmd):
""" Control of Spotify via commands sent over sockets
Commands include play, pause, next, and previous
Takes src as an input so that it knows which UDP port to send on
"""
udp_ip = "127.0.0.1" # AmpliPi's IP
udp_port = self.metaport + 1 # Adding 1 to the 'metaport' variable used in connect()

try:
if cmd in self.supported_cmds:
self.socket.sendto(bytes(self.supported_cmds[cmd]), (udp_ip, udp_port))
else:
raise NotImplementedError(f'"{cmd}" is either incorrect or not currently supported')
except Exception as exc:
raise RuntimeError(f'Command {cmd} failed to send: {exc}') from exc
if cmd in self.supported_cmds:
if cmd == 'play':
self.mpris.play()
elif cmd == 'pause':
self.mpris.pause()
elif cmd == 'next':
self.mpris.next()
elif cmd == 'prev':
self.mpris.previous()
else:
raise NotImplementedError(f'"{cmd}" is either incorrect or not currently supported')

class Pandora(BaseStream):
""" A Pandora Stream """
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
adafruit-circuitpython-rgb-display
aiofiles
dasbus
deepdiff
fastapi
fastapi_utils
pygobject
jinja2
loguru
mypy
Expand Down
35 changes: 22 additions & 13 deletions streams/spot_config.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
# Default configuration settings for Vollibrespot
# This template file will be updated as streams are connected
[Authentication]
device-name = 'AmpliPi_TEMPLATE'
[global]

[Playback] # required heading, we use system defaults
# If set to true, `spotifyd` tries to bind to the session dbus
# and expose MPRIS controls. When running headless, without a dbus session,
# then set this to false to avoid binding errors
use_mpris = true

[Output]
device = 'ch' # ch0-ch3 are accepted
initial-volume = 100
backend = 'alsa'
# The name that gets displayed under the connect tab on
# official clients. Spaces are not allowed!
device_name = "device_name_in_spotify_connect"

[Misc]
disable-audio-cache = true # Cache audio files (relies on local system for cleanup!)
cache-location = '/tmp' # Path to cache
metadata-port = 5030
# The audio bitrate. 96, 160 or 320 kbit/s
bitrate = 160

# The maximal size of the cache directory in bytes
max_cache_size = 100000000

# After the music playback has ended, start playing similar songs based on the previous tracks.
autoplay = true

# The port `spotifyd` uses to announce its service over the network.
zeroconf_port = 1234

# The displayed device type in Spotify clients.
device_type = "s_t_b"
Binary file added streams/spotifyd
Binary file not shown.