librespot-org / librespot-java

The most up-to-date open source Spotify client
Apache License 2.0
387 stars 95 forks source link

API for Zeroconf connection to other devices on the network #352

Open pezzy-o opened 3 years ago

pezzy-o commented 3 years ago

Problem: Spotify Web API only allows me to connect to devices which are specifically registered to my account, rather than those devices which advertise themselves over mdns on my local network.

Possible solution: I'd like to be able to use an API in order to programmatically connect to any Spotify devices on my local network, using Zeroconf. I can see that you've done the hard yards to be able to connect the librespot-java player via Zeroconf: would it be possible to onboard other players using a similar method, and incorporate this into the API?

Background / use case: I have created an application in Spotify, and I can use it for Spotify Web API calls, including connecting to those devices which are registered to my Spotify account. I use Sonos speakers, which seem to rely more on Zeroconf connections for devices on the local network, so when I use the Spotify Web API to list available devices using the Spotify Web API, unless I've recently connected to the Sonos device specifically through the Spotify app, the Sonos devices do not appear in the list of available devices. The Sonos devices still show up on the Spotify Connect app (they're still advertised and over mdns), just not in the Web API, presumably because they aren't registered specifically to my account, and need to be available to other users on my local network.

devgianlu commented 3 years ago

So you'd like an API endpoint to list devides available on your network and one to connect to them?

pezzy-o commented 3 years ago

Basically yes - that was just a long-winded way of asking, so I could explain the use case.

devgianlu commented 3 years ago

First part should be relatively easy: just need to make a mDNS probe and get the results. For the second one we need to craft the zeroconf blob which ultimately is a lot of wrapping around stored credentials data.

I'll start by modifying zeroconf-java to allow probing.

pezzy-o commented 3 years ago

Awesome - thanks for getting started so quickly! This might be me, but I'm getting an authorisation failure (401) when I try to compile (mvn clean package): Downloading from github: https://maven.pkg.github.com/devgianlu/*/xyz/gianlu/zeroconf/zeroconf/1.2.1/zeroconf-1.2.1.pom [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary for librespot-java 1.5.6-SNAPSHOT: [INFO] [INFO] librespot-java ..................................... SUCCESS [ 0.150 s] [INFO] librespot-java sink API ............................ SUCCESS [ 2.256 s] [INFO] librespot-java default sink ........................ SUCCESS [ 0.584 s] [INFO] librespot-java decoder API ......................... SUCCESS [ 0.385 s] [INFO] librespot-java lib ................................. FAILURE [ 1.125 s] [INFO] librespot-java DACP interface ...................... SKIPPED [INFO] librespot-java player .............................. SKIPPED [INFO] librespot-java api ................................. SKIPPED [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.275 s [INFO] Finished at: 2021-05-08T20:10:59+10:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal on project librespot-lib: Could not resolve dependencies for project xyz.gianlu.librespot:librespot-lib:jar:1.5.6-SNAPSHOT: Failed to collect dependencies at xyz.gianlu.zeroconf:zeroconf:jar:1.2.1: Failed to read artifact descriptor for xyz.gianlu.zeroconf:zeroconf:jar:1.2.1: Could not transfer artifact xyz.gianlu.zeroconf:zeroconf:pom:1.2.1 from/to github (https://maven.pkg.github.com/devgianlu/): Authentication failed for https://maven.pkg.github.com/devgianlu//xyz/gianlu/zeroconf/zeroconf/1.2.1/zeroconf-1.2.1.pom 401 Unauthorized -> [Help 1]

devgianlu commented 3 years ago

Yep, I know. The CI is also failing, there's something wrong with the CI on zeroconf-java because I had to switch away from Travis and it broke. Hadn't time to fix it yet.

As a workaround you can either clone zeroconf-java and do mvn clean install or download the JAR and install it manually.

pezzy-o commented 3 years ago

mvn clean install is giving me the same result. I'm not sure how I would modify the compiled JAR file: I took a look inside the archive and couldn't immediately find the same filenames as were modified in the commit, but I'm not overly familiar with java, maven etc. so might be best for me to just wait until the CI is fixed and I can compile it again. Thanks @devgianlu

devgianlu commented 3 years ago

@benturnberg You should be good now.

pezzy-o commented 3 years ago

Great - I can compile again 🤘

Using the librespot-api-1.6.1-SNAPSHOT.jar, I get the following result when running a POST to http://localhost:24879/discovery/list:

2021-05-10 16:26:25,521 TRACE selector:564 - Selected on sun.nio.ch.WindowsSelectorImpl@4166658f
2021-05-10 16:26:25,521 TRACE selector:583 - Selected key channel=java.nio.channels.SocketChannel[connected local=/[0:0:0:0:0:0:0:1]:24879 remote=/[0:0:0:0:0:0:0:1]:59437], selector=sun.nio.ch.WindowsSelectorImpl@4166658f, interestOps=1, readyOps=1 for java.nio.channels.SocketChannel[connected local=/[0:0:0:0:0:0:0:1]:24879 remote=/[0:0:0:0:0:0:0:1]:59437]
2021-05-10 16:26:25,521 TRACE selector:169 - Calling handleReady key 1 for java.nio.channels.SocketChannel[connected local=/[0:0:0:0:0:0:0:1]:24879 remote=/[0:0:0:0:0:0:0:1]:59437]
2021-05-10 16:26:25,522 TRACE listener:91 - Invoking listener io.undertow.server.protocol.http.HttpReadListener@22e2f427 on channel org.xnio.conduits.ConduitStreamSourceChannel@f641445
2021-05-10 16:26:25,522 TRACE transfer-encoding:119 - No content, starting next request
2021-05-10 16:26:25,522 TRACE selector:539 - Beginning select on sun.nio.ch.WindowsSelectorImpl@4166658f (with timeout)
2021-05-10 16:26:25,522 TRACE nio:550 - Select, queue is empty
2021-05-10 16:26:26,274 TRACE selector:564 - Selected on sun.nio.ch.WindowsSelectorImpl@4166658f
2021-05-10 16:26:26,275 TRACE selector:539 - Beginning select on sun.nio.ch.WindowsSelectorImpl@4166658f (with timeout)
2021-05-10 16:26:26,275 TRACE nio:550 - Select, queue is empty
2021-05-10 16:26:26,274 TRACE HttpServerExchange:1866 - Starting to write response for HttpServerExchange{ POST /discovery/list}
2021-05-10 16:26:26,277 TRACE safe-close:151 - Closing resource io.undertow.server.HttpServerExchange$DefaultBlockingHttpExchange@6da75879
2021-05-10 16:26:26,277 TRACE safe-close:151 - Closing resource io.undertow.server.HttpServerExchange$DefaultBlockingHttpExchange@6da75879
2021-05-10 16:26:27,002 TRACE selector:564 - Selected on sun.nio.ch.WindowsSelectorImpl@4166658f
2021-05-10 16:26:27,002 TRACE nio:611 - Running task io.undertow.util.DateUtils$2@4c001e57
2021-05-10 16:26:27,002 TRACE selector:539 - Beginning select on sun.nio.ch.WindowsSelectorImpl@4166658f (with timeout)
2021-05-10 16:26:27,002 TRACE nio:550 - Select, queue is empty
2021-05-10 16:26:30,906 TRACE selector:564 - Selected on sun.nio.ch.WindowsSelectorImpl@4166658f
2021-05-10 16:26:30,906 TRACE nio:611 - Running task io.undertow.server.protocol.ParseTimeoutUpdater@6b5bf646
2021-05-10 16:26:30,907 TRACE selector:539 - Beginning select on sun.nio.ch.WindowsSelectorImpl@4166658f (with timeout)
2021-05-10 16:26:30,907 TRACE nio:550 - Select, queue is empty
devgianlu commented 3 years ago

You should get the list of devices in the response body.

pezzy-o commented 3 years ago

Sorry, should have included that - the response body is:

{
    "value":  [

              ],
    "Count":  0
}
pezzy-o commented 3 years ago

Just to add to this, the API does work for me when invoking /player/next for example... On the other hand, it also returns a blank response when (while connected) I invoke /web-api/v1/me/player/devices.

devgianlu commented 3 years ago

I think the problem goes back to zeroconf-java not being able to discover your librespot-java client.

pezzy-o commented 3 years ago

Anything I can check on my side? I tried disabling AV and firewall just as a sense-check, still the same though.

devgianlu commented 3 years ago

Turns out there was a bug in zeroconf-java. Should be fixed now.

pezzy-o commented 3 years ago

Ok that's good. Now it can see itself, but it's not seeing any other players advertised on the local network:

{  
    "value":  [    
                  {
                      "name":  "librespot-java",
                      "target":  "librespot-java",
                      "port":  57351
                  }
              ],
    "Count":  1
} 
pezzy-o commented 3 years ago

... or at least, it's not returning them in the body of the response

devgianlu commented 3 years ago

After more bug fixing it should be finally good.

pezzy-o commented 3 years ago

It's still only recognising itself.

devgianlu commented 3 years ago

What do you see if you run dns-sd -B _spotify-connect local?

pezzy-o commented 3 years ago

The devices are there:

Timestamp     A/R Flags if Domain                    Service Type              Instance Name
16:03:30.999  Add     2 20 local.                    _spotify-connect._tcp.    librespot-java
16:03:31.162  Add     2 20 local.                    _spotify-connect._tcp.    sonos48A6B8XXXXX1
16:03:31.208  Add     2 20 local.                    _spotify-connect._tcp.    sonos48A6B8XXXXX2
pezzy-o commented 3 years ago

Hey @devgianlu let me know if there's anything else I can do to troubleshoot. Cheers

devgianlu commented 3 years ago

@benturnberg I just need to find the time to debug zeroconf-java with a Spotify Connect enabled device (likely my TV).

pezzy-o commented 3 years ago

Thanks @devgianlu , I appreciate it. Finding the time can certainly be difficult!

sbrunk commented 3 years ago

Hi, I just stumbled upon librespot-java (which looks amazing!) and then this thread when trying to use it. I'm building an NFC remote control to enable the kids to play Spotify albums/playlists on a Sonos speaker by scanning a card with an NFC tag.

Like @benturnberg I can see the speakers with dns-sd:

dns-sd -B _spotify-connect local
Browsing for _spotify-connect._tcp.local
DATE: ---Sun 20 Jun 2021---
13:39:23.219  ...STARTING...
Timestamp     A/R    Flags  if Domain               Service Type         Instance Name
13:39:23.220  Add        3   6 local.               _spotify-connect._tcp. sonosB8E9374XXXXX
13:39:23.220  Add        3   6 local.               _spotify-connect._tcp. sonos7828CAXXXXX2
13:39:23.220  Add        3   6 local.               _spotify-connect._tcp. sonos7828CAXXXXXA
13:39:23.220  Add        3   6 local.               _spotify-connect._tcp. sonos7828CAXXXXX0
13:39:23.220  Add        3   6 local.               _spotify-connect._tcp. librespot-java
13:39:23.220  Add        2   6 local.               _spotify-connect._tcp. Kodi (mediacenter)

But when running the API server (via Coursier)

cs launch -r https://oss.sonatype.org/content/repositories/snapshots xyz.gianlu.librespot:librespot-api:1.6.1-SNAPSHOT -M xyz.gianlu.librespot.api.Main

and then calling the discovery endpoint, I can see only the other Spotify connect devices but not the Sonos speakers:

curl -s -X POST http://localhost:24879/discovery/list | jq
[
  {
    "name": "librespot-java",
    "target": "Sorens-MacBook-Pro",
    "port": 34515
  },
  {
    "name": "Kodi (mediacenter)",
    "target": "mediacenter.local",
    "port": 46233
  }
]

I also ran the discoverer manually (basically the code below from the DiscoveryHandler just to check and got the same result (Sonos speakers not showing up). https://github.com/librespot-org/librespot-java/blob/2745b502e69a2dc69bd461516cb62f7d6dfad4e4/api/src/main/java/xyz/gianlu/librespot/api/handlers/DiscoveryHandler.java#L43-L53

If there's anything I can do to help debugging the issue, I'd be happy to.

sbrunk commented 3 years ago

Wow that was fast @devgianlu. While looking into zeroconf-java, I just saw that you had released 1.3.1. So I tried it and now the speakers show up on the discovery enpoint!

devgianlu commented 3 years ago

Turns out there was a bug in the code I hadn't written myself. It was working with zeroconf-java only because the packet creation is unoptimized while official clients are.

I was writing the code for the connection, but it can be done in two ways:

Let me know if you intended to use it differently.

sbrunk commented 3 years ago

I think connecting with the current user of librespot-java would be the best fit for my use-case.

pezzy-o commented 3 years ago

Current user would work with my use case too. Thanks @devgianlu

sbrunk commented 3 years ago

@devgianlu now that discovery works is there anything I can do to help with the connect part?

devgianlu commented 3 years ago

I don't really have time to develop this feature. What needs to be done is writing the encryption code for the blob having the one for decryption.

dsheets commented 2 years ago

Has anyone made any progress on this?

I have started the patchset to encrypt the blob and make the addUser request to the HTTP server advertised by zeroconf. I thought I'd check in before I do too much more work on the feature. My use case is basically the same as @sbrunk's.

sbrunk commented 2 years ago

@dsheets I haven't looked into this further yet.

thlucas1 commented 6 months ago

Any progress on this? I am trying to make the same addUser request in Python 3. Thanks.

floodwayprintco commented 1 month ago

Just wanted to add myself in here. I have been trying to get my Spocon setup to show up on the network for things like HomeAssistant and have been struggling. I think this is a similar issue?

I am trying to do the mDNS port forwarding and Google led me here.

Is there any way to wake up Spocon/librespot and show it on Spotify Connect?

thlucas1 commented 1 month ago

Not sure on that, as I am not familiar with “Spocon”. Or is that short for Spotify Connect?

I have had success with getting the integration to work with the spotifyd and Spotify Connect Addon, both of which use librespot under the covers. You just have to configure librespot in discovery mode, and create a credentials.json file.

Checkout the SpotifyPlus Device Configuation Options for librespot wiki doc for more details.

thlucas1 commented 1 month ago

@floodwayprintco I did some more research on SpoCon, and found the SpoCon web-site and associated configuration docs. I think you can adjust your /opt/spocon/config.toml configuration settings to use the following options for librespot:

# Spotify Connect device name and info
deviceName = "My-Spocon"
deviceType = "COMPUTER" 
preferredLocale = "en"

[auth] # use zeroconf authentication (no username, password, blob)
strategy = "ZEROCONF"

[zeroconf] # listen on all interfaces, and bind to specific port 8600
listenPort = 8600
listenAll = true

[cache] # enable cache path for librespot auth credentials
enabled = true
dir = "./cache/"
doCleanUp = true

That should force SpoCon to advertise itself via zeroconf / mdns on port 8600. I just picked 8600 out of the blue, you can choose whatever port you want. I like a dedicated (vs random) port, in case you need to open firewalls. If you do open firewalls, open activity for 8600 TCP and 5353 UDP (mdns advertise).

I would also avoid spaces in the deviceName parameter, as some Spotify Clients have issues with handling spaces in the name.

You should then be able to start the SpoCon service; after it is started, do the following:

1) Open the Spotify Desktop client from a machine on the same network as you ran this, ensuring no proxy is in use that may interfere with zeroconf. Note that the Spotify Mobile client does not find devices immediately like the Spotify Desktop client does. It is suggested that you use the Spotify Desktop client for this step.

2) Use the Spotify client "Connect to a Device" menu to select the device name that you specified for the deviceName config value (My-Spocon in this example).

At this point, librespot should have generated a credentials.json file in the cache path that you specified for the dir config value (./cache/credentials.json in this example).

Please refer to the SpotifyPlus librespot Credentials File topic for further instructions on how to utilize the librespot credentials with the SpotifyPlus integration.

Hope it helps - let me know either way, as you are the first person I have heard of that utilizes SpoCon. I have never used it myself, but based on the docs that I read it should work fine. If it does work, then I will add this to my SpotifyPlus wiki documentation.

thlucas1 commented 1 month ago

@floodwayprintco Apologies - I just realized that this thread is for the librespot-java process, and not SpotifyPlus integration.