plietar / librespot

Open Source Spotify client library
MIT License
1.13k stars 187 forks source link

[rust question] How do I load a track? #261

Closed Hoeze closed 6 years ago

Hoeze commented 6 years ago

Hi,

I'm playing a little bit with rust at the moment. Here I'm trying to load the name of a track, given some track id:

fn main() {
    let mut core = Core::new().unwrap();
    let handle = core.handle();
    let session_config = SessionConfig::default();
    let player_config = PlayerConfig::default();

    let args : Vec<_> = env::args().collect();
    if args.len() != 4 {
        println!("Usage: {} USERNAME PASSWORD TRACK", args[0]);
    }
    let username = args[1].to_owned();
    let password = args[2].to_owned();
    let credentials = Credentials::with_password(username, password);

    let track = SpotifyId::from_base62(&args[3]);

    println!("Connecting ..");
    let session = core.run(Session::connect(session_config, credentials, None, handle)).unwrap();
    println!("session ok");

    let track = Track::get(&session, track).wait().unwrap();
    println!("{}", track.name);

    println!("Done");
}

However, when I run this code, Track::get() blocks infinitely. Is there some kind of event loop I have to run before I can use Track::get?

plietar commented 6 years ago

Most of librespot's API returns futures. Whenever you get one of these you should run it through core.run rather than calling wait, just like you do for Session::connect.

For example :

let track = core.run(Track::get(&session, track)).unwrap();
println!("{}", track.name);

Note that this will block the current thread until the track information has been retrieved. If you want to perform other work you can combine the returned future using the tokio and future-rs libraries. See https://tokio.rs/docs/getting-started/futures/#adding-a-timeout for an example of a timeout associated with a future.

Hoeze commented 6 years ago

Thank you very much, I thought that wait() would run that future :)

Hoeze commented 6 years ago

How does "player.rs" run this future? Where is the thread spawned that completes Track::get(&session, track).wait()? Is this happening inside the internal.run() loop?

plietar commented 6 years ago

I started explaining the architecture, but this ended up quite lengthy, sorry.

Here's the TLDR of futures/threads in librespot.


Session::connect returns a future which will asynchronously set up the connection (AP resolve, authentication). This future must be run through Core::run.

While completing that future, Session::connect uses Session::create to create the Session object (which is the result of the future) and create the "packet handling" future. This future is registered with tokio to run in the background (using handle.spawn). Note that it does not return any useful value (It's type is BoxFuture<(), io::Error>, ie return ())). It actually doesn't return as long as the connection is open.

However note that despite its name, spawn only registered the packet handling future with tokio's event loop (I use core and event loop interchangeably). This means that it will not run if the core is not running, and packets will not be sent nor received. That means that after core.run(Session::connect(...)) returns the Session object you should run the core with something else in order to keep processing packets. The packet handling future will be scheduled by the event loop when it has work to do.

At this point, when you make a request on a "low level" API (like AudioKeyManager, MercuryManager or ChannelManager), it sends the corresponding packets to an in-process channel (these have nothing to do with the ChannelManager, which deals with Spotify channels. The naming is confusing sorry), and the packet handling future will forward them to the socket (that's the Session::send_packet function). They also create a channel specific to that request, store the tx side somewhere and return the receiver end. If you want to look into it AudioKeyManager is definitely the simplest one. The others have a lot more parsing and state involved.

When a response comes back from Spotify, the packet handling future will pick it up and call the appropriate function (that's the Session::dispatch function). That function will parse the response, find the channel associated with that request and send the response on there.

"Higher level" APIs like metadata and keymaster are just wrappers around these which prepare the request and parse the response.

Asynchronous requests can be made at any time, from any thread, even if the core isn't running. It will just enqueue packets onto the shared packet sending channel and return a future back. At this point there are three ways to receive data from the channel (future) that the request returned.

Because the OGG decoder expects reads from files to be synchronous, the AudioFile::read function uses .wait() to block on data. The entire player is therefore run on a separate ~process~  thread, and uses .wait() in a couple of other places. This is fine because the main thread is running the core.

When doing the second or third option you may not have anything useful to give to core.run. In this case you can make an empty future which never completes and pass it. That way the core will run forever.

See for example python-librespot. It creates a thread just for the event loop but runs an empty future on it. When some python code makes a request which returns a future, that future is combined using then such that the result is communicated back to Python when the result is available, and is spawned to run in the background

EDIT: typos