librespot-org / librespot-java

The most up-to-date open source Spotify client
Apache License 2.0
370 stars 89 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 2 years ago

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

devgianlu commented 2 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 1 month ago

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