package Slim::Control::Jive;

# Squeezebox Server Copyright 2001-2009 Logitech
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License, 
# version 2.

use strict;

use POSIX qw(strftime);
use Scalar::Util qw(blessed);
use URI;

use Slim::Utils::Log;
use Slim::Utils::Prefs;
use Slim::Player::Playlist;
use Slim::Player::Client;
#use Data::Dump;

my $prefs   = preferences("server");

=head1 NAME

Slim::Control::Jive

=head1 SYNOPSIS

CLI commands used by Jive.

=cut

my $log = logger('player.jive');

# additional top level menus registered by plugins
my @appMenus           = (); # all available apps
my @pluginMenus        = (); # all non-app plugins
my @recentSearches     = ();

=head1 METHODS

=head2 init()

=cut
sub init {
	my $class = shift;

	# register our functions
	
       #        |requires Client (2 == set disconnected client to clientid if client does not exist)
       #        |  |is a Query
       #        |  |  |has Tags
       #        |  |  |  |Function to call
       #        C  Q  T  F

	Slim::Control::Request::addDispatch(['menu', '_index', '_quantity'], 
		[2, 1, 1, \&menuQuery]);

	Slim::Control::Request::addDispatch(['alarmsettings', '_index', '_quantity'], 
		[1, 1, 1, \&alarmSettingsQuery]);

	Slim::Control::Request::addDispatch(['jiveupdatealarm', '_index', '_quantity'], 
		[1, 1, 1, \&alarmUpdateMenu]);

	Slim::Control::Request::addDispatch(['jiveupdatealarmdays', '_index', '_quantity'], 
		[1, 1, 1, \&alarmUpdateDays]);

	Slim::Control::Request::addDispatch(['syncsettings', '_index', '_quantity'],
		[1, 1, 1, \&syncSettingsQuery]);

	Slim::Control::Request::addDispatch(['sleepsettings', '_index', '_quantity'],
		[1, 1, 1, \&sleepSettingsQuery]);

	Slim::Control::Request::addDispatch(['jivetonesettings', '_index', '_quantity'],
		[1, 1, 1, \&toneSettingsQuery]);

	Slim::Control::Request::addDispatch(['jivestereoxl', '_index', '_quantity'],
		[1, 1, 1, \&stereoXLQuery]);

	Slim::Control::Request::addDispatch(['jivelineout', '_index', '_quantity'],
		[1, 1, 1, \&lineOutQuery]);

	Slim::Control::Request::addDispatch(['crossfadesettings', '_index', '_quantity'],
		[1, 1, 1, \&crossfadeSettingsQuery]);

	Slim::Control::Request::addDispatch(['replaygainsettings', '_index', '_quantity'],
		[1, 1, 1, \&replaygainSettingsQuery]);

	Slim::Control::Request::addDispatch(['jivedummycommand', '_index', '_quantity'],
		[1, 1, 1, \&jiveDummyCommand]);

	Slim::Control::Request::addDispatch(['jivealarm'],
		[1, 0, 1, \&jiveAlarmCommand]);

	Slim::Control::Request::addDispatch(['jiveendoftracksleep', '_index', '_quantity' ],
		[1, 1, 1, \&endOfTrackSleepCommand]);

	Slim::Control::Request::addDispatch(['jivefavorites', '_cmd' ],
		[1, 0, 1, \&jiveFavoritesCommand]);

	Slim::Control::Request::addDispatch(['jivealarmvolume'],
		[1, 0, 1, \&jiveAlarmVolumeSlider ]);

	Slim::Control::Request::addDispatch(['jiveplayerbrightnesssettings', '_index', '_quantity'],
		[1, 1, 0, \&playerBrightnessMenu]);

	Slim::Control::Request::addDispatch(['jiveplayertextsettings', '_whatFont', '_index', '_quantity'],
		[1, 1, 0, \&playerTextMenu]);

	Slim::Control::Request::addDispatch(['jiveunmixable'],
		[1, 1, 1, \&jiveUnmixableMessage]);

	Slim::Control::Request::addDispatch(['jivealbumsortsettings'],
		[1, 0, 1, \&albumSortSettingsMenu]);

	Slim::Control::Request::addDispatch(['jivesetalbumsort'],
		[1, 0, 1, \&jiveSetAlbumSort]);

	Slim::Control::Request::addDispatch(['jivesync' ],
		[1, 0, 1, \&jiveSyncCommand]);

	Slim::Control::Request::addDispatch(['jiveplaylists', '_cmd' ],
		[1, 0, 1, \&jivePlaylistsCommand]);

	Slim::Control::Request::addDispatch(['jiverecentsearches'],
		[0, 1, 0, \&jiveRecentSearchQuery]);

	Slim::Control::Request::addDispatch(['jiveplaytrackalbum'],
		[1, 0, 1, \&jivePlayTrackAlbumCommand]);

	Slim::Control::Request::addDispatch(['jiveplaytrackplaylist'],
		[1, 0, 1, \&jivePlayTrackPlaylistCommand]);

	Slim::Control::Request::addDispatch(['date'],
		[0, 1, 1, \&dateQuery]);

	Slim::Control::Request::addDispatch(['firmwareupgrade'],
		[0, 1, 1, \&firmwareUpgradeQuery]);

	Slim::Control::Request::addDispatch(['jiveapplets'],
		[0, 1, 1, \&extensionsQuery]);

	Slim::Control::Request::addDispatch(['jivewallpapers'],
		[0, 1, 1, \&extensionsQuery]);

	Slim::Control::Request::addDispatch(['jivesounds'],
		[0, 1, 1, \&extensionsQuery]);

	Slim::Control::Request::addDispatch(['jivepatches'],
		[0, 1, 1, \&extensionsQuery]);
	
	# setup the menustatus dispatch and subscription
	Slim::Control::Request::addDispatch( ['menustatus', '_data', '_action'],
		[0, 0, 0, sub { warn "menustatus query\n" }]);
	
	if ( $log->is_info ) {
		Slim::Control::Request::subscribe( \&menuNotification, [['menustatus']] );
	}
	
	# setup a cli command for jive that returns nothing; can be useful in some situations
	Slim::Control::Request::addDispatch( ['jiveblankcommand'],
		[0, 0, 0, sub { return 1; }]);

	if ( !main::SLIM_SERVICE ) {
		# Load memory caches to help with menu performance
		buildCaches();
		
		# Re-build the caches after a rescan
		Slim::Control::Request::subscribe( \&buildCaches, [['rescan', 'done']] );
	}
	
	Slim::Control::Request::subscribe(\&_libraryChanged, [['library'], ['changed']]);
}

sub _libraryChanged {
	foreach ( Slim::Player::Client::clients() ) {
		myMusicMenu(0, $_);
	}
}

sub buildCaches {
	main::DEBUGLOG && $log->debug("Begin function");
	
	return if !Slim::Schema::hasLibrary();

	my $sort    = $prefs->get('jivealbumsort') || 'album';

	for my $partymode ( 0..1 ) {
		# Pre-cache albums query
		if ( my $numAlbums = Slim::Schema->rs('Album')->count ) {
			main::DEBUGLOG && $log->debug( "Pre-caching $numAlbums album items for partymode:$partymode" );
			Slim::Control::Request::executeRequest( undef, [ 'albums', 0, $numAlbums, "useContextMenu:1", "sort:$sort", 'menu:track', 'cache:1', "party:$partymode" ] );
		}
		
		main::idleStreams();
		
		# Artists
		if ( my $numArtists = Slim::Schema->rs('Contributor')->browse->search( {}, { distinct => 'me.id' } )->count ) {
			# Add one since we may have a VA item
			$numArtists++;
			main::DEBUGLOG && $log->debug( "Pre-caching $numArtists artist items for partymode:$partymode." );
			Slim::Control::Request::executeRequest( undef, [ 'artists', 0, $numArtists, "useContextMenu:1", 'menu:album', 'cache:1', "party:$partymode" ] );
		}
		
		main::idleStreams();
		
		# Genres
		if ( my $numGenres = Slim::Schema->rs('Genre')->browse->search( {}, { distinct => 'me.id' } )->count ) {
			main::DEBUGLOG && $log->debug( "Pre-caching $numGenres genre items for partymode:$partymode." );
			Slim::Control::Request::executeRequest( undef, [ 'genres', 0, $numGenres, "useContextMenu:1", 'menu:artist', 'cache:1', "party:$partymode" ] );
		}
		
		main::idleStreams();
	}
}

=head2 getDisplayName()

Returns name of module

=cut
sub getDisplayName {
	return 'JIVE';
}

######
# CLI QUERIES

# handles the "menu" query
sub menuQuery {

	my $request = shift;

	main::INFOLOG && $log->info("Begin menuQuery function");

	if ($request->isNotQuery([['menu']])) {
		$request->setStatusBadDispatch();
		return;
	}

	my $client = $request->client() || 0;
	my $disconnected;
	
	if ( !$client ) {
		require Slim::Player::Disconnected;
		
		# Check if this is a disconnected player request
		if ( my $id = $request->disconnectedClientID ) {
			# On SN, if the player does not exist in the database this is a fatal error
			if ( main::SLIM_SERVICE ) {
				my ($player) = SDI::Service::Model::Player->search( { mac => $id } );
				if ( !$player ) {
					main::INFOLOG && $log->is_info && $log->info("Player $id does not exist in SN database");
					$request->setStatusBadDispatch();
					return;
				}
			}
			
			$client = Slim::Player::Disconnected->new($id);
			$disconnected = 1;
			
			main::INFOLOG && $log->is_info && $log->info("Player $id not connected, using disconnected menu mode");
		}
		else {
			# XXX temporary workaround for requests without a playerid
			$client = Slim::Player::Disconnected->new( '_dummy_' . Time::HiRes::time() );
			$disconnected = 1;
			
			$log->error("Menu requests without a client are deprecated, using disconnected menu mode");
		}
	}
	
	my $direct = ( $disconnected || $request->getParam('direct') ) ? 1 : 0;

	# send main menu notification
	my $menu = mainMenu($client, $direct);
	
	# Return results directly and destroy the client if it is disconnected
	# Also return the results directly if param 'direct' is set
	if ( $direct ) {
		$log->is_info && $log->info('Sending direct menu response');
		
		$request->setRawResults( {
			count     => scalar @{$menu},
			offset    => 0,
			item_loop => $menu,
		} );
		
		if ( $disconnected ) {
			$client->forgetClient;
		}
	}
	
	$request->setStatusDone();
}

sub mainMenu {

	main::INFOLOG && $log->info("Begin function");
	my $client = shift;
	my $direct = shift;
	
	unless ($client && $client->isa('Slim::Player::Client')) {
		# if this isn't a player, no menus should get sent
		return;
	}
 
	# as a convention, make weights => 10 and <= 100; Jive items that want to be below all SS items
	# then just need to have a weight > 100, above SS items < 10

	# for the notification menus, we're going to send everything over "flat"
	# as a result, no item_loops, all submenus (setting, myMusic) are just elements of the big array that get sent

	my @menu = map {
		_localizeMenuItemText( $client, $_ );
	}(
		main::SLIM_SERVICE ? () : {
			stringToken    => 'MY_MUSIC',
			weight         => 11,
			id             => 'myMusic',
			isANode        => 1,
			node           => 'home',
		},
		{
			stringToken    => 'FAVORITES',
			id             => 'favorites',
			node           => 'home',
			weight         => 100,
			actions => {
				go => {
					cmd => ['favorites', 'items'],
					params => {
						menu     => 'favorites',
					},
				},
			},
		},

		@pluginMenus,
		@{playerPower($client, 1)},
		@{playerSettingsMenu($client, 1)},
		@{
			# The Digital Input plugin could be disabled
			if( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::DigitalInput::Plugin')) {
				Slim::Plugin::DigitalInput::Plugin::digitalInputItem($client);
			} else {
				[];
			}
		},
		@{
			# The Line In plugin could be disabled
			if( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::LineIn::Plugin')) {
				Slim::Plugin::LineIn::Plugin::lineInItem($client, 0);
			} else {
				[];
			}
		},
		@{
			# The Audioscrobbler plugin could be disabled
			if( Slim::Utils::PluginManager->isEnabled('Slim::Plugin::AudioScrobbler::Plugin')) {
				Slim::Plugin::AudioScrobbler::Plugin::jiveSettings($client);
			} else {
				[];
			}
		},
		@{internetRadioMenu($client)},
		main::SLIM_SERVICE ? () : @{albumSortSettingsItem($client, 1)},
		main::SLIM_SERVICE ? () : @{myMusicMenu(1, $client)},
		main::SLIM_SERVICE ? () : @{recentSearchMenu($client, 1)},
		@{appMenus($client, 1)},

		@{globalSearchMenu($client)},		
	);

	if ( !$direct ) {
		_notifyJive(\@menu, $client);
	}
	
	return \@menu;
}

sub jiveSetAlbumSort {
	my $request = shift;
	my $client  = $request->client;
	my $sort = $request->getParam('sortMe');
	$prefs->set('jivealbumsort', $sort);
	# resend the myMusic menus with the new sort pref set
	myMusicMenu(0, $client);
	$request->setStatusDone();
}

sub playlistModeSettings {
	main::INFOLOG && $log->info("Begin function");

	my $client       = shift;
	my $batch        = shift;
	my $playlistmode = Slim::Player::Playlist::playlistMode($client);

	my @menu = ();

	my @modeStrings = ('DISABLED', 'OFF', 'ON', 'PARTY');
	my @translatedModeStrings = map { ucfirst($client->string($_)) } @modeStrings;
	my %modes = (
		disabled => 1,
		off      => 2,
		on       => 3,
		party    => 4,
	);

	#todo: move node to "betaFeatures",  left as advancedSettings for backwards compat 
	my $choice = {
		text          => $client->string('PLAYLIST_MODE'),
		choiceStrings => [ @translatedModeStrings ] ,
		selectedIndex => $modes{$playlistmode},
		id            => 'settingsPlaylistMode',
		node          => 'advancedSettings',
		weight        => 100,
		actions       => {
			do => { 
				choices => [ 
					{
						player => 0,
						cmd    => [ 'playlistmode', 'set', 'disabled' ],
					},
					{
						player => 0,
						cmd    => [ 'playlistmode', 'set', 'off' ],
					},
					{
						player => 0,
						cmd    => [ 'playlistmode', 'set', 'on' ],
					},
					{
						player => 0,
						cmd    => [ 'playlistmode', 'set', 'party' ],
					},
				], 
			},
		},
	};

	if ($batch) {
		return $choice;
	} else {
		_notifyJive( [ $choice ], $client);
	}

}



sub albumSortSettingsMenu {
	main::INFOLOG && $log->info("Begin function");
	my $request = shift;
	my $client = $request->client;
	my $sort    = $prefs->get('jivealbumsort');
	my %sortMethods = (
		artistalbum =>	'SORT_ARTISTALBUM',
		artflow =>	'SORT_ARTISTYEARALBUM',
		album =>	'ALBUM',
	);
	$request->addResult('count', scalar(keys %sortMethods));
	$request->addResult('offset', 0);
	my $i = 0;
	for my $key (sort keys %sortMethods) {
		$request->addResultLoop('item_loop', $i, 'text', $client->string($sortMethods{$key}));
		my $selected = ($sort eq $key) + 0;
		$request->addResultLoop('item_loop', $i, 'radio', $selected);
		my $actions = {
			do => {
				player => 0,
				cmd    => ['jivesetalbumsort'],
				params => {
					'sortMe' => $key,
				},
			},
		};
		$request->addResultLoop('item_loop', $i, 'actions', $actions);
		$i++;
	}
	
}

sub albumSortSettingsItem {
	main::INFOLOG && $log->info("Begin function");
	my $client = shift;
	my $batch = shift;

	my @menu = ();
	push @menu,
	{
		text           => $client->string('ALBUMS_SORT_METHOD'),
		id             => 'settingsAlbumSettings',
		node           => 'advancedSettings',
		weight         => 105,
			actions        => {
			go => {
				cmd => ['jivealbumsortsettings'],
				params => {
					menu => 'radio',
				},
			},
		},
	};

	if ($batch) {
		return \@menu;
	} else {
		_notifyJive(\@menu, $client);
	}

}

# allow a plugin to add a node to the menu
# XXX what uses this?
sub registerPluginNode {
	main::INFOLOG && $log->info("Begin function");
	my $nodeRef = shift;
	my $client = shift || undef;
	unless (ref($nodeRef) eq 'HASH') {
		$log->error("Incorrect data type");
		return;
	}

	$nodeRef->{'isANode'} = 1;
	main::INFOLOG && $log->info("Registering node menu item from plugin");

	# notify this menu to be added
	my $id = _clientId($client);
	Slim::Control::Request::notifyFromArray( $client, [ 'menustatus', $nodeRef, 'add', $id ] );

	# but also remember this structure as part of the plugin menus
	push @pluginMenus, $nodeRef;

}

sub registerAppMenu {
	my $menuArray = shift;
	
	# now we want all of the items in $menuArray to go into @pluginMenus, but we also
	# don't want duplicate items (specified by 'id'), 
	# so we want the ids from $menuArray to stomp on ids from @pluginMenus, 
	# thus getting the "newest" ids into the @pluginMenus array of items
	# we also do not allow any hash without an id into the array, and will log an error if that happens

	my $isInfo = $log->is_info;

	my %seen;
	my @new;

	for my $href (@$menuArray, reverse @appMenus) {
		my $id = $href->{id};
		if ($id) {
			if ( !$seen{$id} ) {
				main::INFOLOG && $isInfo && $log->info("registering app menu " . $id);
				push @new, $href;
			}
			$seen{$id}++;
		}
		else {
			$log->error("Menu items cannot be added without an id");
		}
	}

	# @new is the new @appMenus
	# we do this in reverse so we get previously initialized nodes first 
	# you can't add an item to a node that doesn't exist :)
	@appMenus = reverse @new;
}

# send plugin menus array as a notification to Jive
sub refreshPluginMenus {
	main::INFOLOG && $log->info("Begin function");
	my $client = shift || undef;
	_notifyJive(\@pluginMenus, $client);
}

#allow a plugin to add an array of menu entries
sub registerPluginMenu {
	my $menuArray = shift;
	my $node = shift;
	my $client = shift || undef;

	unless (ref($menuArray) eq 'ARRAY') {
		$log->error("Incorrect data type");
		return;
	}
	
	my $isInfo = $log->is_info;

	if ($node) {
		my @menuArray = @$menuArray;
		for my $i (0..$#menuArray) {
			if (!$menuArray->[$i]{'node'}) {
				$menuArray->[$i]{'node'} = $node;
			}
		}
	}

	main::INFOLOG && $isInfo && $log->info("Registering menus from plugin");

	if ( $client ) {
		# notify this menu to be added
		_notifyJive($menuArray, $client);
	}

	# now we want all of the items in $menuArray to go into @pluginMenus, but we also
	# don't want duplicate items (specified by 'id'), 
	# so we want the ids from $menuArray to stomp on ids from @pluginMenus, 
	# thus getting the "newest" ids into the @pluginMenus array of items
	# we also do not allow any hash without an id into the array, and will log an error if that happens

	my %seen; my @new;

	for my $href (@$menuArray, reverse @pluginMenus) {
		my $id = $href->{'id'};
		my $node = $href->{'node'};
		if ($id) {
			if (!$seen{$id}) {
				main::INFOLOG && $isInfo && $log->info("registering menuitem " . $id . " to " . $node );
				push @new, $href;
			}
			$seen{$id}++;
		} else {
			$log->error("Menu items cannot be added without an id");
		}
	}

	# @new is the new @pluginMenus
	# we do this in reverse so we get previously initialized nodes first 
	# you can't add an item to a node that doesn't exist :)
	@pluginMenus = reverse @new;

}

# allow a plugin to delete an item from the Jive menu based on the id of the menu item
# if the item is a node, then delete its immediate children too
sub deleteMenuItem {
	my $menuId = shift;
	my $client = shift || undef;
	return unless $menuId;
	main::INFOLOG && $log->is_warn && $log->warn($menuId . " menu id slated for deletion");
	
	# send a notification to delete
	# but also remember that this id is not to be sent
	my @menuDelete;
	my @new;
	for my $href (reverse @pluginMenus) {
		next if !$href;
		if (($href->{'id'} && $href->{'id'} eq $menuId)
			|| ($href->{'node'} && $href->{'node'} eq $menuId))
		{
			main::INFOLOG && $log->info("deregistering menuitem ",  $href->{'id'});
			push @menuDelete, $href;
		} else {
			push @new, $href;
		}
	}
	
	push @menuDelete, { id => $menuId };

	_notifyJive(\@menuDelete, $client, 'remove');

	@pluginMenus = reverse @new;
}

# delete all menus items listed in @pluginMenus and @appMenus
# This used to do menu refreshes when apps may have been removed
sub deleteAllMenuItems {
	my $client = shift || return;
	
	my @menuDelete;
	
	for my $menu ( @pluginMenus, @appMenus ) {
		push @menuDelete, { id => $menu->{id} };
	}
	
	main::INFOLOG && $log->is_info && $log->info( $client->id . ' removing menu items: ' . Data::Dump::dump(\@menuDelete) );
	
	_notifyJive( \@menuDelete, $client, 'remove' );
}

sub _purgeMenu {
	my $menu = shift;
	my @menu = @$menu;
	my @purgedMenu = ();
	for my $i (0..$#menu) {
		last unless (defined($menu[$i]));
		push @purgedMenu, $menu[$i];
	}
	return \@purgedMenu;
}


sub alarmSettingsQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request = shift;
	my $client  = $request->client();

	my @menu = ();
	return \@menu unless $client;

	# All Alarms On/Off

	my $val = $prefs->client($client)->get('alarmsEnabled');

	my @alarmStrings = ('OFF', 'ON');
	my @translatedAlarmStrings = map { ucfirst($client->string($_)) } @alarmStrings;

	my $onOff = {
		text           => $client->string("ALARM_ALL_ALARMS"),
		choiceStrings  => [ @translatedAlarmStrings ],
		selectedIndex  => $val + 1, # 1 is added to make it count like Lua
		actions        => {
			do => { 
				choices => [ 
					{
						player => 0,
						cmd    => [ 'alarm', 'disableall' ],
					},
					{
						player => 0,
						cmd    => [ 'alarm', 'enableall' ],
					},
				], 
			},
		},
	};
	push @menu, $onOff;

	my $setAlarms = getCurrentAlarms($client);

	if ( scalar(@$setAlarms) ) {
		push @menu, @$setAlarms;
	}

	my $addAlarm = {
		text           => $client->string("ALARM_ADD"),
		input   => {
			initialText  => 25200, # default is 7:00
			title => $client->string('ALARM_ADD'),
			_inputStyle  => 'time',
			len          => 1,
			help         => {
				text => $client->string('JIVE_ALARMSET_HELP')
			},
		},
		actions => {
			do => {
				player => 0,
				cmd    => [ 'alarm', 'add' ],
				params => {
					time => '__TAGGEDINPUT__',	
					enabled => 1,
				},
			},
		},
		nextWindow => 'refresh',
	};
	push @menu, $addAlarm;

	# Bug 9226: don't offer alarm volume setting if player is set for fixed volume
	my $digitalVolumeControl = $prefs->client($client)->get('digitalVolumeControl');
	if ( ! ( defined $digitalVolumeControl && $digitalVolumeControl == 0 ) ) {
		my $defaultVolLevel = Slim::Utils::Alarm->defaultVolume($client);
		my $defaultVolumeLevels = alarmVolumeSettings($defaultVolLevel, undef, $client->string('ALARM_VOLUME'));
		push @menu, $defaultVolumeLevels;
	}

	my $fadeEnabled = $prefs->client($client)->get('alarmfadeseconds');
	if (!defined ($fadeEnabled) || $fadeEnabled == 0) {
		$fadeEnabled = 0;
	} else {
		$fadeEnabled = 1;
	}

	my $fadeInAlarm = {
		text           => $client->string("ALARM_FADE"),
		checkbox => ($fadeEnabled == 1) + 0,
		actions  => {
			on  => {
				player => 0,
				cmd    => [ 'jivealarm' ],
				params => {
					fadein => 1,
				},
			},
			off => {
				player => 0,
				cmd    => [ 'jivealarm' ],
				params => {
					fadein => 0,
				},
			},
		},		
	};
	push @menu, $fadeInAlarm;

	sliceAndShip($request, $client, \@menu);

}

sub alarmUpdateMenu {

	my $request = shift;
	my $client  = $request->client();
	my $params;

	my @tags = qw( id enabled days time playlist );
	for my $tag (@tags) {
		$params->{$tag} = $request->getParam($tag);
	}

	my $alarm = Slim::Utils::Alarm->getAlarm($client, $params->{id});
	my @menu = ();

	my $enabled = $alarm->enabled();
	my $onOff = {
		text     => $client->string("ALARM_ALARM_ENABLED"),
		checkbox => ($enabled == 1) + 0,
		onClick  => 'refreshOrigin',
		actions  => {
			on  => {
				player => 0,
				cmd    => [ 'alarm', 'update' ],
				params => {
					id      => $params->{id},
					enabled => 1,
				},
			},
			off => {
				player => 0,
				cmd    => [ 'alarm', 'update' ],
				params => {
					id      => $params->{id},
					enabled => 0,
				},
			},
		},		
	};
	push @menu, $onOff;

	my $setTime = {
		text           => $client->string("ALARM_SET_TIME"),
		input   => {
			initialText  => $params->{time}, # this will need to be formatted correctly
			title => $client->string('ALARM_SET_TIME'),
			_inputStyle  => 'time',
			len          => 1,
			help         => {
				text => $client->string('JIVE_ALARMSET_HELP')
			},
		},
		actions => {
			do => {
				player => 0,
				cmd    => [ 'alarm', 'update' ],
				params => {
					id   => $params->{id},
					time => '__TAGGEDINPUT__',	
				},
			},
		},
		nextWindow => 'parent',
	};
	push @menu, $setTime;


	my $setDays = {
		text      => $client->string("ALARM_SET_DAYS"),
		actions   => {
			go => {
				player => 0,
				cmd    => [ 'jiveupdatealarmdays' ],
				params => {
					id => $params->{id},
				},
			},
		},
	};
	push @menu, $setDays;

	my $playlistChoice = {
		text      => $client->string('ALARM_SELECT_PLAYLIST'),
		actions   => {
			go => {
				player => 0,
				cmd    => [ 'alarm', 'playlists' ],
				params => {
					id   => $params->{id},
					menu => 1
				},
			},
		},
	};
	push @menu, $playlistChoice;

	my $repeat = $alarm->repeat();
	my $repeatOn = {
		text     => $client->string("ALARM_ALARM_REPEAT"),
		radio    => ($repeat == 1) + 0,
		onClick  => 'refreshOrigin',
		actions  => {
			do  => {
				player => 0,
				cmd    => [ 'alarm', 'update' ],
				params => {
					id      => $params->{id},
					repeat => 1,
				},
			},
		},		
	};
	push @menu, $repeatOn;

	my $repeatOff = {
		text     => $client->string("ALARM_ALARM_ONETIME"),
		radio    => ($repeat == 0) + 0,
		onClick  => 'refreshOrigin',
		actions  => {
			do => {
				player => 0,
				cmd    => [ 'alarm', 'update' ],
				params => {
					id      => $params->{id},
					repeat => 0,
				},
			},
		},
	};
	push @menu, $repeatOff;

	my @delete_menu= (
		{
			text    => $client->string('CANCEL'),
			actions => {
				go => {
					player => 0,
					cmd    => [ 'jiveblankcommand' ],
				},
			},
			nextWindow => 'parent',
		},
		{
			text    => $client->string('ALARM_DELETE'),
			actions => {
				go => {
					player => 0,
					cmd    => ['alarm', 'delete'],
					params => {
						id => $params->{id},
					},
				},
			},
			nextWindow => 'grandparent',
		},
	);
	my $removeAlarm = {
		text      => $client->string('ALARM_DELETE'),
		count     => scalar(@delete_menu),
		offset    => 0,
		item_loop => \@delete_menu,
	};
	push @menu, $removeAlarm;

	sliceAndShip($request, $client, \@menu);
}

sub alarmUpdateDays {

	my $request = shift;
	my $client  = $request->client;

	my @params  = qw/ id /;
	my $params;
	for my $key (@params) {
		$params->{$key} = $request->getParam($key);
	}
	my $alarm = Slim::Utils::Alarm->getAlarm($client, $params->{id});

	my @days_menu = ();

	for my $day (0..6) {
		my $dayActive = $alarm->day($day);
		my $string = "ALARM_DAY$day";

		my $day = {
			text       => $client->string($string),
			checkbox   => $dayActive + 0,
			onClick    => 'refreshGrandparent',
			actions => {
				on => {
					player => 0,
					cmd    => [ 'alarm', 'update' ],
					params => {
						id => $params->{id},
						dowAdd => $day,
					},
				},
				off => {
					player => 0,
					cmd    => [ 'alarm', 'update' ],
					params => {
						id => $params->{id},
						dowDel => $day,
					},
				},
	
			},
		};
		push @days_menu, $day;
	}

	sliceAndShip($request, $client, \@days_menu);

	$request->setStatusDone();
}

sub getCurrentAlarms {
	
	my $client = shift;
	my @return = ();
	my @alarms = Slim::Utils::Alarm->getAlarms($client);
	#Data::Dump::dump(@alarms);
	my $count = 1;
	for my $alarm (@alarms) {
		my @days;
		for (0..6) {
			push @days, $_ if $alarm->day($_);
		}
		my $name = $client->string('ALARM_ALARM') . " $count: " . $alarm->displayStr;
		my $daysString = join(',', @days);
		my $thisAlarm = {
			text           => $name,
			actions        => {
				go => {
					cmd    => ['jiveupdatealarm'],
					params => {
						id       => $alarm->id,
						enabled  => $alarm->enabled || 0,
						days     => $daysString,
						time     => $alarm->time || 0,
						playlist => $alarm->playlist || 0, # don't pass an undef to jive
					},
					player => 0,
				},
			},
		};
		push @return, $thisAlarm;
		$count++;
	}
	return \@return;
}

sub alarmVolumeSettings {

	my $current_setting = shift || 50;
	my $id              = shift || 0;
	my $string          = shift;

	my $return = { 
		text      => $string,
		actions   => {
			go => {
				player => 0,
				cmd    => [ 'jivealarmvolume' ],
			},
		},
	};
	return $return;
}

sub jiveAlarmVolumeSlider {

	my $request = shift;
	my $client  = $request->client();

	my $current_setting = Slim::Utils::Alarm->defaultVolume($client) || 50;
	my $id              = shift || 0;
	my $string          = shift;

	my @vol_settings;
	my $slider = {
		slider      => 1,
		min         => 1,
		max         => 100,
		sliderIcons => 'volume',
		initial     => $current_setting,
		actions => {
			do => {
				player => 0,
				cmd    => [ 'alarm', 'defaultvolume' ],
				params => {
					valtag => 'volume',
				},
			},
		},
	};

	$request->addResult('offset', 0);
	$request->addResult('count', 1);
	$request->addResult('item_loop', [ $slider ] );
	$request->setStatusDone();

}


sub syncSettingsQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request           = shift;
	my $client            = $request->client();
	my $playersToSyncWith = getPlayersToSyncWith($client);

	my @menu = @$playersToSyncWith;

	sliceAndShip($request, $client, \@menu);

}

sub endOfTrackSleepCommand {

	my $request = shift;
	my $client  = $request->client();

	if ($client->isPlaying()) {

		# calculate the time remaining in seconds 
		my $dur = $client->controller()->playingSongDuration();
		my $remaining = $dur - Slim::Player::Source::songTime($client);
		$client->execute( ['sleep', $remaining ] );
	} else {
		$request->client->showBriefly(
			{ 
			'jive' =>
				{
					'type'    => 'popupplay',
					'text'    => [ $request->string('NOTHING_CURRENTLY_PLAYING') ],
				},
			}
		);
	}
}


sub sleepSettingsQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request = shift;
	my $client  = $request->client();
	my $val     = $client->currentSleepTime();
	my @menu;

	# Bug: 2151 some extra stuff to add the option to sleep after the current song.
	# first make sure we're playing, and its a valid song.
	my $remaining = 0;

	

	if ($val > 0) {
		my $now = Time::HiRes::time();
		my $then = $client->sleepTime();
		my $sleepyTime = int( ($then - $now) / 60 ) + 1;
		my $sleepString = $client->string( 'SLEEPING_IN_X_MINUTES', $sleepyTime );
		push @menu, { text => $sleepString, style => 'itemNoAction' };
		push @menu, sleepInXHash($client, $val, 0);
	}

	#bug 15675 - don't display 'end of song' option for radio streams
	if ($client->isPlaying() && $client->controller()->playingSongDuration()) {

		push @menu, {
			text    => $client->string('SLEEP_AT_END_OF_SONG'),
			actions => {
				go => {
					player => 0,
					cmd => [ 'jiveendoftracksleep' ],
				},
			},
			nextWindow => 'refresh',
			setSelectedIndex => 1,
		};
	}

	push @menu, sleepInXHash($client, $val, 15);
	push @menu, sleepInXHash($client, $val, 30);
	push @menu, sleepInXHash($client, $val, 45);
	push @menu, sleepInXHash($client, $val, 60);
	push @menu, sleepInXHash($client, $val, 90);

	sliceAndShip($request, $client, \@menu);
}

sub stereoXLQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request        = shift;
	my $client         = $request->client();
	my $currentSetting = $client->stereoxl();
	my @strings = qw/ CHOICE_OFF LOW MEDIUM HIGH /;
	my @menu = ();
	for my $i (0..3) {
		my $xlSetting = {
			text    => $client->string($strings[$i]),
			radio   => ($i == $currentSetting) + 0,
			actions => {
				do => {
					player => 0,
					cmd    => [ 'playerpref', 'stereoxl', $i ],
				},
			},
		};
		push @menu, $xlSetting;
	}

	sliceAndShip($request, $client, \@menu);

	$request->setStatusDone();

}

sub lineOutQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request        = shift;
	my $client         = $request->client();
	my $currentSetting = $prefs->client($client)->get('analogOutMode');
	my @strings = qw/ ANALOGOUTMODE_HEADPHONE ANALOGOUTMODE_SUBOUT ANALOGOUTMODE_ALWAYS_ON ANALOGOUTMODE_ALWAYS_OFF /;
	my @menu = ();
	for my $i (0..3) {
		my $lineOutSetting = {
			text    => $client->string($strings[$i]),
			radio   => ($i == $currentSetting) + 0,
			actions => {
				do => {
					player => 0,
					cmd    => [ 'playerpref', 'analogOutMode', $i ],
				},
			},
		};
		push @menu, $lineOutSetting;
	}

	sliceAndShip($request, $client, \@menu);

	$request->setStatusDone();

}

sub toneSettingsQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request        = shift;
	my $client         = $request->client();
	my $tone           = $request->getParam('cmd');

	my $val     = $client->$tone();

	my @menu = ();
	my $slider = {
		slider  => 1,
		min     => -23 + 0,
		max     => 23,
		adjust  => 24, # slider currently doesn't like a slider starting at or below 0
		initial => $val,
		#help    => NO_HELP_STRING_YET,
		actions => {
			do => {
				player => 0,
				cmd    => [ 'playerpref', $tone ],
				params => {
					valtag => 'value',
				},
			},
		},
	};

	push @menu, $slider;

	sliceAndShip($request, $client, \@menu);

	$request->setStatusDone();
}

sub crossfadeSettingsQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request = shift;
	my $client  = $request->client();
	my $val     = $prefs->client($client)->get('transitionType');
	my @strings = (
		'TRANSITION_NONE', 'TRANSITION_CROSSFADE', 
		'TRANSITION_FADE_IN', 'TRANSITION_FADE_OUT', 
		'TRANSITION_FADE_IN_OUT'
	);
	my @menu;

	push @menu, transitionHash($client, $val, $prefs, \@strings, 0);
	push @menu, transitionHash($client, $val, $prefs, \@strings, 1);
	push @menu, transitionHash($client, $val, $prefs, \@strings, 2);
	push @menu, transitionHash($client, $val, $prefs, \@strings, 3);
	push @menu, transitionHash($client, $val, $prefs, \@strings, 4);

	sliceAndShip($request, $client, \@menu);

}

sub replaygainSettingsQuery {

	main::INFOLOG && $log->info("Begin function");
	my $request = shift;
	my $client  = $request->client();
	my $val     = $prefs->client($client)->get('replayGainMode');
	my @strings = (
		'REPLAYGAIN_DISABLED', 'REPLAYGAIN_TRACK_GAIN', 
		'REPLAYGAIN_ALBUM_GAIN', 'REPLAYGAIN_SMART_GAIN'
	);
	my @menu;

	push @menu, replayGainHash($client, $val, $prefs, \@strings, 0);
	push @menu, replayGainHash($client, $val, $prefs, \@strings, 1);
	push @menu, replayGainHash($client, $val, $prefs, \@strings, 2);
	push @menu, replayGainHash($client, $val, $prefs, \@strings, 3);

	sliceAndShip($request, $client, \@menu);
}


sub sliceAndShip {
	my ($request, $client, $menu) = @_;
	my $numitems = scalar(@$menu);
	my $index    = $request->getParam('_index');
	my $quantity = $request->getParam('_quantity');

	$request->addResult("count", $numitems);
	my ($valid, $start, $end) = $request->normalize(scalar($index), scalar($quantity), $numitems);

	if ($valid) {
		my $cnt = 0;
		$request->addResult('offset', $start);

		for my $eachmenu (@$menu[$start..$end]) {
			$request->setResultLoopHash('item_loop', $cnt, $eachmenu);
			$cnt++;
		}
	}
	$request->setStatusDone()
}

# returns a single item for the homeMenu if radios is a valid command
sub internetRadioMenu {
	main::INFOLOG && $log->info("Begin function");
	my $client = shift;
	my @command = ('radios', 0, 200, 'menu:radio');

	my $test_request = Slim::Control::Request::executeRequest($client, \@command);
	my $validQuery = $test_request->isValidQuery();

	my @menu = ();
	
	if ($validQuery && $test_request->getResult('count')) {
		push @menu,
		{
			text           => $client->string('RADIO'),
			id             => 'radios',
			node           => 'home',
			weight         => 20,
			actions        => {
				go => {
					cmd => ['radios'],
					params => {
						menu => 'radio',
					},
				},
			},
			window        => {
					menuStyle => 'album',
			},
		};
	}

	return \@menu;

}

sub playerSettingsMenu {

	main::INFOLOG && $log->info("Begin function");
	my $client = shift;
	my $batch = shift;

	my @menu = ();
	return \@menu unless $client;

 	push @menu, {
		text           => $client->string('AUDIO_SETTINGS'),
		id             => 'settingsAudio',
		node           => 'settings',
		isANode        => 1,
		weight         => 35,
	};
	
	# always add repeat
	push @menu, repeatSettings($client, 1);

	# always add shuffle
	push @menu, shuffleSettings($client, 1);

	# always add playlist mode
	# Bugs: 13896, 13689, 8878
	# playlist/party mode is in conflict with 7.4 touch/press-to-play behavior
	# fix for bug 13689 will be the complete fix, but for now remove playlistModeSettings from SP menus entirely
	# push @menu, playlistModeSettings($client, 1);

	# add alarm only if this is a slimproto player
	if ($client->isPlayer()) {
		push @menu, {
			text           => $client->string("ALARM"),
			id             => 'settingsAlarm',
			node           => 'settings',
			weight         => 29,
			actions        => {
				go => {
					cmd    => ['alarmsettings'],
					player => 0,
				},
			},
		};
	}

	# bass, if available
	if ( $client->maxBass() - $client->minBass() > 0 ) {
		push @menu, {
			text           => $client->string("BASS"),
			id             => 'settingsBass',
			node           => 'settingsAudio',
			weight         => 10,
			actions        => {
				go => {
					cmd    => ['jivetonesettings'],
					player => 0,
					params => {
						'cmd' => 'bass',
					},
				},
			},
		};
	}

	# treble, if available
	if ( $client->maxTreble() - $client->minTreble() > 0 ) {
		push @menu, {
			text           => $client->string("TREBLE"),
			id             => 'settingsTreble',
			node           => 'settingsAudio',
			weight         => 20,
			actions        => {
				go => {
					cmd    => ['jivetonesettings'],
					player => 0,
					params => {
						'cmd' => 'treble',
					},
				},
			},
		};
	}

	# stereoXL, if available
	if ( $client->can('maxXL') ) {
		push @menu, {
			text           => $client->string("STEREOXL"),
			id             => 'settingsStereoXL',
			node           => 'settingsAudio',
			weight         => 90,
			actions        => {
				go => {
					cmd    => ['jivestereoxl'],
					player => 0,
				},
			},
		};
	}

	# lineOut, if available
	if ( $client->hasHeadSubOut() ) {
		push @menu, {
			text           => $client->string("SETUP_ANALOGOUTMODE"),
			id             => 'settingsLineOut',
			node           => 'settingsAudio',
			weight         => 80,
			actions        => {
				go => {
					cmd    => ['jivelineout'],
					player => 0,
				},
			},
		};
	}


	# sleep setting (always)
	push @menu, {
		text           => $client->string("SLEEP"),
		id             => 'settingsSleep',
		node           => 'settings',
		weight         => 65,
		actions        => {
			go => {
				cmd    => ['sleepsettings'],
				player => 0,
			},
		},
	};	

	# synchronization. only if numberOfPlayers > 1
	my $synchablePlayers = howManyPlayersToSyncWith($client);
	if ($synchablePlayers > 0) {
		push @menu, {
			text           => $client->string("SYNCHRONIZE"),
			id             => 'settingsSync',
			node           => 'settings',
			weight         => 70,
			actions        => {
				go => {
					cmd    => ['syncsettings'],
					player => 0,
				},
			},
		};	
	}

	# information, always display
	push @menu, {
		text           => $client->string( 'JIVE_SQUEEZEBOX_INFORMATION' ),
		id             => 'settingsInformation',
		node           => 'advancedSettings',
		weight         => 100,
		actions        => {
				go =>	{
						cmd    => ['systeminfo', 'items'],
						params => {
							menu => 1
						}
					},
				},
	};

	# player name change, always display
	push @menu, {
		text           => $client->string('INFORMATION_PLAYER_NAME'),
		id             => 'settingsPlayerNameChange',
		node           => 'settings',
		weight         => 67,
		input          => {
			initialText  => $client->name(),
			len          => 1, # For those that want to name their player "X"
			allowedChars => $client->string('JIVE_ALLOWEDCHARS_WITHCAPS'),
			help         => {
				           text => $client->string('JIVE_CHANGEPLAYERNAME_HELP')
			},
			softbutton1  => $client->string('INSERT'),
			softbutton2  => $client->string('DELETE'),
		},
                actions        => {
                                do =>   {
                                                cmd    => ['name'],
                                                player => 0,
						params => {
							playername => '__INPUT__',
						},
                                        },
                                  },
	};


	# transition only for Sb2 and beyond (aka 'crossfade')
	if ($client->isa('Slim::Player::Squeezebox2')) {
		push @menu, {
			text           => $client->string("SETUP_TRANSITIONTYPE"),
			id             => 'settingsXfade',
			node           => 'settingsAudio',
			weight         => 30,
			actions        => {
				go => {
					cmd    => ['crossfadesettings'],
					player => 0,
				},
			},
		};	
	}

	# replay gain (aka volume adjustment)
	if ($client->canDoReplayGain(0)) {
		push @menu, {
			text           => $client->string("REPLAYGAIN"),
			id             => 'settingsReplayGain',
			node           => 'settingsAudio',
			weight         => 40,
			actions        => {
				  go => {
					cmd    => ['replaygainsettings'],
					player => 0,
				  },
			},
		};	
	}

	# brightness settings for players with displays 
	if ( $client->isPlayer() && !$client->display->isa('Slim::Display::NoDisplay') ) {
		push @menu, 
		{
			stringToken    => 'JIVE_PLAYER_DISPLAY_SETTINGS',
			id             => 'squeezeboxDisplaySettings',
			isANode        => 1,
			node           => 'advancedSettings',
		},
		{
			text           => $client->string("PLAYER_BRIGHTNESS"),
			id             => 'settingsPlayerBrightness',
			node           => 'squeezeboxDisplaySettings',
			actions        => {
				  go => {
					cmd    => [ 'jiveplayerbrightnesssettings' ],
					player => 0,
				  },
			},
		},
	}

	# text size settings for players with graphical displays 
	if ( $client->isPlayer() && $client->display->isa('Slim::Display::Graphics') ) {
		push @menu, 
		{
			text           => $client->string("TEXTSIZE"),
			id             => 'settingsPlayerTextsize',
			node           => 'squeezeboxDisplaySettings',
			actions        => {
				  go => {
					cmd    => [ 'jiveplayertextsettings', 'activeFont' ],
					player => 0,
				  },
			},
		},
		{
			text           => $client->string("OFFDISPLAYSIZE"),
			id             => 'settingsPlayerOffTextsize',
			node           => 'squeezeboxDisplaySettings',
			actions        => {
				  go => {
					cmd    => [ 'jiveplayertextsettings', 'idleFont' ],
					player => 0,
				  },
			},
		},
	}

	if ($batch) {
		return \@menu;
	} else {
		_notifyJive(\@menu, $client);
	}
}

sub minAutoBrightness {
	my $current_setting = shift;
	my $string          = shift;

	my @brightness_settings;

	my $slider = {
		slider      => 1,
		min         => 1,
		max         => 5,
		initial     => $current_setting + 0,
		#help    => NO_HELP_STRING_YET,
		actions => {
			do => {
				player => 0,
				cmd    => [ 'playerpref', 'minAutoBrightness' ],
				params => {
					valtag => 'value',
				},
			},
		},
	};

	push @brightness_settings, $slider;

	my $return = { 
		text      => $string,
		count     => scalar(@brightness_settings),
		offset    => 0,
		item_loop => \@brightness_settings,
	};
	return $return;
}

sub sensAutoBrightness {
	my $current_setting = shift;
	my $string          = shift;

	my @brightness_settings;

	my $slider = {
		slider      => 1,
		min         => 1,
		max         => 20,
		initial     => $current_setting + 0,
		#help    => NO_HELP_STRING_YET,
		actions => {
			do => {
				player => 0,
				cmd    => [ 'playerpref', 'sensAutoBrightness' ],
				params => {
					valtag => 'value',
				},
			},
		},
	};

	push @brightness_settings, $slider;

	my $return = { 
		text      => $string,
		count     => scalar(@brightness_settings),
		offset    => 0,
		item_loop => \@brightness_settings,
	};
	return $return;
}

sub playerBrightnessMenu {

	my $request    = shift;
	my $client     = $request->client();

	my @menu = ();

	# WHILE ON, WHILE OFF, IDLE
	my @options = (
		{
			string => 'SETUP_POWERONBRIGHTNESS_ABBR',
			pref   => 'powerOnBrightness',
		},
		{
			string => 'SETUP_POWEROFFBRIGHTNESS_ABBR',
			pref   => 'powerOffBrightness',
		},
		{
			string => 'SETUP_IDLEBRIGHTNESS_ABBR',
			pref   => 'idleBrightness',
		},
	);

	for my $href (@options) {
		my @radios = ();
		my $currentSetting = $prefs->client($client)->get($href->{pref});

		# assemble radio buttons based on what's available for that player
		my $hash  = $client->display->getBrightnessOptions();
		for my $setting (sort { $b <=> $a } keys %$hash) {
			my $item = {
				text => $hash->{$setting},
				radio => ($currentSetting == $setting) + 0,
				actions => {
					do    => {
						cmd    => [ 'playerpref', $href->{pref}, $setting ],
						player => 0,
					},
				},
			};
			push @radios, $item;
		}

		my $item = {
			text    => $client->string($href->{string}),
			count   => scalar(@radios),
			offset  => 0,
			item_loop => \@radios,
		};
		push @menu, $item;
	}

	if( $client->isa( 'Slim::Player::Boom')) {
		my $mab = minAutoBrightness( $prefs->client( $client)->get( 'minAutoBrightness'), $client->string( 'SETUP_MINAUTOBRIGHTNESS'));
		push @menu, $mab;

		my $sab = sensAutoBrightness( $prefs->client( $client)->get( 'sensAutoBrightness'), $client->string( 'SETUP_SENSAUTOBRIGHTNESS'));
		push @menu, $sab;
	}

	sliceAndShip($request, $client, \@menu);

	$request->setStatusDone();

}

sub playerTextMenu {

	my $request    = shift;
	my $client     = $request->client();
	my $whatFont   = $request->getParam('_whatFont');

	my @menu = ();

	my @fonts = ();
	my $i = 0;

	for my $font ( @{ $prefs->client($client)->get($whatFont) } ) {
		push @fonts, {
			name  => $client->string($font),
			value => $i++,
		};
	}

	my $currentSetting = $prefs->client($client)->get($whatFont . '_curr');
	for my $font (@fonts) {
		my $item = {
			text => $font->{name},
			radio => ($font->{value} == $currentSetting) + 0,
			actions => {
				do    => {
					cmd    => [ 'playerpref', $whatFont . '_curr', $font->{value} ],
					player => 0,
				},
			},
		};
		push @menu, $item;
	}

	sliceAndShip($request, $client, \@menu);

	$request->setStatusDone();

}

sub browseMusicFolder {
	main::INFOLOG && $log->info("Begin function");
	my $client = shift;
	my $batch = shift;

	# first we decide if $audiodir has been configured. If not, don't show this
	my $audiodir = $prefs->get('audiodir');

	my $return = 0;
	if (defined($audiodir) && -d $audiodir) {
		main::INFOLOG && $log->info("Adding Browse Music Folder");
		$return = {
				text           => $client->string('BROWSE_MUSIC_FOLDER'),
				id             => 'myMusicMusicFolder',
				node           => 'myMusic',
				weight         => 70,
				actions        => {
					go => {
						cmd    => ['musicfolder'],
						params => {
							menu => 'musicfolder',
						},
					},
				},
			};
	} else {
		# if it disappeared, send a notification to get rid of it if it exists
		main::INFOLOG && $log->info("Removing Browse Music Folder from Jive menu via notification");
		deleteMenuItem('myMusicMusicFolder', $client);
	}

	if ($batch) {
		return $return;
	} else {
		_notifyJive( [ $return ], $client);
	}
}

sub repeatSettings {
	my $client = shift;
	my $batch = shift;

	my $repeat_setting = Slim::Player::Playlist::repeat($client);
	my @repeat_strings = ('OFF', 'SONG', 'PLAYLIST_SHORT',);
	my @translated_repeat_strings = map { ucfirst($client->string($_)) } @repeat_strings;
	my @repeatChoiceActions;
	for my $i (0..$#repeat_strings) {
		push @repeatChoiceActions, 
		{
			player => 0,
			cmd    => ['playlist', 'repeat', "$i"],
		};
	}
	my $return = {
		text           => $client->string("REPEAT"),
		id             => 'settingsRepeat',
		node           => 'settings',
		weight         => 20,
		choiceStrings  => [ @translated_repeat_strings ],
		selectedIndex  => $repeat_setting + 1, # 1 is added to make it count like Lua
		actions        => {
			do => { 
				choices => [ 
					@repeatChoiceActions 
				], 
			},
		},
	};
	if ($batch) {
		return $return;
	} else {
		_notifyJive( [ $return ], $client);
	}
}

sub shuffleSettings {
	my $client = shift;
	my $batch = shift;

	my $shuffle_setting = Slim::Player::Playlist::shuffle($client);
	my @shuffle_strings = ( 'OFF', 'SONG', 'ALBUM',);
	my @translated_shuffle_strings = map { ucfirst($client->string($_)) } @shuffle_strings;
	my @shuffleChoiceActions;
	for my $i (0..$#shuffle_strings) {
		push @shuffleChoiceActions, 
		{
			player => 0,
			cmd => ['playlist', 'shuffle', "$i"],
		};
	}
	my $return = {
		text           => $client->string("SHUFFLE"),
		id             => 'settingsShuffle',
		node           => 'settings',
		selectedIndex  => $shuffle_setting + 1,
		weight         => 10,
		choiceStrings  => [ @translated_shuffle_strings ],
		actions        => {
			do => {
				choices => [
					@shuffleChoiceActions
				],
			},
		},
	};

	if ($batch) {
		return $return;
	} else {
		_notifyJive( [ $return ], $client);
	}
}

sub _clientId {
	my $client = shift;
	my $id = 'all';
	if ( blessed($client) && $client->id() ) {
		$id = $client->id();
	}
	return $id;
}
	
sub _notifyJive {
	my ($menu, $client, $action) = @_;
	$action ||= 'add';
	
	my $id = _clientId($client);
	my $menuForExport = $action eq 'add' ? _purgeMenu($menu) : $menu;
	
	$menuForExport = [ map { _localizeMenuItemText( $client, $_ ) } @{$menuForExport} ];
	
	Slim::Control::Request::notifyFromArray( $client, [ 'menustatus', $menuForExport, $action, $id ] );
}

sub howManyPlayersToSyncWith {
	my $client = shift;
	return 0 if $client->isa('Slim::Player::Disconnected');
	
	my @playerSyncList = Slim::Player::Client::clients();
	my $synchablePlayers = 0;
	
	# Restrict based on players with same userid on SN
	my $userid;
	if ( main::SLIM_SERVICE ) {
		$userid = $client->playerData->userid;
	}
	
	for my $player (@playerSyncList) {
		# skip ourself
		next if ($client eq $player);
		# we only sync slimproto devices
		next if (!$player->isPlayer());
		
		# On SN, only sync with players on the current account
		if ( main::SLIM_SERVICE ) {
			next if $userid == 1;
			next if $userid != $player->playerData->userid;
			
			# Skip players with old firmware
			if (
				( $player->model eq 'squeezebox2' && $player->revision < 82 )
				||
				( $player->model eq 'transporter' && $player->revision < 32 )
			) {
				next;
			}
		}
		
		$synchablePlayers++;
	}
	return $synchablePlayers;
}

sub getPlayersToSyncWith() {
	my $client = shift;
	my @return = ();
	
	# Restrict based on players with same userid on SN
	my $userid;
	if ( main::SLIM_SERVICE ) {
		$userid = $client->playerData->userid;
	}
	
	# first add a descriptive line for this player
	push @return, {
		text  => $client->string('SYNC_X_TO', $client->name()),
		style => 'itemNoAction',
	};

	# come up with a list of players and/or sync groups to sync with
	# callback command also has to remove player from whatever it was previously synced to, if anything
	my $cnt      = 0;
	my @players  = Slim::Player::Client::clients();

	# construct the list
	my @syncList;
	my $currentlySyncedWith = 0;
	
	# the logic is a little tricky here...first make a pass at any sync groups that include $client
	if ($client->isSynced()) {
		if (_syncSNCheck($userid, $client)) {
			$syncList[$cnt] = {};
			$syncList[$cnt]->{'id'}           = $client->id();
			$syncList[$cnt]->{'name'}         = $client->syncedWithNames(0);
			$currentlySyncedWith                = $client->syncedWithNames(0);
			$syncList[$cnt]->{'isSyncedWith'} = 1;
			$cnt++;
		}
	}

	# then grab groups or players that are not currently synced with $client
        if (scalar(@players) > 0) {
		for my $eachclient (@players) {
			next if !$eachclient->isPlayer();
			next if $eachclient->isSyncedWith($client);
			next unless _syncSNCheck($userid, $eachclient);
			if ($eachclient->isSynced() && Slim::Player::Sync::isMaster($eachclient)) {
				$syncList[$cnt] = {};
				$syncList[$cnt]->{'id'}           = $eachclient->id();
				$syncList[$cnt]->{'name'}         = $eachclient->syncedWithNames(1);
				$syncList[$cnt]->{'isSyncedWith'} = 0;
				$cnt++;

			# then players which are not synced
			} elsif (! $eachclient->isSynced && $eachclient != $client ) {
				$syncList[$cnt] = {};
				$syncList[$cnt]->{'id'}           = $eachclient->id();
				$syncList[$cnt]->{'name'}         = $eachclient->name();
				$syncList[$cnt]->{'isSyncedWith'} = 0;
				$cnt++;

			}
		}
	}

	for my $syncOption (sort { $a->{name} cmp $b->{name} } @syncList) {
		push @return, { 
			text  => $syncOption->{name},
			radio => ($syncOption->{isSyncedWith} == 1) + 0,
			actions  => {
				do  => {
					player => 0,
					cmd    => [ 'jivesync' ],
					params => {
						syncWith              => $syncOption->{id},
						syncWithString        => $syncOption->{name},
						unsyncWith            => $currentlySyncedWith,
					},
				},
			},		
			nextWindow => 'refresh',
		};	
	}
	
	if ( $client->isSynced() ) {
		push @return, { 
			text  => $client->string('DO_NOT_SYNC'),
			radio => 0,
			actions  => {
				do  => {
					player => 0,
					cmd    => ['jivesync' ],
					params => {
						syncWith              => 0,
						syncWithString        => 0,
						unsyncWith            => $currentlySyncedWith,
					},
				},
			},		
			nextWindow => 'refresh',
		};	
	}

	return \@return;
}

sub _syncSNCheck {
	my ($userid, $player) = @_;
	# On SN, only sync with players on the current account
	if ( main::SLIM_SERVICE ) {
		return undef if $userid == 1;
		return undef if $userid != $player->playerData->userid;
		
		# Skip players with old firmware
		if (
			( $player->model eq 'squeezebox2' && $player->revision < 82 )
			||
			( $player->model eq 'transporter' && $player->revision < 32 )
		) {
			return undef;
		}
	}
	return 1;
}
	
sub jiveSyncCommand {
	my $request = shift;
	my $client  = $request->client();

	my $syncWith         = $request->getParam('syncWith');
	my $syncWithString   = $request->getParam('syncWithString');
	my $unsyncWith       = $request->getParam('unsyncWith') || undef;

	# first unsync if necessary
	my @messages = ();
	if ($unsyncWith) {
		$client->execute( [ 'sync', '-' ] );
		push @messages, $request->string('UNSYNCING_FROM', $unsyncWith);
	}
	# then sync if requested
	if ($syncWith) {
		my $otherClient = Slim::Player::Client::getClient($syncWith);
		$otherClient->execute( [ 'sync', $client->id ] );
			
		push @messages, $request->string('SYNCING_WITH', $syncWithString);
	}
	my $message = join("\n", @messages);

	$client->showBriefly(
		{ 'jive' =>
			{
				'type'    => 'popupplay',
				'text'    => [ $message ],
			},
		}
	);

	$request->setStatusDone();
}

sub dateQuery {
	# XXX dateQuery uses registerAutoExecute which is called for every notification!
	# (See Request.pm "send the notification to all filters...")
	# An easy workaround here is to abort on any more params in @_
	return if @_ > 1;
	
	my $request = shift;

	if ( $request->isNotQuery([['date']]) ) {
		$request->setStatusBadDispatch();
		return;
	}

	# 7.5+ SP devices use epoch time now, much simpler
	$request->addResult( 'date_epoch', time() );

	# This is the field 7.3 and earlier players expect.
	#  7.4 is smart enough to take no action when missing
	#  the date_utc field it expects.
	# If they are hitting 7.5 SN/SC code, they're about to
	#  be upgraded anyways, but we should avoid mucking
	#  with their clock in the meantime.  These crafted "all-zeros"
	#  responses will avoid Lua errors, but the "date"
	#  command implemented by busybox will fail out on this
	#  data and take no action due to it resulting in a
	#  negative time_t value.
	$request->addResult( 'date', '0000-00-00T00:00:00+00:00' );

	# manage the subscription
	if (defined(my $timeout = $request->getParam('subscribe'))) {
		$request->registerAutoExecute($timeout, \&dateQuery);
	}

	$request->setStatusDone();
}

sub firmwareUpgradeQuery_filter {
	my $self = shift;
	my $request = shift;

	# update the query if new firmware downloaded for this machine type
	if ($request->isCommand([['fwdownloaded']]) && 
		(($request->getParam('machine') || 'jive') eq ($self->getParam('_machine') || 'jive')) ) {
		return 1;
	}

	return 0;
}

sub firmwareUpgradeQuery {
	my $request = shift;

	if ( $request->isNotQuery([['firmwareupgrade']]) ) {
		$request->setStatusBadDispatch();
		return;
	}

	my $firmwareVersion = $request->getParam('firmwareVersion');
	my $model           = $request->getParam('machine') || 'jive';
	
	# always send the upgrade url this is also used if the user opts to upgrade
	if ( my $url = Slim::Utils::Firmware->url($model) ) {
		# Bug 6828, Send relative firmware URLs for Jive versions which support it
		my ($cur_rev) = $firmwareVersion =~ m/\sr(\d+)/;
		if ( $cur_rev >= 1659 ) {
			$request->addResult( relativeFirmwareUrl => URI->new($url)->path );
		}
		else {
			$request->addResult( firmwareUrl => $url );
		}
	}
	
	if ( Slim::Utils::Firmware->need_upgrade( $firmwareVersion, $model ) ) {
		# if this is true a firmware upgrade is forced
		$request->addResult( firmwareUpgrade => 1 );
	}
	else {
		$request->addResult( firmwareUpgrade => 0 );
	}

	# manage the subscription
	if (defined(my $timeout = $request->getParam('subscribe'))) {
		$request->registerAutoExecute($timeout, \&firmwareUpgradeQuery_filter);
	}

	$request->setStatusDone();

}

sub playerPower {

	my $client = shift;
	my $batch = shift;

	return [] unless blessed($client)
		&& $client->isPlayer() && $client->canPowerOff();

	my $name  = $client->name();
	my $power = $client->power();
	my @return; 
	my ($text, $action);

	if ($power == 1) {
		$text = sprintf($client->string('JIVE_TURN_PLAYER_OFF'), $name);
		$action = 0;
	} else {
		$text = sprintf($client->string('JIVE_TURN_PLAYER_ON'), $name);
		$action = 1;
	}

	push @return, {
		text           => $text,
		id             => 'playerpower',
		node           => 'home',
		weight         => 100,
		actions        => {
			do  => {
				player => 0,
				cmd    => ['power', $action],
				},
			},
	};

	if ($batch) {
		return \@return;
	} else {
		# send player power info by notification
		_notifyJive(\@return, $client);
	}

}

sub sleepInXHash {
	main::INFOLOG && $log->info("Begin function");
	my ($client, $val, $sleepTime) = @_;
	my $text = $sleepTime == 0 ? 
		$client->string("SLEEP_CANCEL") :
		$client->string('X_MINUTES', $sleepTime);
	my %return = ( 
		text    => $text,
		actions => {
			go => {
				player => 0,
				cmd => ['sleep', $sleepTime*60 ],
			},
		},
		nextWindow => 'refresh',
		setSelectedIndex => '1',
	);
	return \%return;
}

sub transitionHash {
	
	my ($client, $val, $prefs, $strings, $thisValue) = @_;
	my %return = (
		text    => $client->string($strings->[$thisValue]),
		radio	=> ($val == $thisValue) + 0, # 0 is added to force the data type to number
		actions => {
			do => {
				player => 0,
				cmd => ['playerpref', 'transitionType', "$thisValue" ],
			},
		},
	);
	return \%return;
}

sub replayGainHash {
	
	my ($client, $val, $prefs, $strings, $thisValue) = @_;
	my %return = (
		text    => $client->string($strings->[$thisValue]),
		radio	=> ($val == $thisValue) + 0, # 0 is added to force the data type to number
		actions => {
			do => {
				player => 0,
				cmd => ['playerpref', 'replayGainMode', "$thisValue"],
			},
		},
	);
	return \%return;
}

sub myMusicMenu {
	main::INFOLOG && $log->info("Begin function: ", (Slim::Schema::hasLibrary() ? 'library' : 'no library'));
	my $batch = shift;
	my $client = shift;
	my $sort   = $prefs->get('jivealbumsort') || 'album';
	my $party  = (Slim::Player::Playlist::playlistMode($client) eq 'party');
	my @myMusicMenu = ();
	
	if (Slim::Schema::hasLibrary()) {
		
		@myMusicMenu = (
			{
				text           => $client->string('BROWSE_BY_ARTIST'),
				homeMenuText   => $client->string('BROWSE_ARTISTS'),
				id             => 'myMusicArtists',
				node           => 'myMusic',
				weight         => 10,
				actions        => {
					go => {
						cmd    => ['artists'],
						params => {
							menu  => 'album',
							party => $party,
						},
					},
				},
			},		
			{
				text           => $client->string('BROWSE_BY_ALBUM'),
				homeMenuText   => $client->string('BROWSE_ALBUMS'),
				id             => 'myMusicAlbums',
				node           => 'myMusic',
				weight         => 20,
				actions        => {
					go => {
						cmd    => ['albums'],
						params => {
							menu     => 'track',
							sort     => $sort,
							party    => $party,
						},
					},
				},
				window         => {
					menuStyle => 'album',
					menuStyle => 'album',
				},
			},
			{
				text           => $client->string('BROWSE_BY_GENRE'),
				homeMenuText   => $client->string('BROWSE_GENRES'),
				id             => 'myMusicGenres',
				node           => 'myMusic',
				weight         => 30,
				actions        => {
					go => {
						cmd    => ['genres'],
						params => {
							menu  => 'artist',
							party => $party,
						},
					},
				},
			},
			{
				text           => $client->string('BROWSE_BY_YEAR'),
				homeMenuText   => $client->string('BROWSE_YEARS'),
				id             => 'myMusicYears',
				node           => 'myMusic',
				weight         => 40,
				actions        => {
					go => {
						cmd    => ['years'],
						params => {
							menu  => 'album',
							party => $party,
						},
					},
				},
			},
			{
				text           => $client->string('BROWSE_NEW_MUSIC'),
				id             => 'myMusicNewMusic',
				node           => 'myMusic',
				weight         => 50,
				actions        => {
					go => {
						cmd    => ['albums'],
						params => {
							menu  => 'track',
							sort  => 'new',
							party => $party,
						},
					},
				},
				window        => {
					menuStyle => 'album',
				},
			},
			{
				text           => $client->string('SAVED_PLAYLISTS'),
				id             => 'myMusicPlaylists',
				node           => 'myMusic',
				weight         => 80,
				actions        => {
					go => {
						cmd    => ['playlists'],
						params => {
							menu  => 'track',
							party => $party,
						},
					},
				},
			},
			{
				text           => $client->string('SEARCH'),
				id             => 'myMusicSearch',
				node           => 'myMusic',
				isANode        => 1,
				weight         => 90,
			},
		);
		# add the items for under mymusicSearch
		my $searchMenu = searchMenu(1, $client);
		@myMusicMenu = (@myMusicMenu, @$searchMenu);
	
		if (my $browseMusicFolder = browseMusicFolder($client, 1)) {
			push @myMusicMenu, $browseMusicFolder;
		}
	}

	if ($batch) {
		return \@myMusicMenu;
	} else {
		_emptyMyMusicMenu($client);
		_notifyJive(\@myMusicMenu, $client);
	}

}

sub _emptyMyMusicMenu {
	main::INFOLOG && $log->info("Begin function");
	my $client = shift;
		
	my @myMusicMenu = map +{id => $_, node => 'myMusic'},
		qw(
			myMusicArtists
			myMusicAlbums
			myMusicGenres
			myMusicYears
			myMusicNewMusic
			myMusicPlaylists
			myMusicSearch
			myMusicMusicFolder
		);

	_notifyJive(\@myMusicMenu, $client, 'remove');
}

sub searchMenu {
	main::INFOLOG && $log->info("Begin function");
	my $batch = shift;
	my $client = shift || undef;
	my $party = ( $client && Slim::Player::Playlist::playlistMode($client) eq 'party' );
	my @searchMenu = (
	{
		text           => $client->string('ARTISTS'),
		homeMenuText   => $client->string('ARTIST_SEARCH'),
		id             => 'myMusicSearchArtists',
		node           => 'myMusicSearch',
		weight         => 10,
		input => {
			len  => 1, #bug 5318
			processingPopup => {
				text => $client->string('SEARCHING'),
			},
			help => {
				text => $client->string('JIVE_SEARCHFOR_HELP')
			},
		},
		actions => {
			go => {
				cmd => ['artists'],
				params => {
					menu     => 'album',
					menu_all => '1',
					useContextMenu => '1',
					search   => '__TAGGEDINPUT__',
					party    => $party,
					_searchType => 'artists',
				},
                        },
		},
                window => {
                        text => $client->string('SEARCHFOR_ARTISTS'),
                },
	},
	{
		text           => $client->string('ALBUMS'),
		homeMenuText   => $client->string('ALBUM_SEARCH'),
		id             => 'myMusicSearchAlbums',
		node           => 'myMusicSearch',
		weight         => 20,
		input => {
			len  => 1, #bug 5318
			processingPopup => {
				text => $client->string('SEARCHING'),
			},
			help => {
				text => $client->string('JIVE_SEARCHFOR_HELP')
			},
		},
		actions => {
			go => {
				cmd => ['albums'],
				params => {
					menu     => 'track',
					menu_all => '1',
					useContextMenu => '1',
					search   => '__TAGGEDINPUT__',
					_searchType => 'albums',
					party    => $party,
				},
			},
		},
		window => {
			text => $client->string('SEARCHFOR_ALBUMS'),
			menuStyle  => 'album',
		},
	},
	{
		text           => $client->string('SONGS'),
		homeMenuText   => $client->string('TRACK_SEARCH'),
		id             => 'myMusicSearchSongs',
		node           => 'myMusicSearch',
		weight         => 30,
		input => {
			len  => 1, #bug 5318
			processingPopup => {
				text => $client->string('SEARCHING'),
			},
			help => {
				text => $client->string('JIVE_SEARCHFOR_HELP')
			},
		},
		actions => {
			go => {
				cmd => ['tracks'],
				params => {
					menu     => 'track',
					menuStyle => 'album',
					useContextMenu => '1',
					menu_all => '1',
					search   => '__TAGGEDINPUT__',
					_searchType => 'tracks',
				},
                        },
		},
		window => {
			text => $client->string('SEARCHFOR_SONGS'),
			menuStyle => 'album',
		},
	},
	{
		text           => $client->string('PLAYLISTS'),
		homeMenuText   => $client->string('PLAYLIST_SEARCH'),
		id             => 'myMusicSearchPlaylists',
		node           => 'myMusicSearch',
		weight         => 40,
		input => {
			len  => 1, #bug 5318
			processingPopup => {
				text => $client->string('SEARCHING'),
			},
			help => {
				text => $client->string('JIVE_SEARCHFOR_HELP')
			},
		},
		actions => {
			go => {
				cmd => ['playlists'],
				params => {
					menu     => 'track',
					menu_all => '1',
					search   => '__TAGGEDINPUT__',
					party    => $party,
				},
                        },
		},
		window => {
			text => $client->string('SEARCHFOR_PLAYLISTS'),
		},
	},

	);

	if ($batch) {
		return \@searchMenu;
	} else {
		_notifyJive(\@searchMenu, $client);
	}

}

sub globalSearchMenu {
	my $client = shift || undef;

	my @searchMenu = ({
		stringToken	   => 'SEARCH',
		text           => $client->string('SEARCH'),
		homeMenuText   => $client->string('SEARCH'),
		id             => 'globalSearch',
		node           => 'home',
		weight         => 110,
		input => {
			len  => 1, #bug 5318
			processingPopup => {
				text => $client->string('SEARCHING'),
			},
			help => {
				text => $client->string('JIVE_SEARCHFOR_HELP')
			},
		},
		actions => {
			go => {
				cmd => ['globalsearch', 'items'],
				params => {
					menu     => 'globalsearch',
					search   => '__TAGGEDINPUT__',
				},
			},
		},
		window => {
			text => $client->string('SEARCH'),
		},
	});

	return \@searchMenu;	
}

# send a notification for menustatus
sub menuNotification {
	# the lines below are needed as menu notifications are done via notifyFromArray, but
	# if you wanted to debug what's getting sent over the Comet interface, you could do it here
	my $request  = shift;
	my $dataRef          = $request->getParam('_data')   || return;
	my $action   	     = $request->getParam('_action') || 'add';
	my $client           = $request->clientid();
	main::INFOLOG && $log->is_info && $log->info("Menustatus notification sent:", $action, '->', ($client || 'all'));
	main::DEBUGLOG && $log->is_debug && $log->debug(Data::Dump::dump($dataRef));
}

sub jivePlayTrackPlaylistCommand {

	main::INFOLOG && $log->info("Begin function");

	my $request    = shift;
	my $client     = $request->client || return;
	my $playlistID = $request->getParam('playlist_id');
	my $listIndex  = $request->getParam('list_index');
	
	$client->execute( ["playlist", "clear"] );

	if ( !defined ($playlistID) ) {
		$request->setStatusBadDispatch();
		return;
	}

	# load the playlist
	$client->execute( ["playlistcontrol", "cmd:load", "playlist_id:$playlistID" ]);
	# jump to the correct index
	$client->execute( ["playlist", "jump", $listIndex] );

	$request->setStatusDone();
}

sub jivePlayTrackAlbumCommand {

	main::INFOLOG && $log->info("Begin function");

	my $request    = shift;
	my $client     = $request->client || return;
	my $albumID    = $request->getParam('album_id');
	my $artistID   = $request->getParam('artist_id');
	my $trackID    = $request->getParam('track_id');
	my $playlistID = $request->getParam('playlist_id');
	my $folder     = $request->getParam('folder')|| undef;
	my $listIndex  = $request->getParam('list_index');
 	my $mode       = Slim::Player::Playlist::playlistMode($client);
	
	if ( ( $mode eq 'on' || $mode eq 'party' ) && $trackID ) {
		# send the track with cmd of 'load' so playlistcontrol doesn't turn off playlistmode
		$client->execute( ['playlistcontrol', 'cmd:load', "track_id:$trackID" ] );
		return;
	}

	$client->execute( ["playlist", "clear"] );

	# Database album browse is the simple case
	if ( $albumID ) {
		if ($artistID) {
			$client->execute( ["playlist", "addtracks", { 'contributor.id' => $artistID, 'album.id' => $albumID } ] );
		} else {
			$client->execute( ["playlist", "addtracks", { 'album.id' => $albumID } ] );
		}
		$client->execute( ["playlist", "jump", $listIndex] );

	}

	elsif ( $playlistID ) {
		# load the playlist
		$client->execute( ["playlistcontrol", "cmd:load", "playlist_id:$playlistID" ]);
		# jump to the correct index
		$client->execute( ["playlist", "jump", $listIndex] );
	}

	# hard case is Browse Music Folder - re-create the playlist, starting playback with the current item
	elsif ( $folder && defined $listIndex ) {

		my $wasShuffled = Slim::Player::Playlist::shuffle($client);
		Slim::Player::Playlist::shuffle($client, 0);

		my ($topLevelObj, $items, $count) = Slim::Utils::Misc::findAndScanDirectoryTree( {
			url => $folder,
		} );

		main::INFOLOG && $log->info("Playing all in folder, starting with $listIndex");

		# filter out folders
		@{$items} = grep { Slim::Music::Info::isSong($_) }
		# make sure we get a valid path
		map { ref $_ ? $_ : Slim::Utils::Misc::fixPath($_, $folder) }
		@$items;

		main::INFOLOG && $log->info("Load folder playlist, now starting at index: $listIndex");

		$client->execute(['playlist', 'clear']);
		$client->execute(['playlist', 'addtracks', 'listref', $items]);
		$client->execute(['playlist', 'jump', $listIndex]);

		if ($wasShuffled) {
			$client->execute(['playlist', 'shuffle', 1]);
		}
	}

	$request->setStatusDone();
}

sub jivePlaylistsCommand {

	main::INFOLOG && $log->info("Begin function");
	my $request    = shift;
	my $client     = $request->client || return;
	my $title      = $request->getParam('title');
	my $url        = $request->getParam('url');
	my $command    = $request->getParam('_cmd');
	my $playlistID = $request->getParam('playlist_id');
	my $token      = uc($command); 
	my @delete_menu= (
		{
			text    => $client->string('CANCEL'),
			actions => {
				go => {
					player => 0,
					cmd    => [ 'jiveblankcommand' ],
				},
			},
			nextWindow => 'parent',
		}
	);
	my $actionItem = {
		text    => $client->string($token) . ' ' . $title,
		actions => {
			go => {
				player => 0,
				cmd    => ['playlists', 'delete'],
				params => {
					playlist_id    => $playlistID,
					title          => $title,
					url            => $url,
				},
			},
		},
		nextWindow => 'grandparent',
	};
	push @delete_menu, $actionItem;

	$request->addResult('offset', 0);
	$request->addResult('count', 2);
	$request->addResult('item_loop', \@delete_menu);

	$request->setStatusDone();

}

sub jiveUnmixableMessage {
	my $request = shift;
	my $service = $request->getParam('contextToken');
	my $serviceString = $request->string($service);

       my @menu = (
               {
                       text    => $request->string('OK'),
                       actions => {
                               go => {
                                       player => 0,
                                       cmd    => [ 'jiveblankcommand' ],
                               },
                       },
                       nextWindow => 'parent',
               }
       );

       $request->addResult('offset', 0);
       $request->addResult('count', 1);
       $request->addResult('item_loop', \@menu);
       $request->addResult('window', { textarea => $request->string('UNMIXABLE', $serviceString), });

        $request->setStatusDone();
}

sub jiveAlarmCommand {
	main::INFOLOG && $log->info("Begin function");

	my $request    = shift;
	my $client     = $request->client || return;

	# this command can issue either a snooze or a cancel
	my $snooze      = $request->getParam('snooze') ? 1 : undef;
	my $stop        = $request->getParam('stop')   ? 1 : undef;
	my $fadein      = $request->getParam('fadein');

	my $alarm       = Slim::Utils::Alarm->getCurrentAlarm($client);

	if ( defined($alarm) ) {
		if ( defined($snooze) ) {
			$alarm->snooze();
		} elsif ( defined ($stop) ) {
			$alarm->stop();
		}
	}

	# a fadein:1 tag needs to set a clientpref for alarmfadeseconds
	if ( defined($fadein) ) {
		$log->error('Fade in alarm being set to ', $fadein);
		$prefs->client($client)->set('alarmfadeseconds', $fadein);
	}
	$request->setStatusDone();
}

sub jiveFavoritesCommand {

	main::INFOLOG && $log->info("Begin function");
	my $request = shift;
	my $client  = $request->client || shift;
	my $title   = $request->getParam('title');
	my $url     = $request->getParam('url');
	my $type    = $request->getParam('type');
	my $icon    = $request->getParam('icon');
	my $command = $request->getParam('_cmd');
	my $token   = uc($command); # either ADD or DELETE
	my $action = 'grandparent';
	my $favIndex = defined($request->getParam('item_id'))? $request->getParam('item_id') : undef;

	if ( $command eq 'set_preset' ) {
		# XXX: why do we use a favorites_ prefix here but not above?
		my $preset = $request->getParam('key');
		my $title  = $request->getParam('favorites_title');
		my $url    = $request->getParam('favorites_url');
		my $type   = $request->getParam('favorites_type');
		my $parser = $request->getParam('parser');

		# if playlist_index is sent, that's for the current NP track, derive everything you need from it
		my $playlist_index = $request->getParam('playlist_index');
		if ( defined($playlist_index) ) {
			my $song = Slim::Player::Playlist::song( $client, $playlist_index );
			$url     = $song->url;
			$type    = 'audio';
			$title   = $song->title;
		}

		# favorite needs to be saved as either a playlist or default to audio
		if ( defined($type) && $type ne 'playlist' ) {
			$type = 'audio';
		}
		if ( ! defined $title || ! defined $url ) {
			$request->setStatusBadDispatch();
			return;
		}
		
		$client->setPreset( {
			slot   => $preset,
			URL    => $url,
			text   => $title,
			type   => $type,
			parser => $parser,
		} );

		$client->showBriefly({
			jive => {
				type     => 'popupplay',
				text     => [ $client->string('PRESET_ADDING', $preset), $title ],
			},
		});
	} else {

		my @favorites_menu = (
			{
				text    => $client->string('CANCEL'),
				actions => {
					go => {
						player => 0,
						cmd    => [ 'jiveblankcommand' ],
					},
				},
				nextWindow => 'parent',
			}
		);
		my $actionItem = {
			text    => $client->string($token) . ' ' . $title,
			actions => {
				go => {
					player => 0,
					cmd    => ['favorites', $command ],
					params => {
						title => $title,
						url   => $url,
						type  => $type,
					},
				},
			},
			nextWindow => $action,
		};
		$actionItem->{'actions'}{'go'}{'params'}{'icon'} = $icon if $icon;
		$actionItem->{'actions'}{'go'}{'params'}{'item_id'} = $favIndex if defined($favIndex);
		push @favorites_menu, $actionItem;
	
		$request->addResult('offset', 0);
		$request->addResult('count', 2);
		$request->addResult('item_loop', \@favorites_menu);
	}
	
	
	$request->setStatusDone();
		
} 

sub _jiveNoResults {
	my $request = shift;
	$request->addResult('count', '1');
	$request->addResult('offset', 0);
	$request->addResultLoop('item_loop', 0, 'text', $request->string('EMPTY'));
	$request->addResultLoop('item_loop', 0, 'style', 'itemNoAction');
	$request->addResultLoop('item_loop', 0, 'action', 'none');
}

sub cacheSearch {
	my $request = shift;
	my $search  = shift;
	
	# Don't cache searches on SN
	return if main::SLIM_SERVICE;

	if (defined($search) && $search->{text} && $search->{actions}{go}{cmd}) {
		unshift (@recentSearches, $search);
	}
	recentSearchMenu($request->client, 0);
}

sub recentSearchMenu {
	my $client  = shift;
	my $batch   = shift;

	my @recentSearchMenu = ();
	return \@recentSearchMenu unless $client;

	if (scalar(@recentSearches) == 1) {
		push @recentSearchMenu,
		{
			text           => $client->string('RECENT_SEARCHES'),
			id             => 'homeSearchRecent',
			node           => 'home',
			weight         => 111,
			actions => {
				go => {
					cmd => ['jiverecentsearches'],
       	                 },
			},
			window => {
				text => $client->string('RECENT_SEARCHES'),
			},
		};
		push @recentSearchMenu,
		{
			text           => $client->string('RECENT_SEARCHES'),
			id             => 'myMusicSearchRecent',
			node           => 'myMusicSearch',
			noCustom       => 1,
			weight         => 50,
			actions => {
				go => {
					cmd => ['jiverecentsearches'],
       	                 	},
			},
			window => {
				text => $client->string('RECENT_SEARCHES'),
			},
		};	
		if (!$batch) {
			_notifyJive(\@recentSearchMenu, $client);
		}
	}
	return \@recentSearchMenu;

}

sub jiveRecentSearchQuery {

	my $request = shift;
	main::INFOLOG && $log->info("Begin Function");

	# check this is the correct query.
	if ($request->isNotQuery([['jiverecentsearches']])) {
		$request->setStatusBadDispatch();
		return;
	}

	my $totalCount = scalar(@recentSearches);
	if ($totalCount == 0) {
		# this is an empty resultset
		_jiveNoResults($request);
	} else {
		my $maxCount = 200;
		$totalCount = $totalCount > $maxCount ? ($maxCount - 1) : $totalCount;
		$request->addResult('count', $totalCount);
		$request->addResult('offset', 0);
		for my $i (0..$totalCount) {
			last unless $recentSearches[$i];
			my $href = $recentSearches[$i];
			for my $key (keys %$href) {
				$request->addResultLoop('item_loop', $i, $key, $href->{$key});
			}
		}
	}
	$request->setStatusDone();
}

sub jiveDummyCommand {
	return;
}


# The following allow download of extensions (applets, wallpaper and sounds) from SC to jive

# hash of providers for extension information
my %extensionProviders = ();

sub registerExtensionProvider {
	my $name     = shift;
	my $provider = shift;
	my $optstr   = shift;

	main::INFOLOG && $log->info("adding extension provider $name" . ($optstr ? " optstr: $optstr" : ""));

	$extensionProviders{ $name } = { provider => $provider, optstr => $optstr };
}

sub removeExtensionProvider {
	my $name = shift;

	main::INFOLOG && $log->info("deleting extension provider $name");

	delete $extensionProviders{ $name };
}

# return all extensions available for a specific type, version and target
# uses extension providers to provide a list of extensions available for the query criteria
# these are async so they can fetch and parse data to build a list of extensions
sub extensionsQuery {
	my $request = shift;
 
	my ($type) = $request->getRequest(0) =~ /jive(applet|wallpaper|sound|patche)s/; # S:P:Extensions always appends 's' to type
	my $version= $request->getParam('version');
	my $target = $request->getParam('target');
	my $optstr = $request->getParam('optstr');

	if (!defined $type) {
		$request->setStatusBadDispatch();
		return;
	}

	my @providers;
	my $language  = $Slim::Utils::Strings::currentLang;

	# remove optional providers if key is included in the query and it does not match
	# this allows SP to select whether optional providers are used to build the list
	for my $provider (keys %extensionProviders) {
		if (!$optstr || !defined $extensionProviders{$provider}->{'optstr'} || $optstr =~ /$extensionProviders{$provider}->{optstr}/) {
			push @providers, $provider;
		}
	}

	if (scalar @providers) {

		$request->privateData( { remaining => scalar @providers, results => [] } );

		$request->setStatusProcessing;

		for my $provider (@providers) {

			$extensionProviders{$provider}->{'provider'}->( {
				'name'   => $provider, 
				'type'   => $type, 
				'target' => $target,
				'version'=> $version, 
				'lang'   => $language,
				'details'=> 1,
				'cb'     => \&_extensionsQueryCB,
				'pt'     => [ $request ]
			});
		}

	} else {

		$request->addResult("count", 0);

		$request->setStatusDone();
	}
}

sub _extensionsQueryCB {
	my $request= shift;
	my $res    = shift;
	my $data   = $request->privateData;

	splice @{$data->{'results'}}, 0, 0, @$res;

	if ( ! --$data->{'remaining'} ) {

		# create a list of entries with the duplicates removed, favoring higher version numbers

		# pass 1 - find max versions
		my $max = {};

		for my $entry (@{$data->{'results'}}) {

			my $name = $entry->{'name'};

			if (!defined $max->{$name} || Slim::Utils::Versions->compareVersions($entry->{'version'}, $max->{$name}) > 0) {

				$max->{$name} = $entry->{'version'};
			}
		}

		# pass 2 - build list containing single entry for per extension
		my @results = ();

		for my $entry (@{$data->{'results'}}) {

			my $name = $entry->{'name'};

			if (exists $max->{$name} && (!defined $max->{$name} || $max->{$name} eq $entry->{'version'})) {

				push @results, $entry;

				delete $max->{$name};
			}
		}

		my $cnt = 0;

		for my $entry ( sort { $a->{'title'} cmp $b->{'title'} } @results ) {

			$request->setResultLoopHash('item_loop', $cnt++, $entry);
		}

		$request->addResult("count", $cnt);

		$request->setStatusDone();
	}
}

sub appMenus {
	my $client = shift;
	my $batch  = shift;
	
	my $isInfo = main::INFOLOG && $log->is_info;
	
	my $apps = $client->apps;
	my $menu = [];
	
	my $disabledPlugins = Slim::Utils::PluginManager->disabledPlugins();
	my @disabled = map { $disabledPlugins->{$_}->{name} } keys %{$disabledPlugins};
	
	# We want to add nodes for the following items:
	# My Apps (node = null)
	# Home menu apps (node = home)
	# If a home menu app is not already defined in @appMenus,
	# i.e. pure OPML apps such as SomaFM
	# create one for it using the generic OPML handler
	
	for my $app ( keys %{$apps} ) {
		next unless ref $apps->{$app} eq 'HASH'; # XXX don't crash on old style
		
		# Is this app supported by a local plugin?
		if ( my $plugin = $apps->{$app}->{plugin} ) {
			# Make sure it's enabled
			if ( my $pluginInfo = Slim::Utils::PluginManager->isEnabled($plugin) ) {
				# Get the predefined menu for this plugin
				if ( my ($globalMenu) = grep { $_->{text} && $_->{text} eq $pluginInfo->{name} } @appMenus ) {				
					main::INFOLOG && $isInfo && $log->info( "App: $app, using plugin $plugin" );
				
					# Clone the existing menu and set the node
					my $clone = Storable::dclone($globalMenu);

					# Set node to home or null
					$clone->{node} = $apps->{$app}->{home_menu} == 1 ? 'home' : '';

					# Use title from app list
					$clone->{stringToken} = $apps->{$app}->{title};

					push @{$menu}, $clone;
				}
			}
			else {
				# Bug 13627, Make sure the app is not for a plugin that has been disabled.
				# We could browse menus for a disabled plugin like Last.fm, but playback
				# would be impossible.
				main::INFOLOG && $isInfo && $log->info( "App: $app, not displaying because plugin is disabled" );
				next;
			}
		}
		else {			
			# For type=opml, use generic handler
			if ( $apps->{$app}->{type} eq 'opml' ) {
				main::INFOLOG && $isInfo && $log->info( "App: $app, using generic OPML handler" );
				
				my $url = $apps->{$app}->{url} =~ /^http/
					? $apps->{$app}->{url} 
					: Slim::Networking::SqueezeNetwork->url( $apps->{$app}->{url} );
				
				my $icon = $apps->{$app}->{icon} =~ /^http/
					? $apps->{$app}->{icon} 
					: Slim::Networking::SqueezeNetwork->url( $apps->{$app}->{icon}, 'external' );
				
				my $node = $apps->{$app}->{home_menu} == 1 ? 'home' : '';
				
				push @{$menu}, {
					actions => {
						go => {
							cmd    => [ 'opml_generic', 'items' ],
							params => {
								menu     => 'opml_generic',
								opml_url => $url,
							},
							player => 0,
						},
					},
					displayWhenOff => 0,
					id             => 'opml' . $app,
					node           => $node,
					text           => $apps->{$app}->{title},
					window         => {
						'icon-id'  => $icon,
					},
				};
			}
		}
	}
	
	return [] if !scalar @{$menu};
	
	# Alpha sort and add weighting
	my $weight = 21; # After Internet Radio
	
	my @sorted =
	 	map { $_->{weight} = $weight++; $_ } 
		sort { $a->{text} cmp $b->{text} }
		@{$menu};
	
	if ( $batch ) {
		return \@sorted;
	}
	else {
		_notifyJive(\@sorted, $client);
	}
}

sub _localizeMenuItemText {
	my ( $client, $item ) = @_;
	
	return unless $client;
	
	# Don't alter the global data
	my $clone = Storable::dclone($item);
	
	if ( $clone->{stringToken} ) {
		if ( $clone->{stringToken} eq uc( $clone->{stringToken} ) ) {
			$clone->{text} = $client->string( delete $clone->{stringToken} );
		}
		else {
			$clone->{text} = delete $clone->{stringToken};
		}
	}
	elsif ( $clone->{text} && $clone->{text} eq uc( $clone->{text} ) ) {
		$clone->{text} = $client->string( $clone->{text} );
	}
	
	# call string() for screensaver titles
	if ( $clone->{screensavers} ) {
		for my $s ( @{ $clone->{screensavers} } ) {
			$s->{text} = $client->string( delete $s->{stringToken} ) if $s->{stringToken};
		}
	}
	
	# call string() for input text if necessary
	if ( my $input = $clone->{input} ) {
		if ( $input->{title} && $input->{title} eq uc( $input->{title} ) ) {
			$input->{title}        = $client->string( $input->{title} );
			$input->{help}->{text} = $client->string( $input->{help}->{text} );
			$input->{processingPopup}->{text} = $client->string( $input->{processingPopup}->{text} );
			$input->{softbutton1}  = $client->string( $input->{softbutton1} );
			$input->{softbutton2}  = $client->string( $input->{softbutton2} );
		}
	}
	
	return $clone;
}

1;
