DEV Community

Håvard Wormdal Høiby
Håvard Wormdal Høiby

Posted on • Edited on

Spotify on Workflow

App modules are at the center of workflow. They are the important glue between the internals of workflow and actual applications running on your platform. If you are new to workflow, then the introduction post is a good place to start.

Updated code samples to support workflow@2.x.

spotify code sample

This post will guide you through a practical example of extending workflow with support for Spotify. Workflow works cross platform, and the implementation for the various platform differs. If you need to make an app run on a specific platform, the other platforms can be skipped. This post will show how to write apps for osx and i3.

Running example

As a running example in this post we will create a new app component for Spotify. Spotify does not fall into the previously defined categories of applications that workflow supports, being Terminals, Browser and Text Editors. So lets pick a simple use case for our initial version of the Spotify app. Spotify has defined a uri specification which can be used for automation. Let us use the uri to open Spotify with a given playlist.

Initializing the application

To get started with writing applications there is a npm initializer called create-workflow-app. Lets run it with the npx cli.

npx create-workflow-app workflow-app-spotify
Enter fullscreen mode Exit fullscreen mode

This will generate an example application which opens emacs it the terminal. The three notable files are flows/Example.js, cli.js and src/index.js. The cli.js implements a simple workflow config which lets us test our app, by default it will use workflow-wm-terminal. The yarn example command is set up to run the flows/Example.js.

Putting our example into code.

Lets start defining our interface by implementing the example. The spotify cli takes an uri parameter, one variant of this parameter lets us specify a playlist. The uri's format is spotify:user:<username>:playlist:<playlist-id>. So lets define an example of using this specification from jsx.

<Spotify minimized play>
  <Playlist user={'<username>'} id={'<playlist id>'} />
</Spotify>

Enter fullscreen mode Exit fullscreen mode

We have given our top level Spotify component a parameter called minimized which will cause the Spotify application to be launched minimized. And we have given it a child for specifying the playlist to open. This component has the username and playlist id properties. The play prop on the Spotify component will trigger autoplay.

The application scaffold

In the src/index.js file we have the scaffold for making any app for workflow. The following properties are mandatory for any app.

const Spotify = {
  type: 'app',
  name: 'Spotify',
  params: ['minimized', 'play'],
  open: ({minimized, play}, context, children) => {
    // code for the app
  }
};
Enter fullscreen mode Exit fullscreen mode

The type property is used by workflow to distinguish the app node from layout and workspace nodes. The name property is used in debugging information an is exposed to the wm adapter layer. The params is used to validate the arguments passed to the node in the open function. The open function is responsible for opening the application and making sure it is placed in the expected position on the screen. The parameters to the open function are the parameters to the node in the flow, a context variable which is specific to the underlying platform and windows manager, and any child nodes passed to the node. The application it self is free to define the specification of allowed children and arguments.

Supporting workflow-wm-i3

Let us start with adding support for the i3 windows manager. The i3 windows manager is identified by context = {platform: "linux", wm: "i3"}. It requires the app to define an additional property called xClass [1]. For Spotify this is simply Spotify. The open function should return a shell command which can be executed to open the application, this is specific to i3. workflow-wm-i3 will generate a layout tree based on the xClass which will match the various applications when opened [2].

const Spotify = {
  xClass: 'Spotify',
  open: ({ minimized, play }, context, children) => {
    if (children.length !== 1) {
      throw new Error('Spotify does not support more or less than one child node');
    }

    const [child] = children;
    const uri = child.open(child, context, child.children);

    return `spotify --uri='${uri}' &`;
  }
};
Enter fullscreen mode Exit fullscreen mode

We do also need to define the Platform child node [3]. Inside the platform node we build up the uri which the spotify node will return to workflow-wm-i3. This design lets us easily add new types of child nodes, which will be called by the spotify node.

const Platform = {
  type: "app",
  name: "Platform",
  params: ["user", "id"],
  open: ({user, id}, context, children) => {
    return `spotify:user:${user}:playlist:${id}`;
  }
};
Enter fullscreen mode Exit fullscreen mode

That is all it takes to add support for spotify running under workflow-wm-i3.

Note The example above does not actually trigger autoplay on linux. If you figure out how to activate it, please have a look at this issue.

Supporting workflow-wm-osx

The OSX integration follows a more standardized method of writing apps for workflow. workflow-wm-osx will call each apps open function with the arguments passed to the app and the absolute position on the screen. The app is responsible for opening the application and positioning it on the given position. This is usually done with JXA [4]. For convenience, workflow-wm-osx will pass a function called run through the context parameter which can be used to execute JXA code. The basic scaffold for psudo implementation is given below.

const Spotify = {
  open: async (app, context, children) => {
    const uri = getUri(children, context);

    await context.run(({ minimized, play, position }, uri) => {
      const spotify = Application("Spotify");

      spotify.activate();

      const window = spotify.windows[0];
      window.bounds = app.position;

      spotify.playTrack(uri);
    }, app, uri);
  }
};

Enter fullscreen mode Exit fullscreen mode

Now the most notable thing about the code above is invocation of the run function. This will call into @jxa/run which executes the function parameter with osascript and returns a promise. This means that the function passed cannot be a closure and must only reference its parameters and the context provided by the osascript environment. The code opens spotify and sets the position of the window to the position in from the app. The Spotify specific function playTrack is used to start the playlist.

The api available on the Spotify application can be found in the Script Editor application on OSX. It is possible to generate TypeScript definitions for the api, check out this for getting started.

Now, the actual code to make this work properly on OSX is a bit more complex. Check out the source code for the working version.

Footnotes

[1]: This is the X11 WM_CLASS as found by the xprop utility.

[2]: This simplistic implementation causes this bug.

[3]: For now these nodes will either use the type layout or app. Where a layout node is used as a support node for positioning other nodes, and the app node denotes something that will be visible on the screen.

[4]: JXA, or Javascript for Automation, the OSX way of writing automation scripts using Javascript

Top comments (0)