by debacle » Sun Mar 01, 2009 4:54 pm
I've been working on this MediaMonkey <--> SqueezeCenter concept and thought I'd share some stuff. Thanks to everyone who contributed and got us started on this! I've added:
- OnSeek handler
- some error handling / stability
- a SqueezeCenter plugin that:
- Deals with '#' number signs in filenames (which was broken for me with the script - I assume everyone else would have the same problem).
- Allows you to control MediaMonkey from any client (at the moment, "Play", "Rew" (previous track), "Pause", and "Fwd" (next track) are supported).
- Changes Mute to toggle properly (before, my SqueezeBox clients would just mute every time, instead of toggling - I assume everyone else has this problem). Also, clients will be unmuted when they're powered off, so their volume is restored next time they power on.
Here's my SqueezeBox.vbs at the moment:
Code: Select all
'==========================================================================
' NAME: SqueezeBox Controller
'
' ORIGINAL AUTHOR: Todd Nemeth /revel
' DATE STARTED: 26.09.2007
'
' ADDITIONAL AUTHORS: Baz, Big_Berny, Peke, trixmoto
' UPDATE DATE: 28.02.2008
'
' COMMENT:
'- You need w3Sockets installed and registered from here:
' http://www.dimac.net/default3.asp?M=FreeDownloads/'Menu.asp&P=FreeDownloads/FreeDownloadsstart.asp
'- You will also need SlimServer installed and running.'
'==========================================================================
Option Explicit
Public SqueezeBoxSocket
Public ManualTrackChange
Sub OnStartup
Dim ind
Call Script.RegisterEvent(SDB,"OnPlay","Event_OnPlay")
Call Script.RegisterEvent(SDB,"OnPause","Event_OnPause")
Call Script.RegisterEvent(SDB,"OnStop","Event_OnStop")
Call Script.RegisterEvent(SDB,"OnSeek","Event_OnSeek")
Call Script.RegisterEvent(SDB,"OnShutdown","Event_OnShutdown")
' Call Script.RegisterEvent(SDB,"OnTrackEnd","Event_OnTrackEnd")
' Call Script.RegisterEvent(SDB,"OnPlaybackEnd","Event_OnPlaybackEnd")
' Call Script.RegisterEvent(SDB,"OnIdle","Event_OnIdle")
ind = SDB.UI.AddOptionSheet("SqueezeBox Controller",Script.ScriptPath,"InitSheet","SaveSheet",-2)
InitSqueezeBox
ManualTrackChange = True
Call SDB.Player.Play
End Sub
Sub InitSheet( Sheet)
Dim UI : Set UI = SDB.UI
Dim LabelAbout : Set LabelAbout = UI.NewLabel(Sheet)
LabelAbout.Multiline = True
LabelAbout.Common.SetRect 5,5,470,40
LabelAbout.Common.Anchors = 4
LabelAbout.Caption = "This small script will add ability to Control SqueezeBox using MediaMonkey Playback Controls"
Dim Label1 : Set Label1 = UI.NewLabel(Sheet)
Label1.Autosize = True
Label1.Common.SetRect 10,31,65,20
Label1.Common.Anchors = 4
Label1.Caption = SDB.Localize("SlimServer IP:")
Label1.Common.Hint = "Local IP address where SlimServer is Installed (127.0.0.1, 192.168.1.10, ...)"
Dim Edit1 : Set Edit1 = UI.NewEdit(Sheet)
Edit1.Common.ControlName = "SqueezeBoxIP"
Edit1.Common.SetRect 85,27,121,20
Edit1.Text = SDB.IniFile.StringValue("SqueezeBox","IP")
If Edit1.Text = "" Then
Edit1.Text = "127.0.0.1"
SDB.IniFile.StringValue("SqueezeBox","IP") = "127.0.0.1"
End If
Edit1.Common.Anchors = 1
Edit1.Common.Hint = "Enter Local IP address where SlimServer is Installed (127.0.0.1, 192.168.1.10, ...)"
End Sub
Sub SaveSheet(Sheet)
SDB.IniFile.StringValue("SqueezeBox","IP") = Sheet.Common.ChildControl("SqueezeBoxIP").Text
End Sub
Sub InitSqueezeBox
On Error Resume Next
Set SqueezeBoxSocket = CreateObject("Socket.TCP")
SqueezeBoxSocket.DoTelnetEmulation = True
SqueezeBoxSocket.TelnetEmulation = "TTY"
SqueezeBoxSocket.Host = SDB.IniFile.StringValue("SqueezeBox","IP") & ":9090"
SqueezeBoxSocket.Open
End Sub
Sub CloseSqueezeBox
On Error Resume Next
SqueezeBoxSocket.Close
End Sub
Sub SendSqueezeBoxCmd(strPlayerCmd)
On Error Resume Next
SqueezeBoxSocket.SendLine( "XXXXXXXXXX " & strPlayerCmd )
' Call ReadSqueezeBoxLine
End Sub
Sub ReadSqueezeBoxLine
On Error Resume Next
Dim line
line = SqueezeBoxSocket.GetLine
End Sub
Function SqueezeBoxMode
On Error Resume Next
SqueezeBoxMode = ""
' SqueezeBoxSocket.SendLine "mode ?"
' SqueezeBoxMode = SqueezeBoxSocket.GetLine
End Function
Function Max(a,b)
If a > b Then
Max = a
Else
Max = b
End If
End Function
Sub Event_OnPlay
Dim strRetVal, TrackPath
TrackPath = Escape(CheckPath(SDB.Player.CurrentSong))
TrackPath = Replace(TrackPath,"%5C","%2F")
TrackPath = Replace(TrackPath,"#","%23")
If ManualTrackChange = True Then
' MsgBox("Manual")
Call SendSqueezeBoxCmd("stop")
' MsgBox( TrackPath )
Call SendSqueezeBoxCmd("playlist play file%3A%2F%2F%2F" & TrackPath )
Else
' MsgBox("Auto")
Call SendSqueezeBoxCmd("playlist add file%3A%2F%2F%2F" & TrackPath )
ManualTrackChange = True
End If
End Sub
Function CheckPath(SongValue)
If Left(SongValue.Path,1) = "?" Then
If SongValue.Cached Then
CheckPath = SongValue.CachedPath
Else
CheckPath = SongValue.Path
End If
Else
CheckPath = SongValue.Path
End If
End Function
Sub Event_OnStop
Call SendSqueezeBoxCmd("stop")
End Sub
Sub Event_OnShutdown
On Error Resume Next
Call SendSqueezeBoxCmd("stop")
CloseSqueezeBox
End Sub
Sub Event_OnPause
If SDB.Player.IsPaused Then
Call SendSqueezeBoxCmd("pause FromMediaMonkeyOn")
Else
Call SendSqueezeBoxCmd("pause FromMediaMonkeyOff")
End If
End Sub
Sub Event_OnSeek
Dim strRetVal, TrackPath
strRetVal = SqueezeBoxMode
TrackPath = Escape(CheckPath(SDB.Player.CurrentSong))
TrackPath = Replace(TrackPath,"%5C","%2F")
If InStr(1,strRetval,"mode stop") > 0 Then
Call SendSqueezeBoxCmd("playlist play file%3A%2F%2F%2F" & TrackPath )
End If
Call SendSqueezeBoxCmd("time " & SDB.Player.PlaybackTime*0.001 )
End Sub
Sub Event_OnTrackEnd
MsgBox("Track End")
ManualTrackChange = False
End Sub
Sub Event_OnPlaybackEnd
MsgBox("Playback End")
ManualTrackChange = False
End Sub
Sub Event_OnIdle
MsgBox("Idle End")
ManualTrackChange = False
End Sub
Notes:
- I've added some "On Error" stuff to prevent error messages if SqueezeCenter happens to not be running.
- The "XXXX" bit is the MAC address of my "master" SoftSqueeze client (I describe my setup later). Basically, this client is always running, and MediaMonkey explicitly controls this client. If you don't specify a MAC address, SqueezeCenter will send the commands to some random connected client; this can cause headaches in some situations (for example, when connecting a streaming client to the stream.mp3).
- I tried adding a line to replace '#' with '%23', because SqueezeCenter was hanging on filenames with a '#' in them. But this didn't work - by the time the SqueezeCenter 'playlist play' command gets the filename, it has already been unescaped. So my SqueezeCenter plugin (discussed later) re-escapes the string before passing it to the main 'playlist play' handler. This works!
- I liked the 'playlist add' versus 'playlist play' idea, but I wanted it to immediately switch tracks when someone manually changes the track in MediaMonkey (as opposed to switching during automated playback). You'll see that I tried adding code to do this, but OnTrackEnd, OnPlaybackEnd, and OnIdle don't seem to be ever called! So I've disabled that for now, and always call 'playlist play'. Most tracks have a couple seconds of silence at the end, which is enough to prevent cutoffs in my setup.
- Notice that I never call 'GetLine' anymore. SqueezeCenter is supposed to send a response for every single command, but sometimes this seems to be delayed for several seconds. So I don't call it. This means I can't really query the 'mode' of the SqueezeBox client anymore, so I just assume that it is in sync with what MediaMonkey's doing. This could only cause problems for the OnSeek handler, which means that automated playback will never be affected.
- The "FromMediaMonkey" stuff is to prevent infinite loops with the SqueezeCenter plugin (code for that is given later).
- I've added a command to start playback once the connection is set up. This is so my music server restarts itself on system reboots.
Here's how my main SqueezeCenter plugin code looks right now:
Code: Select all
package Plugins::MediaMonkey::Plugin;
use strict;
no strict "refs";
use Slim::Utils::Prefs;
use Plugins::MediaMonkey::Logging;
use lib qw(H:\Perl\lib);
use OLE;
use URI::Escape;
# create a logging object
my $log = Plugins::MediaMonkey::Logging::new('MediaMonkey');
$log->setTypes('all');
$log->setLevel(20);
# get the preferences
my $prefs = preferences('server');
# global variables
my $callbackset = 0;
my $mm_plyr = 0;
my $ignoreNextPowerOnCommand = 0;
my $playback = 0;
my %volumes = ();
my $originalJumpCommand; # need to keep track of original
my $originalPauseCommand; # need to keep track of original
my $originalPlayCommand; # need to keep track of original
my $originalStopCommand; # need to keep track of original
my $originalMuteCommand; # need to keep track of original
my $originalVolumeCommand; # need to keep track of original
my $originalSyncCommand; # need to keep track of original
my $originalPowerCommand; # need to keep track of original
my $originalPlaylistPlayCommand;
use vars qw($VERSION);
$VERSION = &vlVersion();
# whether we're enabled (the callback remains in the function
# stack after "disabling" the plugin, so we need to keep track of this)
my $pluginEnabled = 0;
# setup routine
sub initPlugin
{
# note that we should act
if ( $callbackset == 0 ) {
$log->msg("wrapping functions\n");
# trap 'jump' so we can route it to MediaMonkey
$originalJumpCommand = Slim::Control::Request::addDispatch(['playlist', 'index', '_index', '_noplay', '_seekdata'], [1, 0, 0, \&MediaMonkey_jumpCommand]);
my $originalJump2Command = Slim::Control::Request::addDispatch(['playlist', 'jump', '_index', '_noplay', '_seekdata'], [1, 0, 0, \&MediaMonkey_jumpCommand]);
if ( (!defined($originalJumpCommand)) || (ref($originalJumpCommand ) ne 'CODE') || ($originalJumpCommand ne $originalJump2Command) ) {
$log->msg("problem wrapping 'jump' command!\n");
}
# trap 'pause' so we can route it to MediaMonkey
$originalPauseCommand = Slim::Control::Request::addDispatch(['pause', '_newvalue'], [1, 0, 0, \&MediaMonkey_pauseCommand]);
if ( (!defined($originalPauseCommand)) || (ref($originalPauseCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'pause' command!\n");
}
# trap 'play' so we can route it to MediaMonkey
$originalPlayCommand = Slim::Control::Request::addDispatch(['play', '_newvalue'], [1, 0, 0, \&MediaMonkey_playCommand]);
if ( (!defined($originalPlayCommand)) || (ref($originalPlayCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'play' command!\n");
}
# trap 'stop' so we can route it to MediaMonkey
$originalStopCommand = Slim::Control::Request::addDispatch(['stop'], [1, 0, 0, \&MediaMonkey_stopCommand]);
if ( (!defined($originalStopCommand)) || (ref($originalStopCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'stop' command!\n");
}
# trap 'mute' so we can route it to MediaMonkey
$originalMuteCommand = Slim::Control::Request::addDispatch(['mixer', 'muting', '_newvalue'], [1, 0, 0, \&MediaMonkey_muteCommand]);
if ( (!defined($originalMuteCommand)) || (ref($originalMuteCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'mute' command!\n");
}
# trap 'volume' so we can route it to MediaMonkey
$originalVolumeCommand = Slim::Control::Request::addDispatch(['mixer', 'volume', '_newvalue'], [1, 0, 0, \&MediaMonkey_volumeCommand]);
if ( (!defined($originalVolumeCommand)) || (ref($originalVolumeCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'volume' command!\n");
}
# trap 'sync' for inspection
$originalSyncCommand = Slim::Control::Request::addDispatch(['sync','_indexid-'],[1, 0, 0, \&MediaMonkey_syncCommand]);
if ( (!defined($originalSyncCommand)) || (ref($originalSyncCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'sync' command!\n");
}
# trap 'power' for inspection
$originalPowerCommand = Slim::Control::Request::addDispatch(['power','_newvalue'],[1, 0, 0, \&MediaMonkey_powerCommand]);
if ( (!defined($originalPowerCommand)) || (ref($originalPowerCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'power' command!\n");
}
# trap 'playlist play' for inspection
$originalPlaylistPlayCommand = Slim::Control::Request::addDispatch(['playlist', 'play', '_item', '_title'],[1, 0, 0, \&MediaMonkey_playlistPlayCommand]);
if ( (!defined($originalPlaylistPlayCommand)) || (ref($originalPlaylistPlayCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'playlist play' command!\n");
}
$callbackset = 1;
}
$pluginEnabled = 1;
$log->msg("MediaMonkey plugin enabled\n");
}
sub shutdownPlugin
{
$log->msg("MediaMonkey plugin shutting down");
# we should not act
$pluginEnabled = 0;
$log->msg("MediaMonkey plugin disabled\n");
}
sub enabled
{
return 1;
}
sub vlVersion()
{
my $rcsVersion = '$Revision: 1.0 $';
$rcsVersion =~ s/.*:\s*([0-9\.]*).*$/$1/;
return $rcsVersion;
}
sub getDisplayName()
{
return 'PLUGIN_MEDIAMONKEY';
}
sub connectToMM()
{
if( !$mm_plyr )
{
$log->msg("getting MediaMonkey object\n");
my $mm_app = CreateObject OLE 'SongsDB.SDBApplication' || die $!;
$mm_app->{'ShutdownAfterDisconnect'} = 0;
$mm_plyr = CreateObject OLE 'SongsDB.SDBPlayer' || die $!;
}
}
# our 'jump' wrapper function
sub MediaMonkey_jumpCommand
{
$playback = 1;
my @args = @_;
$ignoreNextPowerOnCommand = 1;
my $request = $args[0];
my $client = $request->client();
my $index = $request->getParam('_index');
my $noplay = $request->getParam('_noplay');
my $seekdata = $request->getParam('_seekdata');
$log->msg("MediaMonkey_jumpCommand running:\n");
$log->msg(" _index: " . $index . "\n");
$log->msg(" _noplay: " . $noplay . "\n");
$log->msg(" _seekdata: " . $seekdata . "\n");
if( !$pluginEnabled || !defined( $index ) )
{
my $rc = &$originalJumpCommand(@args);
return $rc;
}
$log->msg("MediaMonkey_jumpCommand taking over.\n");
if( defined( $index ) )
{
if( $client->power() )
{
connectToMM();
if( $index > 0 )
{
$mm_plyr->Next();
}
elsif( $index < 0 )
{
$mm_plyr->Previous();
}
else
{
$mm_plyr->Play();
}
}
}
else
{
$ignoreNextPowerOnCommand = 0;
}
$request->setStatusDone();
}
# our 'pause' wrapper function
sub MediaMonkey_pauseCommand
{
my @args = @_;
my $request = $args[0];
my $newvalue = $request->getParam('_newvalue');
my $fromMM = 0;
$log->msg("MediaMonkey_pauseCommand running:\n");
$log->msg(" _newvalue: " . $newvalue . "\n");
if( !$pluginEnabled )
{
my $rc = &$originalPauseCommand(@args);
return $rc;
}
if( $newvalue eq "FromMediaMonkeyOn" )
{
$request->deleteParam('_newvalue');
$request->addParam('_newvalue', 1);
my $rc = &$originalPauseCommand(@args);
return $rc;
}
elsif( $newvalue eq "FromMediaMonkeyOff" )
{
$request->{'_request'}[0] = 'play';
$request->deleteParam('_newvalue');
my $rc = &$originalPlayCommand(@args);
return $rc;
}
$log->msg("MediaMonkey_pauseCommand taking over.\n");
connectToMM();
$mm_plyr->Pause();
$request->setStatusDone();
}
# our 'play' wrapper function
sub MediaMonkey_playCommand
{
$playback = 1;
my @args = @_;
$log->msg("MediaMonkey_playCommand running.\n");
my $rc = &$originalPlayCommand(@args);
return $rc;
}
# our 'stop' wrapper function
sub MediaMonkey_stopCommand
{
my @args = @_;
$log->msg("MediaMonkey_stopCommand running.\n");
my $rc = &$originalStopCommand(@args);
return $rc;
}
# our 'mute' wrapper function
sub MediaMonkey_muteCommand
{
my @args = @_;
my $request = $args[0];
$log->msg("MediaMonkey_muteCommand running.\n");
if( $pluginEnabled )
{
my $client = $request->client();
if( $client->volume() == 0 )
{
$request->{'_request'}[1] = 'volume';
$request->deleteParam('_newvalue');
$request->addParam('_newvalue', $volumes{ $client } );
my $rc = MediaMonkey_volumeCommand(@args);
return $rc;
}
else
{
$volumes{ $client } = $client->volume();
}
}
my $rc = &$originalMuteCommand(@args);
return $rc;
}
# our 'volume' wrapper function
sub MediaMonkey_volumeCommand
{
my @args = @_;
my $request = $args[0];
my $client = $request->client();
$log->msg("MediaMonkey_volumeCommand running.\n");
my $rc = &$originalVolumeCommand(@args);
$volumes{ $client } = $client->volume();
$client->mixerDisplay('volume');
return $rc;
}
# our 'sync' wrapper function
sub MediaMonkey_syncCommand
{
my @args = @_;
my $request = $args[0];
my $indexid = $request->getParam('_indexid-');
$log->msg("MediaMonkey_syncCommand running:\n");
$log->msg(" _indexid: " . $indexid . "\n");
my $rc = &$originalSyncCommand(@args);
return $rc;
}
# our 'power' wrapper function
sub MediaMonkey_powerCommand
{
my @args = @_;
my $request = $args[0];
my $newvalue = $request->getParam('_newvalue');
$log->msg("MediaMonkey_powerCommand running:\n");
$log->msg(" _newvalue: " . $newvalue . "\n");
if( $pluginEnabled )
{
if( $newvalue == 1 )
{
if( 0 && $ignoreNextPowerOnCommand )
{
$ignoreNextPowerOnCommand = 0;
return;
}
}
else
{
my $client = $request->client();
if( $client->volume() == 0 && exists( $volumes{ $client } ) )
{
$client->volume( $volumes{ $client } );
}
}
}
$ignoreNextPowerOnCommand = 0;
my $rc = &$originalPowerCommand(@args);
if( $pluginEnabled && $newvalue == 1 && $playback )
{
connectToMM();
if( $mm_plyr->{'isPaused'} == 1 )
{
$mm_plyr->Pause();
}
}
return $rc;
}
# our 'playlistPlay' wrapper function
sub MediaMonkey_playlistPlayCommand
{
my @args = @_;
my $request = $args[0];
my $item = $request->getParam('_item');
my $escape = uri_escape(uri_unescape($item));
$escape =~ s/%3A/:/g;
$escape =~ s/%2F/\//g;
my $title = $request->getParam('_title');
$log->msg("MediaMonkey_playlistPlayCommand running.\n");
$log->msg(" item: " . $item . "\n");
$log->msg(" escape: " . $escape . "\n");
$log->msg(" title: " . $title . "\n");
$request->deleteParam('_item');
$request->addParam('_item', $escape );
my $rc = &$originalPlaylistPlayCommand(@args);
return $rc;
}
1;
Notes:
- A lot of trial and error went into tweaking this. I tried other, completely different approaches. In the end, this is what I came up with.
- You can look up other example plugins online to see what other files you need to make a functional plugin, where to put the files, etc. If people are interested, I can post a .zip file or something.
- This doesn't connect to MediaMonkey until it needs to, so you can start up SqueezeCenter and then launch MediaMonkey (in that order), and the applications will talk to each other.
- Notice that the 'playlist play' handler re-escapes the filename to handle the '#' problem. It first runs an unescape, in case the filename came from SqueezeCenter itself (in which case it is already escaped). It also puts back ':' colons and '/' slashes - I don't think the main 'playlist play' handler likes those to be escaped.
My Setup:
- I have a "server" box (old headless PC) running in a corner of my living room. It runs MediaMonkey, SqueezeCenter, and the "master" SqueezeBox client. It is connected via cable to my stereo receiver nearby.
- When the server machine starts up, it starts SqueezeCenter, gives it time to get started, then starts the "master" client, waits a little more time, then starts up MediaMonkey. I'm tweaking this to try and make it work 100% of the time without taking too long to get going.
- Other PCs in the house can run SqueezeBox clients, and they are set to synchronize with the "master" client. Since the master is always running, you instantly hook into the stream when you connect a client.
- MediaMonkey volume is set to 0. I use the output from the "master" client for my stereo, that way it is totally in sync with the other clients in the house, in case you can hear two at once.
I've been working on this MediaMonkey <--> SqueezeCenter concept and thought I'd share some stuff. Thanks to everyone who contributed and got us started on this! I've added:
[list]
[*]OnSeek handler[/*]
[*]some error handling / stability[/*]
[*]a SqueezeCenter plugin that:
[list]
[*]Deals with '#' number signs in filenames (which was broken for me with the script - I assume everyone else would have the same problem).[/*]
[*]Allows you to control MediaMonkey from any client (at the moment, "Play", "Rew" (previous track), "Pause", and "Fwd" (next track) are supported).[/*]
[*]Changes Mute to toggle properly (before, my SqueezeBox clients would just mute every time, instead of toggling - I assume everyone else has this problem). Also, clients will be unmuted when they're powered off, so their volume is restored next time they power on.[/*]
[/list][/*]
[/list]
Here's my SqueezeBox.vbs at the moment:
[code]
'==========================================================================
' NAME: SqueezeBox Controller
'
' ORIGINAL AUTHOR: Todd Nemeth /revel
' DATE STARTED: 26.09.2007
'
' ADDITIONAL AUTHORS: Baz, Big_Berny, Peke, trixmoto
' UPDATE DATE: 28.02.2008
'
' COMMENT:
'- You need w3Sockets installed and registered from here:
' http://www.dimac.net/default3.asp?M=FreeDownloads/'Menu.asp&P=FreeDownloads/FreeDownloadsstart.asp
'- You will also need SlimServer installed and running.'
'==========================================================================
Option Explicit
Public SqueezeBoxSocket
Public ManualTrackChange
Sub OnStartup
Dim ind
Call Script.RegisterEvent(SDB,"OnPlay","Event_OnPlay")
Call Script.RegisterEvent(SDB,"OnPause","Event_OnPause")
Call Script.RegisterEvent(SDB,"OnStop","Event_OnStop")
Call Script.RegisterEvent(SDB,"OnSeek","Event_OnSeek")
Call Script.RegisterEvent(SDB,"OnShutdown","Event_OnShutdown")
' Call Script.RegisterEvent(SDB,"OnTrackEnd","Event_OnTrackEnd")
' Call Script.RegisterEvent(SDB,"OnPlaybackEnd","Event_OnPlaybackEnd")
' Call Script.RegisterEvent(SDB,"OnIdle","Event_OnIdle")
ind = SDB.UI.AddOptionSheet("SqueezeBox Controller",Script.ScriptPath,"InitSheet","SaveSheet",-2)
InitSqueezeBox
ManualTrackChange = True
Call SDB.Player.Play
End Sub
Sub InitSheet( Sheet)
Dim UI : Set UI = SDB.UI
Dim LabelAbout : Set LabelAbout = UI.NewLabel(Sheet)
LabelAbout.Multiline = True
LabelAbout.Common.SetRect 5,5,470,40
LabelAbout.Common.Anchors = 4
LabelAbout.Caption = "This small script will add ability to Control SqueezeBox using MediaMonkey Playback Controls"
Dim Label1 : Set Label1 = UI.NewLabel(Sheet)
Label1.Autosize = True
Label1.Common.SetRect 10,31,65,20
Label1.Common.Anchors = 4
Label1.Caption = SDB.Localize("SlimServer IP:")
Label1.Common.Hint = "Local IP address where SlimServer is Installed (127.0.0.1, 192.168.1.10, ...)"
Dim Edit1 : Set Edit1 = UI.NewEdit(Sheet)
Edit1.Common.ControlName = "SqueezeBoxIP"
Edit1.Common.SetRect 85,27,121,20
Edit1.Text = SDB.IniFile.StringValue("SqueezeBox","IP")
If Edit1.Text = "" Then
Edit1.Text = "127.0.0.1"
SDB.IniFile.StringValue("SqueezeBox","IP") = "127.0.0.1"
End If
Edit1.Common.Anchors = 1
Edit1.Common.Hint = "Enter Local IP address where SlimServer is Installed (127.0.0.1, 192.168.1.10, ...)"
End Sub
Sub SaveSheet(Sheet)
SDB.IniFile.StringValue("SqueezeBox","IP") = Sheet.Common.ChildControl("SqueezeBoxIP").Text
End Sub
Sub InitSqueezeBox
On Error Resume Next
Set SqueezeBoxSocket = CreateObject("Socket.TCP")
SqueezeBoxSocket.DoTelnetEmulation = True
SqueezeBoxSocket.TelnetEmulation = "TTY"
SqueezeBoxSocket.Host = SDB.IniFile.StringValue("SqueezeBox","IP") & ":9090"
SqueezeBoxSocket.Open
End Sub
Sub CloseSqueezeBox
On Error Resume Next
SqueezeBoxSocket.Close
End Sub
Sub SendSqueezeBoxCmd(strPlayerCmd)
On Error Resume Next
SqueezeBoxSocket.SendLine( "XXXXXXXXXX " & strPlayerCmd )
' Call ReadSqueezeBoxLine
End Sub
Sub ReadSqueezeBoxLine
On Error Resume Next
Dim line
line = SqueezeBoxSocket.GetLine
End Sub
Function SqueezeBoxMode
On Error Resume Next
SqueezeBoxMode = ""
' SqueezeBoxSocket.SendLine "mode ?"
' SqueezeBoxMode = SqueezeBoxSocket.GetLine
End Function
Function Max(a,b)
If a > b Then
Max = a
Else
Max = b
End If
End Function
Sub Event_OnPlay
Dim strRetVal, TrackPath
TrackPath = Escape(CheckPath(SDB.Player.CurrentSong))
TrackPath = Replace(TrackPath,"%5C","%2F")
TrackPath = Replace(TrackPath,"#","%23")
If ManualTrackChange = True Then
' MsgBox("Manual")
Call SendSqueezeBoxCmd("stop")
' MsgBox( TrackPath )
Call SendSqueezeBoxCmd("playlist play file%3A%2F%2F%2F" & TrackPath )
Else
' MsgBox("Auto")
Call SendSqueezeBoxCmd("playlist add file%3A%2F%2F%2F" & TrackPath )
ManualTrackChange = True
End If
End Sub
Function CheckPath(SongValue)
If Left(SongValue.Path,1) = "?" Then
If SongValue.Cached Then
CheckPath = SongValue.CachedPath
Else
CheckPath = SongValue.Path
End If
Else
CheckPath = SongValue.Path
End If
End Function
Sub Event_OnStop
Call SendSqueezeBoxCmd("stop")
End Sub
Sub Event_OnShutdown
On Error Resume Next
Call SendSqueezeBoxCmd("stop")
CloseSqueezeBox
End Sub
Sub Event_OnPause
If SDB.Player.IsPaused Then
Call SendSqueezeBoxCmd("pause FromMediaMonkeyOn")
Else
Call SendSqueezeBoxCmd("pause FromMediaMonkeyOff")
End If
End Sub
Sub Event_OnSeek
Dim strRetVal, TrackPath
strRetVal = SqueezeBoxMode
TrackPath = Escape(CheckPath(SDB.Player.CurrentSong))
TrackPath = Replace(TrackPath,"%5C","%2F")
If InStr(1,strRetval,"mode stop") > 0 Then
Call SendSqueezeBoxCmd("playlist play file%3A%2F%2F%2F" & TrackPath )
End If
Call SendSqueezeBoxCmd("time " & SDB.Player.PlaybackTime*0.001 )
End Sub
Sub Event_OnTrackEnd
MsgBox("Track End")
ManualTrackChange = False
End Sub
Sub Event_OnPlaybackEnd
MsgBox("Playback End")
ManualTrackChange = False
End Sub
Sub Event_OnIdle
MsgBox("Idle End")
ManualTrackChange = False
End Sub
[/code]
[b]Notes:[/b]
[list]
[*]I've added some "On Error" stuff to prevent error messages if SqueezeCenter happens to not be running.[/*]
[*]The "XXXX" bit is the MAC address of my "master" SoftSqueeze client (I describe my setup later). Basically, this client is [i]always[/i] running, and MediaMonkey explicitly controls this client. If you don't specify a MAC address, SqueezeCenter will send the commands to some random connected client; this can cause headaches in some situations (for example, when connecting a streaming client to the stream.mp3).[/*]
[*]I tried adding a line to replace '#' with '%23', because SqueezeCenter was hanging on filenames with a '#' in them. But this didn't work - by the time the SqueezeCenter 'playlist play' command gets the filename, it has already been unescaped. So my SqueezeCenter plugin (discussed later) re-escapes the string before passing it to the main 'playlist play' handler. This works![/*]
[*]I liked the 'playlist add' versus 'playlist play' idea, but I wanted it to immediately switch tracks when someone manually changes the track in MediaMonkey (as opposed to switching during automated playback). You'll see that I tried adding code to do this, but OnTrackEnd, OnPlaybackEnd, and OnIdle don't seem to be ever called! So I've disabled that for now, and always call 'playlist play'. Most tracks have a couple seconds of silence at the end, which is enough to prevent cutoffs in my setup.[/*]
[*]Notice that I never call 'GetLine' anymore. SqueezeCenter is supposed to send a response for every single command, but sometimes this seems to be delayed for several seconds. So I don't call it. This means I can't really query the 'mode' of the SqueezeBox client anymore, so I just assume that it is in sync with what MediaMonkey's doing. This could only cause problems for the OnSeek handler, which means that automated playback will never be affected.[/*]
[*]The "FromMediaMonkey" stuff is to prevent infinite loops with the SqueezeCenter plugin (code for that is given later).[/*]
[*]I've added a command to start playback once the connection is set up. This is so my music server restarts itself on system reboots.[/*]
[/list]
Here's how my main SqueezeCenter plugin code looks right now:
[code]
package Plugins::MediaMonkey::Plugin;
use strict;
no strict "refs";
use Slim::Utils::Prefs;
use Plugins::MediaMonkey::Logging;
use lib qw(H:\Perl\lib);
use OLE;
use URI::Escape;
# create a logging object
my $log = Plugins::MediaMonkey::Logging::new('MediaMonkey');
$log->setTypes('all');
$log->setLevel(20);
# get the preferences
my $prefs = preferences('server');
# global variables
my $callbackset = 0;
my $mm_plyr = 0;
my $ignoreNextPowerOnCommand = 0;
my $playback = 0;
my %volumes = ();
my $originalJumpCommand; # need to keep track of original
my $originalPauseCommand; # need to keep track of original
my $originalPlayCommand; # need to keep track of original
my $originalStopCommand; # need to keep track of original
my $originalMuteCommand; # need to keep track of original
my $originalVolumeCommand; # need to keep track of original
my $originalSyncCommand; # need to keep track of original
my $originalPowerCommand; # need to keep track of original
my $originalPlaylistPlayCommand;
use vars qw($VERSION);
$VERSION = &vlVersion();
# whether we're enabled (the callback remains in the function
# stack after "disabling" the plugin, so we need to keep track of this)
my $pluginEnabled = 0;
# setup routine
sub initPlugin
{
# note that we should act
if ( $callbackset == 0 ) {
$log->msg("wrapping functions\n");
# trap 'jump' so we can route it to MediaMonkey
$originalJumpCommand = Slim::Control::Request::addDispatch(['playlist', 'index', '_index', '_noplay', '_seekdata'], [1, 0, 0, \&MediaMonkey_jumpCommand]);
my $originalJump2Command = Slim::Control::Request::addDispatch(['playlist', 'jump', '_index', '_noplay', '_seekdata'], [1, 0, 0, \&MediaMonkey_jumpCommand]);
if ( (!defined($originalJumpCommand)) || (ref($originalJumpCommand ) ne 'CODE') || ($originalJumpCommand ne $originalJump2Command) ) {
$log->msg("problem wrapping 'jump' command!\n");
}
# trap 'pause' so we can route it to MediaMonkey
$originalPauseCommand = Slim::Control::Request::addDispatch(['pause', '_newvalue'], [1, 0, 0, \&MediaMonkey_pauseCommand]);
if ( (!defined($originalPauseCommand)) || (ref($originalPauseCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'pause' command!\n");
}
# trap 'play' so we can route it to MediaMonkey
$originalPlayCommand = Slim::Control::Request::addDispatch(['play', '_newvalue'], [1, 0, 0, \&MediaMonkey_playCommand]);
if ( (!defined($originalPlayCommand)) || (ref($originalPlayCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'play' command!\n");
}
# trap 'stop' so we can route it to MediaMonkey
$originalStopCommand = Slim::Control::Request::addDispatch(['stop'], [1, 0, 0, \&MediaMonkey_stopCommand]);
if ( (!defined($originalStopCommand)) || (ref($originalStopCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'stop' command!\n");
}
# trap 'mute' so we can route it to MediaMonkey
$originalMuteCommand = Slim::Control::Request::addDispatch(['mixer', 'muting', '_newvalue'], [1, 0, 0, \&MediaMonkey_muteCommand]);
if ( (!defined($originalMuteCommand)) || (ref($originalMuteCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'mute' command!\n");
}
# trap 'volume' so we can route it to MediaMonkey
$originalVolumeCommand = Slim::Control::Request::addDispatch(['mixer', 'volume', '_newvalue'], [1, 0, 0, \&MediaMonkey_volumeCommand]);
if ( (!defined($originalVolumeCommand)) || (ref($originalVolumeCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'volume' command!\n");
}
# trap 'sync' for inspection
$originalSyncCommand = Slim::Control::Request::addDispatch(['sync','_indexid-'],[1, 0, 0, \&MediaMonkey_syncCommand]);
if ( (!defined($originalSyncCommand)) || (ref($originalSyncCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'sync' command!\n");
}
# trap 'power' for inspection
$originalPowerCommand = Slim::Control::Request::addDispatch(['power','_newvalue'],[1, 0, 0, \&MediaMonkey_powerCommand]);
if ( (!defined($originalPowerCommand)) || (ref($originalPowerCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'power' command!\n");
}
# trap 'playlist play' for inspection
$originalPlaylistPlayCommand = Slim::Control::Request::addDispatch(['playlist', 'play', '_item', '_title'],[1, 0, 0, \&MediaMonkey_playlistPlayCommand]);
if ( (!defined($originalPlaylistPlayCommand)) || (ref($originalPlaylistPlayCommand ) ne 'CODE') ) {
$log->msg("problem wrapping 'playlist play' command!\n");
}
$callbackset = 1;
}
$pluginEnabled = 1;
$log->msg("MediaMonkey plugin enabled\n");
}
sub shutdownPlugin
{
$log->msg("MediaMonkey plugin shutting down");
# we should not act
$pluginEnabled = 0;
$log->msg("MediaMonkey plugin disabled\n");
}
sub enabled
{
return 1;
}
sub vlVersion()
{
my $rcsVersion = '$Revision: 1.0 $';
$rcsVersion =~ s/.*:\s*([0-9\.]*).*$/$1/;
return $rcsVersion;
}
sub getDisplayName()
{
return 'PLUGIN_MEDIAMONKEY';
}
sub connectToMM()
{
if( !$mm_plyr )
{
$log->msg("getting MediaMonkey object\n");
my $mm_app = CreateObject OLE 'SongsDB.SDBApplication' || die $!;
$mm_app->{'ShutdownAfterDisconnect'} = 0;
$mm_plyr = CreateObject OLE 'SongsDB.SDBPlayer' || die $!;
}
}
# our 'jump' wrapper function
sub MediaMonkey_jumpCommand
{
$playback = 1;
my @args = @_;
$ignoreNextPowerOnCommand = 1;
my $request = $args[0];
my $client = $request->client();
my $index = $request->getParam('_index');
my $noplay = $request->getParam('_noplay');
my $seekdata = $request->getParam('_seekdata');
$log->msg("MediaMonkey_jumpCommand running:\n");
$log->msg(" _index: " . $index . "\n");
$log->msg(" _noplay: " . $noplay . "\n");
$log->msg(" _seekdata: " . $seekdata . "\n");
if( !$pluginEnabled || !defined( $index ) )
{
my $rc = &$originalJumpCommand(@args);
return $rc;
}
$log->msg("MediaMonkey_jumpCommand taking over.\n");
if( defined( $index ) )
{
if( $client->power() )
{
connectToMM();
if( $index > 0 )
{
$mm_plyr->Next();
}
elsif( $index < 0 )
{
$mm_plyr->Previous();
}
else
{
$mm_plyr->Play();
}
}
}
else
{
$ignoreNextPowerOnCommand = 0;
}
$request->setStatusDone();
}
# our 'pause' wrapper function
sub MediaMonkey_pauseCommand
{
my @args = @_;
my $request = $args[0];
my $newvalue = $request->getParam('_newvalue');
my $fromMM = 0;
$log->msg("MediaMonkey_pauseCommand running:\n");
$log->msg(" _newvalue: " . $newvalue . "\n");
if( !$pluginEnabled )
{
my $rc = &$originalPauseCommand(@args);
return $rc;
}
if( $newvalue eq "FromMediaMonkeyOn" )
{
$request->deleteParam('_newvalue');
$request->addParam('_newvalue', 1);
my $rc = &$originalPauseCommand(@args);
return $rc;
}
elsif( $newvalue eq "FromMediaMonkeyOff" )
{
$request->{'_request'}[0] = 'play';
$request->deleteParam('_newvalue');
my $rc = &$originalPlayCommand(@args);
return $rc;
}
$log->msg("MediaMonkey_pauseCommand taking over.\n");
connectToMM();
$mm_plyr->Pause();
$request->setStatusDone();
}
# our 'play' wrapper function
sub MediaMonkey_playCommand
{
$playback = 1;
my @args = @_;
$log->msg("MediaMonkey_playCommand running.\n");
my $rc = &$originalPlayCommand(@args);
return $rc;
}
# our 'stop' wrapper function
sub MediaMonkey_stopCommand
{
my @args = @_;
$log->msg("MediaMonkey_stopCommand running.\n");
my $rc = &$originalStopCommand(@args);
return $rc;
}
# our 'mute' wrapper function
sub MediaMonkey_muteCommand
{
my @args = @_;
my $request = $args[0];
$log->msg("MediaMonkey_muteCommand running.\n");
if( $pluginEnabled )
{
my $client = $request->client();
if( $client->volume() == 0 )
{
$request->{'_request'}[1] = 'volume';
$request->deleteParam('_newvalue');
$request->addParam('_newvalue', $volumes{ $client } );
my $rc = MediaMonkey_volumeCommand(@args);
return $rc;
}
else
{
$volumes{ $client } = $client->volume();
}
}
my $rc = &$originalMuteCommand(@args);
return $rc;
}
# our 'volume' wrapper function
sub MediaMonkey_volumeCommand
{
my @args = @_;
my $request = $args[0];
my $client = $request->client();
$log->msg("MediaMonkey_volumeCommand running.\n");
my $rc = &$originalVolumeCommand(@args);
$volumes{ $client } = $client->volume();
$client->mixerDisplay('volume');
return $rc;
}
# our 'sync' wrapper function
sub MediaMonkey_syncCommand
{
my @args = @_;
my $request = $args[0];
my $indexid = $request->getParam('_indexid-');
$log->msg("MediaMonkey_syncCommand running:\n");
$log->msg(" _indexid: " . $indexid . "\n");
my $rc = &$originalSyncCommand(@args);
return $rc;
}
# our 'power' wrapper function
sub MediaMonkey_powerCommand
{
my @args = @_;
my $request = $args[0];
my $newvalue = $request->getParam('_newvalue');
$log->msg("MediaMonkey_powerCommand running:\n");
$log->msg(" _newvalue: " . $newvalue . "\n");
if( $pluginEnabled )
{
if( $newvalue == 1 )
{
if( 0 && $ignoreNextPowerOnCommand )
{
$ignoreNextPowerOnCommand = 0;
return;
}
}
else
{
my $client = $request->client();
if( $client->volume() == 0 && exists( $volumes{ $client } ) )
{
$client->volume( $volumes{ $client } );
}
}
}
$ignoreNextPowerOnCommand = 0;
my $rc = &$originalPowerCommand(@args);
if( $pluginEnabled && $newvalue == 1 && $playback )
{
connectToMM();
if( $mm_plyr->{'isPaused'} == 1 )
{
$mm_plyr->Pause();
}
}
return $rc;
}
# our 'playlistPlay' wrapper function
sub MediaMonkey_playlistPlayCommand
{
my @args = @_;
my $request = $args[0];
my $item = $request->getParam('_item');
my $escape = uri_escape(uri_unescape($item));
$escape =~ s/%3A/:/g;
$escape =~ s/%2F/\//g;
my $title = $request->getParam('_title');
$log->msg("MediaMonkey_playlistPlayCommand running.\n");
$log->msg(" item: " . $item . "\n");
$log->msg(" escape: " . $escape . "\n");
$log->msg(" title: " . $title . "\n");
$request->deleteParam('_item');
$request->addParam('_item', $escape );
my $rc = &$originalPlaylistPlayCommand(@args);
return $rc;
}
1;
[/code]
[b]Notes:[/b]
[list]
[*]A [i]lot[/i] of trial and error went into tweaking this. I tried other, completely different approaches. In the end, this is what I came up with.[/*]
[*]You can look up other example plugins online to see what other files you need to make a functional plugin, where to put the files, etc. If people are interested, I can post a .zip file or something.[/*]
[*]This doesn't connect to MediaMonkey until it needs to, so you can start up SqueezeCenter and then launch MediaMonkey (in that order), and the applications will talk to each other.[/*]
[*]Notice that the 'playlist play' handler re-escapes the filename to handle the '#' problem. It first runs an unescape, in case the filename came from SqueezeCenter itself (in which case it is already escaped). It also puts back ':' colons and '/' slashes - I don't think the main 'playlist play' handler likes those to be escaped.[/*]
[/list]
[b]My Setup:[/b]
[list]
[*]I have a "server" box (old headless PC) running in a corner of my living room. It runs MediaMonkey, SqueezeCenter, and the "master" SqueezeBox client. It is connected via cable to my stereo receiver nearby.[/*]
[*]When the server machine starts up, it starts SqueezeCenter, gives it time to get started, then starts the "master" client, waits a little more time, then starts up MediaMonkey. I'm tweaking this to try and make it work 100% of the time without taking too long to get going.[/*]
[*]Other PCs in the house can run SqueezeBox clients, and they are set to synchronize with the "master" client. Since the master is always running, you instantly hook into the stream when you connect a client.[/*]
[*]MediaMonkey volume is set to 0. I use the output from the "master" client for my stereo, that way it is totally in sync with the other clients in the house, in case you can hear two at once.[/*]
[/list]