Clone wiki

Shampoo / DeveloperGuide

Deprecated, need update __TOC__

Implement a new datastore module

Datastores are actual storage units for music track files and associated cover art pictures.

There's only one single functional datastore per running instance of the application, at all times.

Datastore adapters must conform to the biz.ddcr.shampoo.server.io.datastore.DatastoreInterface interface. This interface should remain stable until version 1.0.

An example of datastore implementation is biz.ddcr.shampoo.server.io.datastore.LocalFilesystemDataStore.

If your datastore processes files in an ordered fashion (queue-based), you may also use the abstract class biz.ddcr.shampoo.server.io.datastore.QueueBasedStore, but it is not mandatory.

Asynchronous processing for each unit of work is already directly handled within webservices and the web frontend. You can safely use a single thread only, but more is also possible.

If your datastore cannot or shouldn't handle content delivery to third parties on-demand (i.e. hasDirectSecuredAccessURL() returns false), file download will be delegated to Shampoo's own HTTP server. Otherwise you're required to handle the authentication mechanism by yourself. See source code for clues on how to perform that.

Users will be able to directly use your implementation via the main configuration file. Create a bean definition for your implementation into applicationContext.xml and make sure this object can read all properties users are allowed to change from the configuration file. Users will use your implementation by directly specifying its bean identifier in the configuration file. Here's how the default implementation is defined in applicationContext.xml:

<bean id="localFilesystem" class="biz.ddcr.shampoo.server.io.datastore.LocalFilesystemDataStore">
        <property name="path" value="${file.datastore.localFilesystem.path}"/>
        <property name="numberOfSubDirLevels" value="${file.datastore.localFilesystem.path.levels}"/>
        <property name="privateFileURI" value="${file.datastore.localFilesystem.expose_file_uri_to_private_webservice}"/>
        <!-- leave it to false, untested experimental feature -->
        <property name="privateFilePath" value="false"/>
    </bean>

At the moment, only the streaming module might want to directly access files from the datastore. File downloads from the GUI and public webservices use the internal webserver.

See the JavaDoc for further info.

biz.ddcr.shampoo.server.io.datastore.DatastoreInterface

    /**
     * Callback interface when an item has been transfered from or to the datastore
     */
    public interface TransferCallbackInterface {
        /**
         * newContainer may point to an updated version of the file metadata if it was modified during the transfer, otherwise, if the header is unchanged, this event is not called
         *
         * @param updatedFileInfo 
         */
        public void onNewHeader(FileInfoInterface updatedFileInfo);
        /**
         * If the underlying business layer wraps the call to the method within an asynchronous thread the value can be used to keep track of the progress of the trasnfer
         * @param percentDone
         **/
        public void onProgress(byte percentDone);
        /**
         * If the underlying business layer wraps the call to the method within an asynchronous thread the value can be used to abort the transfer
         * @return  
         **/
        public boolean onCancellable();
    }

    /**
     * Store a file in the datastore from the TypedStreamInterface stream
     * The file, after the transfer, will be identified by the requestId parameter
     * The move flag specifies if the original stream must be deleted once the file has been fully copied
     * The actual tranfer must be synchronous, it's the responsibility of the underlying business layer to wrap it withint an asynchronous thread if required
     * If the canOverwrite flag is set to false and an existing file is about to be overwritten, an Exception is thrown
     * Returns a transferId that will be used by commit() or rollback()
     * @param requestedId 
     * @param file
     * @param callback 
     * @param canOverwrite 
     * @return
     * @throws Exception
     **/
    public Integer moveTypedStream(final String requestedId, final TypedStreamInterface file, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer moveTypedStream(final String requestedId, final TypedStreamInterface file, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception
     * @param requestedId
     * @param file
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer moveTypedStream(final String requestedId, final TypedStreamInterface file, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer moveTypedStream(final String requestedId, final TypedStreamInterface file, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception
     * @param requestedId
     * @param file
     * @param callback
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copyTypedStream(final String requestedId, final TypedStreamInterface file, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer moveTypedStream(final String requestedId, final TypedStreamInterface file, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception
     * @param requestedId
     * @param file
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copyTypedStream(final String requestedId, final TypedStreamInterface file, final boolean canOverwrite) throws Exception;

    /**
     * Fetch a file as a TypedStreamInterface from the datastore and identified using the requestId identifier
     * The transfer must be synchronous
     * @param requestedFormat 
     * @param requestedId
     * @return
     * @throws Exception
     **/
    public UntypedStream getRawStream(final FILE_FORMAT requestedFormat, final String requestedId) throws Exception;
    /**
     * See public UntypedStream getRawStream(final FILE_FORMAT requestedFormat, final String requestedId) throws Exception;
     * @param requestedId
     * @return
     * @throws Exception
     */
    public UntypedStream getRawAudioStream(final String requestedId) throws Exception;
    /**
     * See public UntypedStream getRawStream(final FILE_FORMAT requestedFormat, final String requestedId) throws Exception;
     * @param requestedId
     * @return
     * @throws Exception
     */
    public UntypedStream getRawCoverArtStream(final String requestedId) throws Exception;

    /**
     * Returns a transferId that will be used by commit() or rollback()
     * The actual tranfer must be synchronous, it's the responsibility of the underlying business layer to wrap it within an asynchronous thread if required
     * @param requestedFormat 
     * @param requestedId
     * @param callback 
     * @return
     * @throws Exception
     **/
    public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId, final TransferCallbackInterface callback) throws Exception;
    /**
     * See public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId, final TransferCallbackInterface callback) throws Exception;
     * @param requestedFormat
     * @param requestedId
     * @return
     * @throws Exception
     */
    public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId) throws Exception;
    /**
     * See public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId, final TransferCallbackInterface callback) throws Exception;
     * @param requestedId
     * @param callback
     * @return
     * @throws Exception
     */
    public Integer removeAudio(final String requestedId, final TransferCallbackInterface callback) throws Exception;
    /**
     * See public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId, final TransferCallbackInterface callback) throws Exception;
     * @param requestedId
     * @return
     * @throws Exception
     */
    public Integer removeAudio(final String requestedId) throws Exception;
    /**
     * See public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId, final TransferCallbackInterface callback) throws Exception;
     * @param requestedId
     * @param callback
     * @return
     * @throws Exception
     */
    public Integer removePicture(final String requestedId, final TransferCallbackInterface callback) throws Exception;
    /**
     * See public Integer remove(final FILE_FORMAT requestedFormat, final String requestedId, final TransferCallbackInterface callback) throws Exception;
     * @param requestedId
     * @return
     * @throws Exception
     */
    public Integer removePicture(final String requestedId) throws Exception;

    /**
     * Copy or move a file internally, i.e. without downloading the file first and then reuploading it
     * Returns a transferId that will be used by commit() or rollback()
     * The actual tranfer must be synchronous, it's the responsibility of the underlying business layer to wrap it within an asynchronous thread if required
     *
     * @param requestedFormat 
     * @param canOverwrite 
     * @param destinationId 
     * @param sourceId 
     * @param callback 
     * @return 
     * @throws Exception 
     */
    public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param requestedFormat
     * @param sourceId
     * @param destinationId
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param callback
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer renameAudio(final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer renameAudio(final String sourceId, final String destinationId, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param callback
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer renamePicture(final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer renamePicture(final String sourceId, final String destinationId, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param requestedFormat
     * @param sourceId
     * @param destinationId
     * @param callback
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copy(final FILE_FORMAT requestedFormat,final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param requestedFormat
     * @param sourceId
     * @param destinationId
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copy(final FILE_FORMAT requestedFormat,final String sourceId, final String destinationId, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param callback
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copyAudio(final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copyAudio(final String sourceId, final String destinationId, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param callback
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copyPicture(final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
    /**
     * See public Integer rename(final FILE_FORMAT requestedFormat, final String sourceId, final String destinationId, final TransferCallbackInterface callback, final boolean canOverwrite) throws Exception;
     * @param sourceId
     * @param destinationId
     * @param canOverwrite
     * @return
     * @throws Exception
     */
    public Integer copyPicture(final String sourceId, final String destinationId, final boolean canOverwrite) throws Exception;

    /**
     * Make the actual changes if not previously rollbacked. This doesn't imply that the interface is XA-compliant, it's just an additional safe-guard against corruption.
     * A commit should not fail, nor should it throw an Exception
     * When multiple operations are to be processed within the same commit command, then deleting a file should not make any subsequent copy or move operation on the same file impossible
     **/
    public void commit();
    /**
     * See public void commit();
     * @param transferId
     */
    public void commit(Integer transferId);
    /**
     * See public void commit();
     * @param transferIds
     */
    public void commit(Collection<Integer> transferIds);
    /**
     * Withdraw the changes if not already committed. This doesn't imply that the interface is XA-compliant, it's just an additional safe-guard against corruption.
     * A rollback should not fail, nor should it throw an Exception
     * When multiple operations are to be processed within the same rollback command, then deleting a file should not make any subsequent copy or move operation on the same file impossible
     **/
    public void rollback();
    /**
     * See public void rollback();
     * @param transferId
     */
    public void rollback(Integer transferId);
    /**
     * See public void rollback();
     * @param transferIds
     */
    public void rollback(Collection<Integer> transferIds);

    /**
     * Ask whether the datastore allows direct access to a stream via a free-form URL to third-parties.
     * It's the responsibility of the datastore to make a stream secured and implemnt authentication if direct access is enabled
     * The third-parties are the GWT GUI and the public webservices
     * @param requestedFormat 
     * @param requestedId
     * @return
     */
    public boolean hasDirectSecuredAccessPublicURL(final FILE_FORMAT requestedFormat, final String requestedId);
    /**
     * See public boolean hasDirectSecuredAccessURL(final FILE_FORMAT requestedFormat, final String requestedId);
     * @param requestedId
     * @return
     */
    public boolean hasAudioDirectSecuredAccessPublicURL(final String requestedId);
    /**
     * See public boolean hasDirectSecuredAccessURL(final FILE_FORMAT requestedFormat, final String requestedId);
     * @param requestedId
     * @return
     */
    public boolean hasPictureDirectSecuredAccessPublicURL(final String requestedId);
    /**
     * Get a direct free-form URL to a datastore stream accessible to third-parties.
     * It's the responsibility of the datastore to make a stream secured and implemnt authentication if direct access is enabled
     * The third-parties are the GWT GUI and the public webservices, the protocol SHOULD be http
     * userId is the authenticated user login and md5_password the MD5 hash of his password
     * Throws an IOException if hasDirectSecuredAccessURL() returns false, i.e. there's no direct access to this stream or it's forbidden
     * @param channelId 
     * @param privateKey 
     * @param requestedId
     * @param requestedFormat 
     * @return
     * @throws IOException  
     */
    public String getDirectSecuredAccessPublicURL(final String userId, final String md5_password, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param requestedFormat
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getDirectSecuredAccessPublicURL(final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param channelId
     * @param privateKey
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getAudioDirectSecuredAccessPublicURL(final String userId, final String md5_password, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getAudioDirectSecuredAccessPublicURL(final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param channelId
     * @param privateKey
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getPictureDirectSecuredAccessPublicURL(final String userId, final String md5_password, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getPictureDirectSecuredAccessPublicURL(final String requestedId) throws IOException;

    /**
     * Ask whether the datastore allows direct access to a stream via a free-form URL to third-parties.
     * It's the responsibility of the datastore to make a stream secured and implemnt authentication if direct access is enabled
     * The third-parties are the private webservices (streamer)
     * @param requestedFormat 
     * @param requestedId
     * @return
     */
    public boolean hasDirectSecuredAccessPrivateURL(final FILE_FORMAT requestedFormat, final String requestedId);
    /**
     * See public boolean hasDirectSecuredAccessURL(final FILE_FORMAT requestedFormat, final String requestedId);
     * @param requestedId
     * @return
     */
    public boolean hasAudioDirectSecuredAccessPrivateURL(final String requestedId);
    /**
     * See public boolean hasDirectSecuredAccessURL(final FILE_FORMAT requestedFormat, final String requestedId);
     * @param requestedId
     * @return
     */
    public boolean hasPictureDirectSecuredAccessPrivateURL(final String requestedId);
    /**
     * Get a direct free-form URL to a datastore stream accessible to third-parties.
     * It's the responsibility of the datastore to make a stream secured and implemnt authentication if direct access is enabled
     * The third-parties are the private webservices (streamer), if Liquidsoap is the streamer then the protocol MUST be either http, ftp, smb, or a local file path (doesn't support File URI)
     * Throws an IOException if hasDirectSecuredAccessURL() returns false, i.e. there's no direct access to this stream or it's forbidden
     * @param channelId 
     * @param privateKey 
     * @param requestedId
     * @param requestedFormat 
     * @return
     * @throws IOException  
     */
    public String getDirectSecuredAccessPrivateURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param requestedFormat
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getDirectSecuredAccessPrivateURL(final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param channelId
     * @param privateKey
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getAudioDirectSecuredAccessPrivateURL(final String channelId, final String privateKey, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getAudioDirectSecuredAccessPrivateURL(final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param channelId
     * @param privateKey
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getPictureDirectSecuredAccessPrivateURL(final String channelId, final String privateKey, final String requestedId) throws IOException;
    /**
     * See public String getDirectSecuredAccessURL(final String channelId, final String privateKey, final FILE_FORMAT requestedFormat, final String requestedId) throws IOException;
     * @param requestedId
     * @return
     * @throws IOException
     */
    public String getPictureDirectSecuredAccessPrivateURL(final String requestedId) throws IOException;

Implement a new streamer module

A streamer is a piece of software that actually handles all required tasks for streaming multimedia from separate audio or video files.

A different instance of a streamer plays each Channel content in Shampoo.

Streamer adapters must conform to the biz.ddcr.shampoo.server.service.streamer.StreamerListenerInterface interface. This interface will evolve until version 1.0, and then remain stable.

Streamers that communicate with streaming webservices implementing this interface perform in "active" (non-listener) mode only. I.e. they send requests to Shampoo and only expect responses, Shampoo will never query them by itself. This behaviour is modelled after how Liquidsoap works, the main streaming software Shampoo is compatible with. setters are commands initiated by the streamer that Shampoo must acknowledge. And getters are commands that Shampoo is expected to respond to.

A generic streaming module, shaped as a REST webservice, is already implemented. See next section.

See the JavaDoc for further info.

biz.ddcr.shampoo.server.service.streamer.StreamerListenerInterface

/**
     * The streamer is up and running for the given channel. Should only be sent out when the streamer is first launched or if it was previously off.
     * @param channelId
     * @param privateKey
     * @throws Exception 
     */
    public void setStreamerOnAir(String channelId, String privateKey) throws Exception;
    /**
     * The stremaer is currently unavailable and cannot answer requests for the given channel.
     * @param channelId
     * @param privateKey
     * @throws Exception 
     */
    public void setStreamerOffAir(String channelId, String privateKey) throws Exception;
    /**
     * A live stream has currently started and is being handled by the streamer for the given channel.
     * Time is a Unix timestamp for this event.
     * @param channelId
     * @param privateKey
     * @param time
     * @throws Exception 
     */
    public void setLiveOnAir(String channelId, String privateKey, String timetableSlotId, Long time) throws Exception;
    /**
     * A live stream has currently started and is being handled by the streamer for the given channel.
     * Time is a Unix timestamp that specifies when this event was actually triggered.
     * No timestamp for this event is logged, a default one will be computed as soon as the message is received by the webservice.
     * @param channelId
     * @param privateKey
     * @throws Exception 
     */
    public void setLiveOnAir(String channelId, String timetableSlotId, String privateKey) throws Exception;
    /**
     * A live stream has just stopped or is not being handled by the streamer any more for the given channel.
     * No timestamp for this event is logged, a default one will be computed as soon as the message is received by the webservice.
     * @param channelId
     * @param privateKey
     * @param time
     * @throws Exception 
     */
    public void setLiveOffAir(String channelId, String timetableSlotId, String privateKey, Long time) throws Exception;
    /**
     * A live stream has just stopped or is not being handled by the streamer any more for the given channel.
     * Time is a Unix timestamp that specifies when this event was actually triggered.
     * @param channelId
     * @param privateKey
     * @param time
     * @throws Exception 
     */    
    public void setLiveOffAir(String channelId, String timetableSlotId, String privateKey) throws Exception;
    /**
     * Notifies that the specified queued item has started being played by the streamer. Mostly used by Shampoo to update the public webservices with 'currently on-air' info.
     * Time is a Unix timestamp that specifies when this event was actually triggered.
     * @param channelId
     * @param privateKey
     * @param queuedItemId
     * @param time
     * @throws Exception 
     */
    public void setItemOnAir(String channelId, String privateKey, String queuedItemId, Long time) throws Exception;
    /**
     * Notifies that the specified queued item has started being played by the streamer. Mostly used by Shampoo to update the public webservices with 'currently on-air' info.
     * No timestamp for this event is logged, a default one will be computed as soon as the message is received by the webservice. 
     * @param channelId
     * @param privateKey
     * @param queuedItemId
     * @throws Exception 
     */
    public void setItemOnAir(String channelId, String privateKey, String queuedItemId) throws Exception;
    /**
     * Notifies that the specified queued item that was previously played by the stremaer has just stopped. This track will be taken off from Shampoo's current queue and put into the Archives.
     * Time is a Unix timestamp that specifies when this event was actually triggered.
     * @param channelId
     * @param privateKey
     * @param queuedItemId
     * @param time
     * @throws Exception 
     */
    public void setItemOffAir(String channelId, String privateKey, String queuedItemId, Long time) throws Exception;
    /**
     * Notifies that the specified queued item that was previously played by the stremaer has just stopped. This track will be taken off from Shampoo's current queue and put into the Archives.
     * No timestamp for this event is logged, a default one will be computed as soon as the message is received by the webservice. 
     * @param channelId
     * @param privateKey
     * @param queuedItemId
     * @throws Exception 
     */
    public void setItemOffAir(String channelId, String privateKey, String queuedItemId) throws Exception;
    /**
     * Asks if a live is schedule for the given channel at the specified time, and returns its metadata.
     * @param channelId
     * @param privateKey
     * @param time
     * @return
     * @throws Exception 
     */
    public StreamableMetadataContainerInterface<StreamableLiveMetadataInterface> getLiveAt(String channelId, String privateKey, Long time) throws Exception;
    /**
     * Asks if a live should currently be on-air for the given channel and returns its metadata. Mostly used to retrieve the login and password and check if streaming clients are allowed to push this live to Liquidsoap.
     * @param channelId
     * @param privateKey
     * @return
     * @throws Exception 
     */
    public StreamableMetadataContainerInterface<StreamableLiveMetadataInterface> getCurrentLive(String channelId, String privateKey) throws Exception;
    /**
     * Returns the next item (track) to play for this channel. It will return the top track from Shampoo's internal queue that has not yet been queried by this method.
     * @param channelId
     * @param privateKey
     * @return
     * @throws Exception 
     */
    public StreamableMetadataContainerInterface<StreamableQueueItemMetadataInterface> popNextItem(String channelId, String privateKey) throws Exception;
    /**
     * Returns misc. data about the queried channel, those data are also echoed in responses from getNextItem() and getCurrentLive(). Useful for feeding ICY metadata as soon as the connection is established.
     * @param channelId
     * @param privateKey
     * @return
     * @throws Exception 
     */
    public StreamableMetadataContainerInterface<StreamableChannelMetadataInterface> getChannel(String channelId, String privateKey) throws Exception;

Private webservice (Shampoo-Streamer bridge adapter)

Instead of developping your own streamer module within Shampoo, you can use the bundled webservice to relay your streamer commands to Shampoo. It's a standard REST webservice that complies with the biz.ddcr.shampoo.server.service.streamer.StreamerListenerInterface interface. Compared to the public webservice, this one should remain hidden from public consumption and made only available to your streaming software. The authentication procedure is minimal and no bandwidth throttling or request number limitation is implemented here.

Endpoints

setters (using methods that start with the set prefix) are URLs accessible via HTTP POST.

getters (using methods that start with the get or pop prefix) are URLs accessible via HTTP GET.

host_and_deployment_path depends on the current Shampoo installation. See the deployment documentation for more info.

key and password are always mandatory, they're defined for each channel through the web frontend.

timestamp is optional. It specifies when the event exactly occurred, if omitted, the timestamp is the time this event was received and recorded. It's a Unix Epoch timestamp coded as a long integer (64bit).

When specified, queueID is mandatory, it is a queue identifier for an item to stream, as registered in the database.

  • setStreamerOnAir() http[s]://<host_and_deployment_path>/ws/http/streamer/on?c=<key>&k=<password>

Send this event when your streamer is offline and cannot process requests any more.

  • setStreamerOffAir() http[s]://<host_and_deployment_path>/ws/http/streamer/off?c=<key>&k=<password>

Send this event when your streamer is back online and can resume request processing. Triggering should only be meaningful if a previously sent event was setStreamerOffAir() or if no status has been recorded before, i.e. the streamer was previously offline or if it's just boot up.

  • setLiveOnAir() http[s]://<host_and_deployment_path>/ws/http/streamer/live/on?c=<key>&k=<password>[&t=<timestamp>]

Send this event when the streamer has just started processing and broadcasting a live.

  • setLiveOffAir() http[s]://<host_and_deployment_path>/ws/http/streamer/live/off?c=<key>&k=<password>[&t=<timestamp>]

Send this event when the streamer has just stopped processing and broadcasting a live.

  • setItemOnAir() http[s]://<host_and_deployment_path>/ws/http/streamer/item/on?c=<key>&k=<password>[&t=<timestamp>]&i=<queueID>

Send this event when the streamer has just started processing and broadcasting an item previously requested via popNextItem(). You must specify the track identifier that was previously given to you when you called popNextItem().

  • setItemOffAir() http[s]://<host_and_deployment_path>/ws/http/streamer/item/off?c=<key>&k=<password>[&t=<timestamp>]&i=<queueID>

Send this event when the streamer has just stopped processing and broadcasting an item previously requested via popNextItem(). You must specify the queue identifier that was previously given to you when you called popNextItem().

  • getLiveAt() and getCurrentLive() http[s]://<host_and_deployment_path>/ws/http/streamer/live/metadata?c=<key>&k=<password>[&t=<timestamp>]

Send this event when you want to check if a live is planned on the channel. Mostly useful to retrieve the login and password associated with the live in order for the streamer to properly handle authentication when a streaming client tries to connect.

  • popNextItem() http[s]://<host_and_deployment_path>/ws/http/streamer/item/next?c=<key>&k=<password>

Send this event to retrieve a track to queue for playing.

  • getChannel() http[s]://<host_and_deployment_path>/ws/http/streamer/channel/metadata?c=<key>&k=<password>

Send this event to retrieve a channel and the associated streamer configuration data.

The order of the sent events is important: A setXOnAir() event cannot be called before setXOffAir() for two sequential items in the queue, and items should be broadcast following the same sequence as the popNexItem() events they originate from. These constraints can be turned off within the source code, but they must remain active when Liquidsoap is the current streamer.

Responses

Standard HTTP status codes are issued in case of error. Details of the exceptions are available through the web frontend in the log section. setters will trigger a 200 HTTP status code if the command has been successfully received and acknowledged. getters will output the result of the command in a predefined format, if successful. Available formats are XML, JSON, Liquidsoap JSON, and Liquidsoap ANNOTATE. Liquidsoap JSON is a subset of the JSON format where all values are Strings. The current format for each channel is specified through the web frontend.

biz.ddcr.shampoo.server.domain.streamer.StreamableLiveMetadataInterface

getLiveAt() and getCurrentLive() responses comply to the biz.ddcr.shampoo.server.domain.streamer.StreamableLiveMetadataInterface interface. See source code for details.

biz.ddcr.shampoo.server.domain.streamer.StreamableQueueItemMetadataInterface

popNextItem() responses comply to the biz.ddcr.shampoo.server.domain.streamer.StreamableQueueItemMetadataInterface interface. See source code for details.

  • scheduledStartTime is a 64bit Unix timestamp that specifies when this item should be played. The streamer should try to respect this value. It represents the best time slot for preserving the most coherent queue schedule on Shampoo's end. Based on the time of events received by Shampoo, the queue scheduling is always updated and will adapt to the streamer's own timing, latencies, track skips, abrupt endings, etc.
  • Just like scheduledStartTime, the streamer should try to respect the authorizedPlayingDuration value. It represents the actual duration of the item to play, in seconds. Sometimes, it is necessary to not play a track to its full extent in order to preserve the original timetable schedule. Depending on the configuration of Programmes in Shampoo, Playlist changes might result in shorter tracks.
  • pictureURLEndpoint and itemURLEndpoint are the actual locations of the media files that the streamer should fetch for braodcast. pictureURLEndpoint is the cover art associated with this item. itemURLEndpoint is the track to queue and play for this item. The form of the URL depends on the current implementation of Shampoo's Datastore. The protocol can be HTTP or FTP for example.
  • The streamer should keep track of the specified queueID, this value is necessary when calling setItemOnAir() and setItemOffAir().

Example

popNextItem() response in plain XML:

<simple-streamable-track-queue-item-metadata>
  <picture-size>105183</picture-size>
  <copyright/>
  <scheduled-start-time>1310815152093</scheduled-start-time>
  <authorized-playing-duration>185.338</authorized-playing-duration>
  <requested>false</requested>
  <track-description/>
  <track-vBR>false</track-vBR>
  <track-format>mp3</track-format>
  <track-date-of-release>1977</track-date-of-release>
  <track-album>Plume De Poule</track-album>
  <track-duration>185.33878</track-duration>
  <channel-label>channel1</channel-label>
  <playlist-label>playlist1</playlist-label>
  <track-type>song</track-type>
  <playlist-iD>c6248794408b4def94335c386f9b0cb3</playlist-iD>
  <track-bitrate>224000</track-bitrate>
  <picture-uRLEndpoint>http://localhost:8080/ws/http/streamer/cover/down?c=channel1&k=0215681688&i=02bb98fd866346ba9db0b1cec7707259</picture-uRLEndpoint>
  <track-samplerate>44100</track-samplerate>
  <track-author>Shilum Kakawe & Les Oupa-Oupas</track-author>
  <track-title>Plume De Poule</track-title>
  <track-iD>02bb98fd866346ba9db0b1cec7707259</track-iD>
  <programme-rotation-count>0</programme-rotation-count>
  <track-genre>Disco</track-genre>
  <track-channels>2</track-channels>
  <picture-format>jpeg</picture-format>
  <track-size>5197764</track-size>
  <item-uRLEndpoint>http://localhost:8080/ws/http/streamer/track/down?c=channel1&k=0215681688&i=02bb98fd866346ba9db0b1cec7707259</item-uRLEndpoint>
  <playlist-description>Main playlist - Edit 1</playlist-description>
  <programme-label>Main Programme</programme-label>
  <tag/>
</simple-streamable-track-queue-item-metadata>

Public webservice

Some features of Shampoo can be integrated within your own websites, smartphone or desktop applications, etc via a bundled public webservice.

This webservice conforms to the biz.ddcr.shampoo.server.service.module.PublicListenerInterface interface. This interface will evolve until version 1.0, and then remain stable. It is shaped as JSON and XML REST webservice. Access rights and quota limitations are accessible through the Shampoo GUI, in the webservices area.

Modules

Features provided by this webservice are split into different modules, which are:

  • Now playing, what's currently playing on a given Channel.
  • Coming next, what will soon be played by a Channel.
  • Archive, the complete tracklisting of what has been played on a given Channel.
  • Timetable, the schedule and programmation of a given Channel.
  • Vote, gives the ability to rate a track or a show.
  • Request, request a track to be played on a given Channel.

They can all be activated and deactivated on-demand by Channel administrators.

Access rights and quotas

They are defined by Channel administrators on an API-key basis. A single application per host or machine should be given a unique API key.

Endpoints

setters (using methods that start with the set prefix) are URLs accessible via HTTP POST. getters (using methods that start with the get or pop prefix) are URLs accessible via HTTP GET.

host_and_deployment_path depends on the current Shampoo installation. See the deployment documentation for more info.

apiKey, ''hmac', and timestamp are always mandatory, they're defined for each channel through the web frontend. See next section for the detailed explanation on hmac computation. Timestamp is a Unix Epoch timestamp coded as a long integer (64bit).

metadataFormat is optional, it specifies the output format for getters. JSON and XML are valid values. If unspecified, the default format is XML.

  • fetchNowPlaying() http[s]://<host_and_deployment_path>/ws/http/public/nowplaying/metadata?a=<apiKey>&h=<hmac>&t=<timestamp>[&f=<metadataFormat>]

    Retrieves what's currently playing on the Channel defined for this API key.

TODO

HMAC

HMAC = md5(APIKey + md5(privateKey) + timestamp)

Where + is actual text concatenation operation and md5() the MD5 sum of a value.

TODO

Example

fetchNowPlaying() response in plain XML:

<?xml version="1.0" encoding="UTF-8"?>
<minimal-simple-streamable-track-queue-item-metadata>
<copyright/>
<scheduled-start-time>1317924345480</scheduled-start-time>
<authorized-playing-duration>272.509</authorized-playing-duration>
<type>track</type>
<requested>false</requested>
<track-description>-</track-description>
<track-date-of-release>2009</track-date-of-release>
<track-album>Idiosyncrasies</track-album>
<playlist-label>playlist1</playlist-label>
<track-type>song</track-type>
<picture-uRLEndpoint>http://localhost:8080/ShampooGWT/ws/http/public/down/media/cover?a=test123&amp;h=0757ee73dce13d0a251b8c8b2834d30a&amp;t=1317924418140&amp;i=b55cf900167240f78a63662382e088ae</picture-uRLEndpoint>
<track-author>Kris Menace</track-author>
<track-title>Idiosyncrasy</track-title>
<average-vote>2.5</average-vote>
<track-iD>b55cf900167240f78a63662382e088ae</track-iD>
<programme-rotation-count>24</programme-rotation-count>
<track-genre>House</track-genre>
<playlist-description/>
<programme-label>prgramme1</programme-label>
<friendly-caption>[prgramme1] Kris Menace - Idiosyncrasy (2009)</friendly-caption>
<tag/>
<channel-label>channel1</channel-label>
<channel-tag/>
<channel-description/>
<channel-uRL/>
</minimal-simple-streamable-track-queue-item-metadata>

You can download a code example from here, and see how to use the webservice 'Now playing' module. This short template comes in the form of a Wordpress plugin.

Updated