This is my own implementation of an autoplay plugin – based on the WaveInput plugin – for the squeezebox (LMS) software, written using literate programming so it’s easy to publish. It’s not quite a tutorial, but maybe someone will find it helpful.

Use case

I would like to automatically play a particular stream whenever a player is idle. I need this, so that the player connects to the output from amazon’s echo dot once playback of e.g. a radio stream has stopped.

Requirements:

  1. Detect idle players
  2. Start playing a stream after X seconds of idleness
  3. Provide means to specify…
    • which players to match
    • which stream to play
    • after how many idle seconds the stream should be played
    • whether players should be synchronized if more than one match

Autoplay plugin for squeezebox

The following are the required files for this plugin to work. Simply put them in a new directory inside your squeezebox’s plugin directory. Mine is at: /var/lib/squeezeserver/cache/InstalledPlugins/Autoplay

Plugin installation file

This is copied and adjusted from the WaveInput plugin. Apparently it is necessary.

install.xml

<?xml version="1.0"?>
<extension>
  <name>PLUGIN_AUTOPLAY</name>
  <module>Plugins::Autoplay::Plugin</module>
  <playerMenu>RADIO</playerMenu>
  <version>0.01</version>
  <description>PLUGIN_AUTOPLAY_DESC</description>
  <creator>bpa</creator>
  <defaultState>enabled</defaultState>
  <homepageURL></homepageURL>
  <optionsURL>plugins/Autoplay/settings/basic.html</optionsURL>
  <!-- <icon>plugins/Autoplay/html/images/waveinput.png</icon> -->
  <type>2</type><!-- type=extension -->
  <targetApplication>
    <id>Squeezecenter</id>
    <minVersion>7.3</minVersion>
    <maxVersion>7.*</maxVersion>
  </targetApplication>
  <targetPlatform>Linux</targetPlatform>
</extension>

Plugin strings file

The strings file contains a mapping from uppercase identifiers to translations for various languages.

strings.txt

# String file for Autoplay plugin

PLUGIN_AUTOPLAY
    EN  Autoplay
PLUGIN_AUTOPLAY_DESC
    EN  Autoplay is a plugin that automatically starts playing a stream after a player is idle for a certain time

PLUGIN_AUTOPLAY_CLIENTREGEX
    EN  Client regex
PLUGIN_AUTOPLAY_CLIENTREGEX_DESC
    EN  Regular expression that matches the client name which should automatically be started.

PLUGIN_AUTOPLAY_IDLETIME
    EN  Idle time
PLUGIN_AUTOPLAY_IDLETIME_DESC
    EN  Amount of time that must pass before auto-starting the specified stream.

PLUGIN_AUTOPLAY_STREAMURL
    EN  Stream URL
PLUGIN_AUTOPLAY_STREAMURL_DESC
    EN  The stream to start playing when idle for too long.

PLUGIN_AUTOPLAY_SYNC
    EN  Synchronization
PLUGIN_AUTOPLAY_SYNC_DESC
    EN  If enabled, syncronize all matching players.

Plugin settings file

The settings file is responsible for:

  1. serving and handling the HTTP based settings page
  2. providing defaults for settings

Settings.pm

package Plugins::Autoplay::Settings;

use strict;
use base qw(Slim::Web::Settings);

use Slim::Utils::Log;
use Slim::Utils::Prefs;
use Slim::Player::Client;
use Slim::Utils::OSDetect;

my $prefs = preferences('plugin.autoplay');
my $log   = logger('plugin.autoplay');
my $osdetected = Slim::Utils::OSDetect::OS();

my %defaults = (
    clientregex  => "Wohnzimmer",
    sync => 0,
    streamurl => "wavin:LoopbackAudioRecording",
    idletime => 5
    );

$log->debug("Settings called");

sub new {
    my $class = shift;
    $log->debug("New Settings");
    $class->SUPER::new;
}

sub name {

    # assumes at least SC 7.0
    if ( substr($::VERSION,0,3) lt 7.4 ) {
        return Slim::Web::HTTP::protectName('PLUGIN_AUTOPLAY');
    } else {
        # $::noweb to detect TinySC or user with no web interface
        if (!$::noweb) {
            return Slim::Web::HTTP::CSRF->protectName('PLUGIN_AUTOPLAY');
        }
    }

}

sub page {
    # assumes at least SC 7.0
    if ( substr($::VERSION,0,3) lt 7.4 ) {
        return Slim::Web::HTTP::protectURI('plugins/Autoplay/settings/basic.html');
    } else {
        # $::noweb to detect TinySC or user with no web interface
        if (!$::noweb) {
            return Slim::Web::HTTP::CSRF->protectURI('plugins/Autoplay/settings/basic.html');
        }
    }
}

sub prefs {
    $log->debug("Prefs called");
    return ($prefs,
            qw( clientregex ),
            qw( streamurl ),
            qw( idletime ),
            qw( sync ));
}

sub handler {
    my ($class, $client, $params) = @_;
    $log->debug("Handler called");

    if ($params->{'saveSettings'}) {
        $prefs->set('clientregex', qr/$params->{'clientregex'}/);
        $prefs->set('sync', $params->{'sync'});
        $prefs->set('streamurl', $params->{'streamurl'});
        $prefs->set('idletime', $params->{'idletime'});
    }
    return $class->SUPER::handler( $client, $params );
}

sub setDefaults {
    my $force = shift;

    foreach my $key (keys %defaults) {
        if (!defined($prefs->get($key)) || $force) {
            $log->debug("Missing pref value: Setting default value for $key: " . $defaults{$key});
            $prefs->set($key, $defaults{$key});
        }
    }
}

sub init {
    my $self = shift;
    $log->debug("Initializing settings");
    setDefaults(0);
}

1;

Plugin main file

The main plugin file is responsible for the actual autoplay logic. It…

  1. sets a timer which gets periodically refreshed to look for clients
  2. starts playing a selected stream for every client that is idle and matches a setting. It optionally syncs players if multiple match

This must reside inside a Plugin.pm file according to: http://wiki.slimdevices.com/index.php/SqueezeCenter_7_Plugins

Plugin.pm

#
# A plugin to automatically start playing a certain stream
#
use strict;

package Plugins::Autoplay::Plugin;

use base qw(Slim::Plugin::OPMLBased);

use Slim::Utils::Log;
use Slim::Utils::Prefs;
use Slim::Utils::Timers;
use Slim::Player::Client;

use Plugins::Autoplay::Settings;

# create log categogy before loading other modules
my $log = Slim::Utils::Log->addLogCategory({
    'category'     => 'plugin.autoplay',
    'defaultLevel' => 'ERROR',
    #       'defaultLevel' => 'INFO',
    'description'  => getDisplayName(),
});

use Slim::Utils::Misc;
my $prefs       = preferences('plugin.autoplay');

## -- settings --
my $checkInterval = 2;
my $lastIdleTimeCheck = 0;

$prefs->setValidate({ "validator" => 'intlimit', 'low' => 1, 'high' => 6000}, 'idletime');

sub isMatchingIdleClient {
    my $client = shift;
    my $clientRegex = $prefs->get("clientregex");
    if ( ($client->name() =~ ${clientRegex}) && $client->power() && !$client->isPlaying() ) {
        return 1;
    } else {
        return 0;
    }
}

sub getMatchingIdleClients {
    my @clients;
    for my $client (Slim::Player::Client::clients()) {
        if( isMatchingIdleClient( $client ) ) {
            push @clients, $client;
        }
    }

    return @clients;
}

sub checkClients {
    my $streamUrl = $prefs->get("streamurl");
    my $idleTimeBeforeAutoplay = $prefs->get("idletime");
    my @clients = getMatchingIdleClients();

    if ( scalar ( @clients ) > 0 ) {
        if ( $lastIdleTimeCheck == 0 ) {

            $lastIdleTimeCheck = Time::HiRes::time();

        } else {
            my $elapsedTime = Time::HiRes::time() - $lastIdleTimeCheck;
            if ( $idleTimeBeforeAutoplay < $elapsedTime ) {

                # sync clients
                if( $prefs->get('sync') ) {
                    my $mainClient = $clients[0];
                    my @otherClients = @clients[1 .. scalar(@clients) - 1];
                    $log->debug("Syncing first player " . $mainClient->name() . " with " . scalar(@otherClients) . " more.");
                    for my $otherClient (@otherClients) {
                        if(!$otherClient->isSynced()) {
                            $log->debug("Syncing " . $mainClient->name() . " with " . $otherClient->name());
                            $mainClient->controller()->sync($otherClient, 1);
                        }
                    }
                }

                for my $client (@clients) {
                    $log->debug("Player " . $client->name() . " currently powered, but idle for " . $elapsedTime);

                    if( !$prefs->get('sync') || $client == $clients[0] ) {
                        # power on/off because sometimes the stream gets corrupted after a while
                        $client->power(0);
                        $client->power(1);
                        $log->info("Auto-starting stream: " . $streamUrl . " on client " . $client->name());
                        $client->execute(["playlist", "play", $streamUrl]);
                    }
                    $lastIdleTimeCheck = 0;

                    # interesting:
                    # $client->controller()->activePlayers();
                }
            }

        }
    }
    Slim::Utils::Timers::setTimer(undef, Time::HiRes::time() + $checkInterval, \&checkClients);
}

################################
### Plugin Interface ###########
################################
sub initPlugin {
    my $class = shift;

    $log->info("Initialising " . $class->_pluginDataFor('version'));

    $class->SUPER::initPlugin(@_);

    Plugins::Autoplay::Settings->new($class);
    Plugins::Autoplay::Settings->init();

    #       Slim::Control::Request::subscribe( \&pauseCallback, [['pause']] );

    Slim::Utils::Timers::killTimers( undef, \&checkClients );
    Slim::Utils::Timers::setTimer(undef, Time::HiRes::time() + $checkInterval, \&checkClients);

    return 1;
}

sub shutdownPlugin
{
    #       Slim::Control::Request::unsubscribe(\&pauseCallback);
    Slim::Utils::Timers::killTimers( undef, \&checkClients );
    return;
}

sub getDisplayName()
{
    return('PLUGIN_AUTOPLAY')
}

1;

# Local Variables:
# tab-width:4
# indent-tabs-mode:t
# End:

HTML Setting page template

The HTML template is displayed as the setting page for the plugin.

HTML/EN/plugins/Autoplay/settings/basic.html

  [% PROCESS settings/header.html %]
    [% WRAPPER settingSection %]
          [% WRAPPER setting title="PLUGIN_AUTOPLAY_CLIENTREGEX" desc="PLUGIN_AUTOPLAY_CLIENTREGEX_DESC" %]
              <input type="text" name="pref_clientregex" value="[% prefs.clientregex %]" />
          [% END %]
          [% WRAPPER setting title="PLUGIN_AUTOPLAY_SYNC" desc="PLUGIN_AUTOPLAY_SYNC_DESC" %]
              <input type="checkbox" name="pref_sync" [% IF prefs.sync == 1 %] checked="checked" [% END %] value="1" />
          [% END %]
          [% WRAPPER setting title="PLUGIN_AUTOPLAY_STREAMURL" desc="PLUGIN_AUTOPLAY_STREAMURL_DESC" %]
              <input type="text" name="pref_streamurl" value="[% prefs.streamurl %]" />
          [% END %]
          [% WRAPPER setting title="PLUGIN_AUTOPLAY_IDLETIME" desc="PLUGIN_AUTOPLAY_IDLETIME_DESC" %]
              <input type="text" name="pref_idletime" value="[% prefs.idletime %]" />
          [% END %]
    [% END %]
  [% PROCESS settings/footer.html %]
Advertisements