pimoroni / pirate-audio

Examples and documentation for the Pirate Audio range of Raspberry Pi add-ons
MIT License
239 stars 45 forks source link

Add support for Spotify Connect #17

Open moritz1000 opened 4 years ago

moritz1000 commented 4 years ago

I bought a Pirate Audio device because I thought it supports Spotify Connect but it turns out I can stream Spotify to it but no via Spotify Connect.

I could not find a way to add Spotify Connect to Mopidy.

I tried achieving this with Volumio since it supports external displays and GPIO control via plugins and also Spotify Connect but I could not even get the sound output to work.

To summarize I think Spotify Connect would be a great feature but I am unable to get it working.

Gadgetoid commented 4 years ago

I g uess both Volumio and Mopidy being music player daemons basically means they hog the audio ouput device and don't give things like raspotify a look in.

You should probably just install raspotify onto a fresh Raspbian and see how you go with that: https://github.com/dtcooper/raspotify

All of the display code we've got working thus far is Mopidy/mpd-centric and wont work with Raspotify. There surely must be a way to query Librespot for the currently playing song/album art/etc though.

Gadgetoid commented 4 years ago

Okay it looks like librespot (the grunt behind raspotify) supports an --onevent option that lets you configure it with an endpoint for handling track start/change/stop events: https://github.com/librespot-org/librespot/issues/185

The event type and releated track IDs are saved to environment variables:

https://github.com/librespot-org/librespot/blob/aa880f8888226a8e5fc6e1e54dfb7cf58176ac95/src/player_event_handler.rs#L18-L33

So all that remains - okay so this isn't exactly trivial - is to write a small Python handler that:

  1. Reads these environment variables
  2. Looks up the TRACK_ID on Spotify's API service
  3. Retrieves the track name, album art, etc
  4. Displays it on the Pirate Audio display
  5. Exits, leaving the display unchanged until the next event
Gadgetoid commented 4 years ago

This all said. VolumIO should work- I guess you set up the DAC in the System settings? I think the missing link is the enable pin. If you can edit /boot/config.txt then adding the line gpio=25=op,dh should get you up and running.

moritz1000 commented 4 years ago

Thank you for talking your time and writing such a detailed response.

I guess you set up the DAC in the System settings?

I looked for something called "dtoverlay=hifiberry-dac" as used for pirate audio but did not add gpio=25=op,dh to /boot/config.txt. There was a list of many different hifiberry dacs in VolumIO and I tried some of them.

First of all I will try to get Rasbian + raspotify + sound output working and then look into the rest but I guess this goes way over my head.

dogchunx commented 4 years ago

I've been curious about this too and just had a quick go at getting Spotify Connect playing through the Pirate Audo DAC via Raspotify.

Turns out all I needed to do run the "Easy" installation instructions as detailed in the Raspotify project page and then change the options in the config file to use the correct audio device, which in my case was:

OPTIONS="--device hw:1,0"

The LCD and buttons don't function doing it this way, but to anyone who just wants a basic way of controlling what Pirate Audio is playing via Spotify Connect, it doesn't seem like a bad solution. This also doesn't touch Mopidy, but this might be moot as what's playing can now be controlled from the Spotify client on any desktop or mobile device.

moritz1000 commented 4 years ago

Did you add OPTIONS="--device hw:1,0" in /etc/default/raspotify? Thank you for testing this.

dogchunx commented 4 years ago

Hey, no probs. That's where I added it, yep.

pokono commented 4 years ago

+1

shokwaav commented 4 years ago

Does raspotify show the media info on the LCD?

moritz1000 commented 4 years ago

Does raspotify show the media info on the LCD?

Sadly no. I still think that a real Spotify Connect integration would be so useful.

Abysmax commented 4 years ago

+1 I actually think this would be a great feature.

Ennneerrrgggyyy commented 3 years ago

I bought a Pirate Audio device because I thought it supports Spotify Connect but it turns out I can stream Spotify to it but no via Spotify Connect.

Oh no, same happened to me. I thought it would support Spotify Conect, too. It would be awesome if it would.

Gadgetoid commented 3 years ago

I think full Spotify Connect integration is probably something that needs to be handled in Mopidy Spotify. See this- somewhat dated (and locked)- thread here: https://github.com/mopidy/mopidy-spotify/issues/14

This slightly more current thread suggests that Raspotify and Mopidy can potentially better co-exist by pausing MPD when Raspotify is active. Although this would not include any visual indication of what's playing on screen (as mentioned above)- https://discourse.mopidy.com/t/mopidy-spotify-connect-raspotify/2397

That's, at least, a good start but getting Raspotify "now playing" to co-exist on a screen that's actively being driven by Mopidy will be quite tricky.

I'm imaging a new SD card to set up Mopidy and Raspotify side-by-side to see how bad it is. Least I can do is some discovery on how much of a headache this might be. I'm a Spotify user myself and can appreciate the desire to "beam" music to it 😆

Nothing I do will be as slick as direct Mopidy integration though.

Some time later....

Welp! I just wasted about an hour discovering that Mopidy's mpd plugin is no longer installed by default...

BUT I have come up with the most devious and contrived of scripts:

#!/bin/bash

if [ "$PLAYER_EVENT" = "start" ]; then
        mpc clear
        mpc add spotify:track:$TRACK_ID
        mpc play
        sleep 1
        mpc pause
elif [ "$PLAYER_EVENT" = "change" ]; then
        mpc clear
        mpc add spotify:track:$TRACK_ID
        mpc play
        sleep 1
        mpc pause
elif [ "$PLAYER_EVENT" = "stop" ]; then
        mpc play
elif [ "$PLAYER_EVENT" = "volume_set" ]; then
        mpc_volume=$(($VOLUME * 100 / 65536))
        mpc volume $mpc_volume
fi

You will need to sudo apt install mopidy-mpd mpc and test that mpc is working by running mpc play or something on the command line. It should show something like:

pi@raspberrypi:~ $ mpc play
Adagio - I'll Possess You
[playing] #1/1   0:00/5:48 (0%)
volume: 50%   repeat: off   random: off   single: off   consume: off

Now that's working I'll explain the ridiculous script above...

You will need to put this somewhere like /home/pi/raspotify.sh make sure you chmod +x /home/pi/raspotify.sh and then:

sudo nano /etc/default/raspotify

And add: --onevent /home/pi/raspotify.sh to your OPTIONS="" line.

Mine looks like this because I'm playing audio out of the Pi's headphone jack in both cases:

OPTIONS="--device hw:1,0 --onevent /home/pi/raspotify.sh"

Now when you play a song via Spotify Connect, Raspotify will call this hook, add it to Mopidy, play it so that it shows on the LCD and then pause it. I've tested this with my Spotify account set up on a Pi 4 and it appears to work. In fact I can play on Mopidy and Raspotify simultaneously if I want a clashing horrible racket.

I'll type this up more coherently when it's not 11pm, but I feel I'm onto something here.

Edit: I have updated the script so that it syncs Raspotify volume changes over to Mopidy, alas I don't think it's possibly to sync the other way, but this does mean your Raspotify volume shows on the LCD.

Thoughts

It's not very robust! I've been trying to sync the playing track better... but it really doesn't work well. People who just want to use Spotify Connect and not Mopidy should speak up, because I suspect I can build something leaner that doesn't involve Mopidy at all. The big difficulty there is sourcing the album art and track information, however. Right now Mopidy (even for those who don't necessarily want to use it directly) is a really good way to grab full details from a spotify Track ID. Rolling that from scratch wont be so fun.

Ennneerrrgggyyy commented 3 years ago

Thank you so much for your help @Gadgetoid !

I would'nt need Mopidy to be happy with my setup. :) Using my Raspi and the Pirate Audio as a Spotify Connect device with visible Track details and being able to control the volume etc. would be awesome for me.

Gadgetoid commented 3 years ago

Okay, take 2- no Mopidy edition. This is VERY BETA and needs A LOT OF WORK.

Right now it displays track info and album art (caching art locally) but does not show volume/transport information.

It will probably never support the buttons, since that requires a much more complex Spotify authentication context- but maybe that's feasible as a bigger, separate project. (I think this whole script could run totally separate of anything else and just use current_user_playing() to get the currently playing song on a poll.)

You will need to (possibly incomplete):

Go to https://developer.spotify.com/dashboard and set up a new app. Grab your Client ID and Secret and keep them handy.

sudo pip3 install spotipy pidi_display_st7789
mkdir -p /home/pi/spotipy/cache/
touch /home/pi/spotipy/hook.log
sudo chown raspotify:raspotify/home/pi/spotipy/hook.log
sudo usermod -a -G gpio raspotify
sudo usermod -a -G spi raspotify

Make sure your /etc/default/raspotify options contains --onevent /home/pi/spotipy/hook.sh, here's mine:

OPTIONS="--device hw:1,0 --onevent /home/pi/spotipy/hook.sh"

Make sure you restart raspotify:

sudo systemctl restart raspotify

Save this script in /home/pi/spotipy/hook.sh

#!/bin/bash

exec >> /home/pi/spotipy/hook.log
exec 2>&1

echo "Changing track: $TRACK_ID"

if [ "$PLAYER_EVENT" = "start" ]; then
        echo $TRACK_ID > /tmp/spotify_album_display
elif [ "$PLAYER_EVENT" = "change" ]; then
        echo $TRACK_ID > /tmp/spotify_album_display
elif [ "$PLAYER_EVENT" = "stop" ]; then
        echo $TRACK_ID > /tmp/spotify_album_display
elif [ "$PLAYER_EVENT" = "volume_set" ]; then
        sleep 0.1
fi

Save this script in /home/pi/spotipy/art.py:

import time
import os
import requests
from pathlib import Path
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy.util as util
import unittest.mock as mock
import argparse
import sys

FIFO_NAME = "/tmp/spotify_album_display"

class Display():
    """Base class to represent a pidi display output."""
    # pylint: disable=too-many-instance-attributes
    def __init__(self, args=None):
        """Initialise a new display."""
        self._size = args.size
        self._title = ''
        self._shuffle = False
        self._repeat = False
        self._state = ''
        self._volume = 0
        self._progress = 0
        self._elapsed = 0

        self._title = ''
        self._album = ''
        self._artist = ''

    def update_album_art(self, input_file):
        """Update the display album art."""
        raise NotImplementedError

    def update_overlay(self, shuffle, repeat, state, volume,
                       progress, elapsed, title, album, artist):
        """Update the display transport information."""
        # pylint: disable=too-many-arguments
        self._shuffle = shuffle
        self._repeat = repeat
        self._state = state
        self._volume = volume
        self._progress = progress
        self._elapsed = elapsed
        self._title = title
        self._album = album
        self._artist = artist

    def redraw(self):
        """Redraw the display."""
        raise NotImplementedError

    def add_args(argparse):  # pylint: disable=no-self-argument
        """Expand argparse instance with display-specific args."""

try:
    os.unlink(FIFO_NAME)
except (IOError, OSError):
    pass

os.mkfifo(FIFO_NAME)
os.chmod(FIFO_NAME, 0o777)

mock_display = mock.Mock()
mock_display.Display = Display
sys.modules["pidi.display"] = mock_display
import pidi_display_st7789

parser = argparse.ArgumentParser()
pidi_display_st7789.DisplayST7789.add_args(parser)

args = parser.parse_args()
args.size = 240

display = pidi_display_st7789.DisplayST7789(args)

auth_manager = SpotifyClientCredentials()
spotify = spotipy.Spotify(auth_manager=auth_manager)

with open(FIFO_NAME, "r") as fifo:
    while True:
        track_id = fifo.read().strip()
        if len(track_id) == 0:
            time.sleep(0.1)
            continue

        track = spotify.track(track_id)

        image_url = None
        album_id = track["album"]["id"]
        album_name = track["album"]["name"]
        artist_name = track["album"]["artists"][0]["name"]
        track_name = track["name"]

        for image in track["album"]["images"]:
            if image["height"] == 300:
                image_url = image["url"]

        image_cache_path = Path(f"/home/pi/spotipy/cache/{album_id}.png")
        if not image_cache_path.is_file():
            print("Missing cached image, loading now..")
            image = requests.get(image_url)
            with open(image_cache_path, "wb") as f:
                f.write(image.content)

        display.update_album_art(image_cache_path)
        display.update_overlay(
            False,
            False,
            "play",
            1,
            0,
            0,
            track_name,
            album_name,
            artist_name
        )

        # Crossfade album art fudge
        for x in range(30):
            display.redraw()
            time.sleep(1.0 / 30)

Now grab your Client ID and Secret from before and set them on your env, or save them into a script for easier setup:

export SPOTIPY_CLIENT_ID="YOUR CLIENT ID"
export SPOTIPY_CLIENT_SECRET="YOUR CLIENT SECRET"

Run art.py to test:

python3 /home/pi/spotipy/art.py

The display should light up with whatever was last written to it.

Test that the front end works by firing a track ID into the fifo:

echo "2yLMuUJ8xXBOzdbRE5ZkJW" > /tmp/spotify_album_display

You should see the album art and track info update on Pirate Audio with the song Ember by Kubbi.

Now connect to Raspotify and change some tracks to see if it works.

Ennneerrrgggyyy commented 3 years ago

That's awesome @Gadgetoid . I really appreciate your help.

To be honest the whole RPi, Linux, ssh etc. thing is fairly new to me. But I will definetely try to make it work and want to use the whole project to get a deeper understanding of everything.

Ennneerrrgggyyy commented 3 years ago

What I did: I've set up the RPi completely from scratch and installed Raspotify first. Afterwards I did all the steps you described. The only error I've encountered: sudo chown raspotify:raspotify/home/pi/spotipy/hook.log chown: missing operand after ‘raspotify:raspotify/home/pi/spotipy/hook.log’ Try 'chown --help' for more information.

I can't connect through Spotify Connect when "--onevent /home/pi/spotipy/hook.sh" is added. When I add only OPTIONS="--device hw:1,0" I can connect properly. BUT: I can hear no audio through lineout.

Do I need to install audio/video drivers for the Pirate Audio which normally would be included in pirate-audio/mopidy? Before starting from scratch it worked fine with Raspotify and Mopidy combined.

Also, when I run art.py: $ python3 /home/pi/spotipy/art.py Traceback (most recent call last): File "/home/pi/spotipy/art.py", line 80, in <module> display = pidi_display_st7789.DisplayST7789(args) File "/usr/local/lib/python3.7/dist-packages/pidi_display_st7789/__init__.py", line 21, in __init__ spi_speed_hz=args.spi_speed_mhz * 1000 * 1000 File "/usr/local/lib/python3.7/dist-packages/ST7789/__init__.py", line 124, in __init__ self._spi = spidev.SpiDev(port, cs) FileNotFoundError: [Errno 2] No such file or directory

Gadgetoid commented 3 years ago

Ah looks like you need to run sudo raspi-config and enable SPI.

Edit: Done a lot of work on this, and new code incoming shortly. Was important to me that volume/transport at least kinda matched and that the album art crossfade worked properly.

Gadgetoid commented 3 years ago

Updated code replaces hook.sh and art.py documented above- https://github.com/pimoroni/pidi-spotify

For the time being you will have to run it like so:

git clone https://github.com/pimoroni/pidi-spotify
cd pidi-spotify
python3 -m pidi_spotify

It will need the same auth as documented above.

This has:

I have no idea how librespot/Raspotify volume works. We could do local volume control but it would not sync up with Spotify Connect volume... ie if you turn the volume down locally you'll never be able to turn it back up again.

I feel like the A/B X/Y buttons should do something but since they can't control Spotify at all... I'm really not sure what that should be.

Ennneerrrgggyyy commented 3 years ago

Wow - I am really grateful and impressed how quickly you can hack something like this. I've already learned a lot today. I hope it is okay, to ask so many questions.

Is started with adding

dtoverlay=hifiberry-dac
gpio=25=op,dh 

to /boot/config.txt. Afterwards I executed the following:

sudo pip3 install spotipy pidi_display_st7789
mkdir -p /home/pi/spotipy/cache/
touch /home/pi/spotipy/hook.log
sudo chown raspotify:raspotify /home/pi/spotipy/hook.log
sudo usermod -a -G gpio raspotify
sudo usermod -a -G spi raspotify

Then I added

OPTIONS="--device hw:1,0 --onevent /home/pi/spotipy/hook.sh"

and executed

sudo systemctl restart raspotify

Afterwards I executed:

cd pidi-spotify
python3 -m pidi_spotify

Somehow the Terminal gets unresponsive at this point

pi@raspberrypi:~/pidi-spotify $ python3 -m pidi_spotify
open 5

Also the problem persists, that I can't connect through Spotify Connect when "--onevent /home/pi/spotipy/hook.sh" is added. When I add only OPTIONS="--device hw:1,0" I can connect properly.

Gadgetoid commented 3 years ago

You're most of the way there! I should mention that to test pidi_spotify you will need a second terminal Window, because the running application will block that terminal. You can exit with Ctrl+C. I haven't turned it into a system-service-like-thingy just yet.

I encountered the problem with adding a hook, where I wasn't able to log in. It could be that Raspotify cannot run hook.sh

Try: chmod +x /home/pi/spotipy/hook.sh

And make sure your hook.sh is updated ot the new one included with pidi-spotify.

Edit: I should clarify that chmod +x sets the execute bit on that script, allowing Raspotify to run it.

Ennneerrrgggyyy commented 3 years ago

Shouldn't the new script generate the new hook.sh? Or do I still have to sudo nano generate it?

Is it correct that "python3 -m pidi_spotify" kind of freezes with open 5?

pi@raspberrypi:~/spotipy $ chmod +x /home/pi/spotipy/hook.sh
chmod: changing permissions of '/home/pi/spotipy/hook.sh': Operation not permitted
Gadgetoid commented 3 years ago

You'll have to copy the "hook.sh" from the pidi-spotify directory copied down with git.

So assumping you're somewhere like: /home/pi/pidi-spotify you can cp hook.sh /home/pi/spotipy/hook.sh

And try: sudo chmod +x /home/pi/spotipy/hook.sh ... the Pi likes this simon-says nonsense 😆

(and yes python3 -m pidi_spotify will kind of freeze with "open 5" ... I should probably have put a more helpful message in there before pushing it up for testing!)

Ennneerrrgggyyy commented 3 years ago

oh boy! that did it. and it works really good. 😍

Ennneerrrgggyyy commented 3 years ago

So at the moment the whole thing only works while "python3 -m pidi_spotify" is running, right?

This is what you meant when you wrote that you haven't turned it into a system-service-like-thingy just yet, right?

Gadgetoid commented 3 years ago

That's correct! Yes. I need to create a way to configure pidi_spotify and write a script to run it as a service.

You can probably create a script called /home/pi/spotipy/start.sh and put something like this in it (changing values and/or directories as needed):

#!/bin/bash

cd /home/pi/pidi_spotify
export SPOTIPY_CLIENT_ID="YOUR CLIENT ID"
export SPOTIPY_CLIENT_SECRET="YOUR CLIENT SECRET"
python3 -m pidi_spotify

Then add it to your crontab by typing:

crontab -e

And then adding:

@reboot /home/pi/spotipy/start.sh

When it's finished I hope pidi_spotify will just be a program you can run either as a system service, or as a utility to control that system service, so we can get rid of hook.sh altogether and just have a command like pidi_spotify --raspotify do everything that hook does.... in theory anyway! This is all an elaborate proof of concept.

Edit (again): thanks for running through these steps and trying this early, it really helps me identify the sticking points and build a better application. Thanks to you I've no doubt (once I've actually finished this thing anyway) a lot of people will be enjoying slick album art display soon!

Gadgetoid commented 3 years ago

Right I've added instructions to the PiDi Spotify repository. Any other questions/issues/suggestions/requests then please raise them in a new issue over there: https://github.com/pimoroni/pidi-spotify

For those wanting to use Raspotify/Spotify Connect with Mopidy then you will still need to use my first soution above which I will re-iterate here:

sudo apt install mopidy-mpd mpc

Then use the below script as your --onevent hook for Raspotify, by editing /etc/default/raspotify and making your options line look something like this:

OPTIONS="--device hw:1,0 --onevent /home/pi/raspotify.sh"

Save this as /home/pi/raspotify.sh and ensure it's executable with sudo chmod +x /home/pi/raspotify.sh:

#!/bin/bash

if [ "$PLAYER_EVENT" = "start" ]; then
        mpc clear
        mpc add spotify:track:$TRACK_ID
        mpc play
        sleep 1
        mpc pause
elif [ "$PLAYER_EVENT" = "change" ]; then
        mpc clear
        mpc add spotify:track:$TRACK_ID
        mpc play
        sleep 1
        mpc pause
elif [ "$PLAYER_EVENT" = "stop" ]; then
        mpc play
elif [ "$PLAYER_EVENT" = "volume_set" ]; then
        mpc_volume=$(($VOLUME * 100 / 65536))
        mpc volume $mpc_volume
fi
robrecord commented 3 years ago

pidi-spotify and raspotify are now all I need for Spotify on my pirate-audio-enabled Pi 2; that and shairport-sync for Airplay.

Thank you @Gadgetoid!