SMAPI sample server

SMAPI sample server

The SMAPI sample server is a Java server that uses SMAPI to create a simple music service. It provides a simple Sonos apps controller interface and serves to Sonos players a static directory of music files. It demonstrates the following features:

  • Browse responses using the getMetadata method.
  • Search responses using a search endpoint.
  • Music playback using a music service endpoint and the getMediaURI method to serve static content.
  • Info view using getExtendedMetadata and getExtendedMetadataText methods, which display additional information about a track.
  • DeviceLink Authentication Mode using getDeviceLinkCode and getDeviceAuthToken methods.

DOWNLOAD, SET UP, BUILD, & TEST

Download and extract the source code package and see the included README.md for installation instructions. Below we'll briefly go over how the features were implemented.

 

Try it out

The sample server demonstrates the core features of SMAPI. To see it in action, add the sample server to one of your Sonos players by following the instructions to add your service with customSD. This enables you to choose the sample server as a music service source.

Download Sonos software, run the Sonos app, and then select Add Music Services to add the sample server as a music source. You'll then see the DeviceLink authorization page. Follow the prompts and enter the username "acmeUser" and password "password" to authenticate.

You should now see the sample server in your list of music sources. Open it up to browse for and play content. Try the search field to search the sample server for an artist, album, or track. You can also learn more details or perform actions on a track, such as view the album or artist information, add the track to Sonos favorites, or add it to a Sonos playlist. 

Architecture

The SMAPI sample server uses Spring Boot as a Java framework and is a sample of how to implement a basic SMAPI server. It doesn't use a content delivery network (CDN) for content, but instead serves a directory of static files. It does not include a database for metatdata nor does it persist settings across instances. If you stop the server and restart, you will need to reauthorize the user again.

For this example, we use the MusicServiceConfig class to configure the service.

The following classes implement required browse and search SMAPI methods:

  • MusicServiceEndpoint
  • MusicServiceGetMetadataEndpoint
  • MusicServiceGetExtendedMetadataEndpoint
  • MusicServiceGetExtendedMetadataTextEndpoint
  • MusicServiceSearchEndpoint

These were broken up into several classes to keep each file relatively small, but they could just as easily have been combined into a single class.

The class DeviceLinkAuthenticationModeEndpoint implements the SMAPI DeviceLink Authentication Mode methods. This works in conjunction with DeviceLinkPageController, which serves the login page.

Optional playlist editing and status reporting methods have placeholders in their respective classes for future implementation.

The MusicServiceAnnotationMethodEndpointMapping class routes requests to endpoints, so that the @PayloadRoot parameters and the @BindObjectId annotation suffice to set up a new request handler.

The sections below describe how the server uses these interfaces to offer the common features. Please see the actual source code for details.

Browsing content

As you browse the sample service, the Sonos app displays content for albums, artists, or tracks. Each time you select an item, the Sonos app sends a getMetadata request for the item's ID. The server responds with the metadata the item represents. The returned metadata is either a container of other items, like a list of albums, or an individual item such as a track.

The starting ID for any audio service's hierarchy must be root. Therefore, every service must respond to the getMetadata request for <id>root</id>. The contents of this response determines what other IDs are available to browse. The names of all other IDs for your service are up to you. For example, the sample server implements the IDs “browse:albums”, “browse:artists”, and “browse:tracks”. But your service can use anything you want, even “foo”, “bar”, and “baz”, for example. You determine what containers can be browsed and what is in those containers. Ideally, the container hierarchy should match what you already provide listeners when they browse your own application.

The sample server uses the MusicServiceGetMetadataEndpoint to respond to requests. For example, here's how it generates a response for the artists container:

@PayloadRoot(namespace = MusicServiceConfig.SONOS_SOAP_NAMESPACE, localPart = "getMetadata")
    @BindObjectId(BROWSE_ARTISTS)
    @ResponsePayload
    public GetMetadataResponse getMetadataBrowseArtists(@RequestPayload GetMetadata request) {
        logger.info("GetMetadataBrowseArtists");
 
        _verifyObjectId(BROWSE_ARTISTS, request);
 
        MediaList responseList = new MediaList();
        List<AbstractMedia> mediaCollection = responseList.getMediaCollectionOrMediaMetadata();
 
        ContentApiDaoInterface dataSource = new ContentApiDao();
        List<Artist> artists = dataSource.getArtists(request.getIndex(), request.getCount());
        String baseRequestUrl = getBaseRequestUrl();
        mediaCollection.addAll(artists.stream()
                .map(artist -> buildArtistMediaCollection(artist, source, getBaseRequestUrl()))
                .collect(Collectors.toList()));
 
        responseList.setIndex(request.getIndex());
        responseList.setTotal(dataSource.getAllCount(Artist.class));
        responseList.setCount(mediaCollection.size());
 
        return new GetMetadataResponse() {{
            setGetMetadataResult(responseList);
        }};
    }

Handling search requests

Users can search all music services for content. The sample server handles search requests in the MusicServiceSearchEndpoint. When your service provides search, it must respond to the getMetadata request for <id>search</id>. Your service's response must be a collection of the available search category IDs. The Sonos app then uses these to form its search requests. For example, here's how the sample server handles an album search:

@PayloadRoot(namespace = MusicServiceConfig.SONOS_SOAP_NAMESPACE, localPart = "search")
    @BindObjectId(ALBUMS)
    @ResponsePayload
    public SearchResponse searchAlbums(@RequestPayload Search request) {
        _verifyObjectId(ALBUMS, request);
        SearchResponse response = new SearchResponse();
        MediaList responseList = new MediaList();
 
        ContentApiDaoInterface dataSource = new ContentApiDao();
        List<AbstractMedia> mediaCollection = responseList.getMediaCollectionOrMediaMetadata();
        mediaCollection.addAll(dataSource.getSearchAlbums(ALBUMS, request.getTerm(), request.getIndex(), request.getCount()).stream()
                .map(album -> buildAlbumMediaCollection(album, source, getBaseRequestUrl()))
                .collect(Collectors.toList()));
 
        responseList.setIndex(request.getIndex());
        responseList.setCount(mediaCollection.size());
        responseList.setTotal(mediaCollection.size());
 
        response.setSearchResult(responseList);
        return response;
    }

See the search documentation and the Implementing Universal Search tutorial for more details on how to implement search.

Playing music

Sonos players get the URIs for your specific content using getMediaURI requests. The sample server handles these requests in the MusicServiceEndpoint:

@PayloadRoot(namespace = MusicServiceConfig.SONOS_SOAP_NAMESPACE, localPart = "getMediaURI")
    @BindObjectId(prefix = TRACK)
    @ResponsePayload
    public GetMediaURIResponse getMediaUriTrack(@RequestPayload GetMediaURI request) {
        logger.info("GetMediaURI");
 
        _verifyObjectIdPrefix(TRACK, request);
        int trackId = parseIdFromRequest(TRACK, request);
 
        GetMediaURIResponse response = new GetMediaURIResponse();
        ContentApiDaoInterface dataSource = new ContentApiDao();
        TrackInfo trackInfo = dataSource.getTrackInfo(trackId);
        response.setGetMediaURIResult(ContentApiDefaultEndpoint.getBaseRequestUrl() + trackInfo.getTrackUri());
        return response;
    }

Since the sample server only serves static files, we've implemented a simple StaticContent class. When you develop your implementation, you should be able to update from serving static files to Sonos to serving content from your content delivery network.

Implementing info view

Listeners can view more information about content by doing the following:

  • On the Sonos app for Mac or PC: select a track and choose Info & Options.
  • On the Sonos app for mobile devices, click the three dots next to the track and tap More.

SMAPI supports this view with the getExtendedMetadata and getExtendedMetadataText requests. The sample server generates responses to these requests in the MusicServiceGetExtendedMetadataEndpoint and the MusicServiceGetExtendedMetadataTextEndpoint. For example, here's how it generates a getExtendedMetadata response for an album:

@PayloadRoot(namespace = MusicServiceConfig.SONOS_SOAP_NAMESPACE, localPart = "getExtendedMetadata")
    @BindObjectId(prefix = ALBUM)
    @ResponsePayload
    public GetExtendedMetadataResponse getExtendedMetadataAlbum(@RequestPayload GetExtendedMetadata request) {
        logger.info("GetExtendedMetadataAlbum");
        _verifyObjectIdPrefix(ALBUM, request);
        int albumId = parseIdFromRequest(ALBUM, request);
 
        GetExtendedMetadataResponse response = new GetExtendedMetadataResponse();
        ContentApiDaoInterface dataSource = new ContentApiDao();
        Album album = dataSource.getAlbum(albumId);
 
        ExtendedMetadata extendedMetadata = new ExtendedMetadata();
        extendedMetadata.setMediaCollection(MusicLibraryDataUtil.buildAlbumMediaCollection(album, source, getBaseRequestUrl()));
 
        List<RelatedText> relatedTexts = extendedMetadata.getRelatedText();
        dataSource.getAlbumInfo(albumId).stream().forEach(ai -> relatedTexts.add(buildRelatedText(source)
                .id(ALBUM + albumId)
                .type(ai.getType())
                .build()));
 
        response.setGetExtendedMetadataResult(extendedMetadata);
        return response;
    }

To find out more, see Customize Info View.

Implementing DeviceLink

Sonos apps use OAuth 2.0 for browser-based authentication of your service. The sample server demonstrates this by implementing getDeviceLinkCode and getDeviceAuthToken in the DeviceLinkAuthenticationModeEndpoint. See the DeviceLink Authentication Mode overview and the Implementing DeviceLink tutorial to learn more.

Status Reporting

We haven't implemented status reporting in the sample server, but you can see the starting points in the StatusReportingEndpoint. See the Reporting overview for more details on the information your service can receive.