#!/usr/bin/perl
#
=head1 Shoutcast 2.0.x Plugin

 A Plugin for monitoring a Shoutcast 2.0.x Server (Multigraph)

=head1 Munin Configuration

 [shoutcast2_multi]
  env.host 127.0.0.1			*default*
  env.port 8000					*default*
  env.pass changeme				*default*

=head2 Munin Configuration Explanation

 host = host we are attempting to connection to, can be ip, or hostname
 port = port we need to connect to in order to get to admin.cgi
 pass = password to use to authenticate as admin user

=head1 AUTHOR

 Matt West < https://github.com/mhwest13 >

=head1 License

 GPLv2

=head1 Magic Markers

#%# family=auto
#%# capabilities=autoconf

=cut

use strict;
use warnings;
use LWP::UserAgent;
use XML::Simple;
use Munin::Plugin;

need_multigraph();

=head1 Variable Declarations

 This section is mainly for importing / declaring our environment variables.
 This is were we will import the data from our plugin-conf.d file so we can
 override the default settings which will only work for Shoutcast test configs.

=cut

my $host = $ENV{host} || '127.0.0.1';
my $port = $ENV{port} || 8000;
my $pass = $ENV{pass} || 'changeme';

# Initialize hashref for storing results information...
my $dataRef;

# Create a hashref for our graph information that we will call up later...
my $graphsRef;

my $ua = LWP::UserAgent->new();
$ua->agent('Munin Shoutcast Plugin/0.1');
$ua->timeout(5);
$ua->credentials($host.':'.$port, 'Shoutcast Server', 'admin'=>$pass);

=head1 Graphs Declarations

 The following section of code contains our graph information. This is the data
 provided to Munin, so that it may properly draw our graphs based on the values
 the plugin returns.

 While you are free to change colors or labels changing the type, min, or max
 could cause unfortunate problems with your graphs.

=cut

$graphsRef->{active} = {
	config => {
		args => '--base 1000 --lower-limit 0',
		vlabel => 'Is a DJ Actively Connected?',
		category => 'streaming',
		title => 'Active States',
		info => 'This graph shows us the active state or not, depending on if a DJ is connected',
	},
	datasrc => [
		{ name => 'active', draw => 'AREA', min => '0', max => '1', label => 'On or Off', type => 'GAUGE' },
	],
};

$graphsRef->{listeners} = {
	config => {
		args => '--base 1000 --lower-limit 0',
		vlabel => 'Listener Count',
		category => 'streaming',
		title => 'Listeners',
		info => 'This graph shows us the various counts for listener states we are tracking',
	},
	datasrc => [
		{ name => 'maxlisteners', draw => 'AREA', min => '0', label => 'Max Listeners', type => 'GAUGE' },
		{ name => 'currlisteners', draw => 'STACK', min => '0', label => 'Current Listeners', type => 'GAUGE' },
	],
};

$graphsRef->{sid_active} = {
	config => {
		args => '--base 1000 --lower-limit 0',
		vlabel => 'Is a DJ Actively Connected?',
		title => 'Active State',
		info => 'This graph shows us the active state or not, depending on if a DJ is connected',
	},
	datasrc => [
		{ name => 'active', draw => 'AREA', min => '0', max => '1', label => 'On or Off', type => 'GAUGE', xmlkey => 'STREAMSTATUS' },
	],
};

$graphsRef->{sid_listeners} = {
	config => {
		args => '--base 1000 --lower-limit 0',
		vlabel => 'Listener Count',
		title => 'Listeners',
		info => 'This graph shows us the various counts for listener states we are tracking',
	},
	datasrc => [
		{ name => 'maxlisteners', draw => 'AREA', min => '0', label => 'Max Listeners', type => 'GAUGE', xmlkey => 'MAXLISTENERS' },
		{ name => 'currlisteners', draw => 'STACK', min => '0', label => 'Current Listeners', type => 'GAUGE', xmlkey => 'CURRENTLISTENERS' },
		{ name => 'peaklisteners', draw => 'LINE2', min => '0', label => 'Peak Listeners', type => 'GAUGE', xmlkey => 'PEAKLISTENERS' },
		{ name => 'uniqlisteners', draw => 'LINE2', min => '0', label => 'Unique Listeners', type => 'GAUGE', xmlkey => 'UNIQUELISTENERS' },
	],
};

if (defined($ARGV[0]) && ($ARGV[0] eq 'config')) {
	config();
	exit;
}

if (defined($ARGV[0]) && (($ARGV[0] eq 'autoconf') || ($ARGV[0] eq 'suggest'))) {
	check_autoconf();
	exit;
}

# I guess we are collecting stats to return, execute main subroutine.
main();

exit;

=head1 Subroutines

 The following is a description of what each subroutine is for and does

=head2 main

 This subroutine is our main routine should we not be calling up autoconf
 or config. Ultimately this routine will print out the values for each graph
 and graph data point we are tracking.

=cut

sub main {
	my ($returnBit,$adminRef) = fetch_admin_data();
	if ($returnBit == 0) {
		exit;
	}
	my $streamConfigRef = $adminRef->{STREAMCONFIGS}->{STREAMCONFIG};
	my $sidDataRef;
	if ($adminRef->{STREAMCONFIGS}->{TOTALCONFIGS} == 1) {
		my $sid = $streamConfigRef->{id};
		my ($return,$tmpSidRef) = fetch_sid_data($sid);
		if ($return == 0) {
			# Only one stream, and we didn't get a good response.
			exit;
		}
		$sidDataRef->{$sid} = $tmpSidRef;
	} else {
		foreach my $sid (keys %{$streamConfigRef}) {
			my ($return,$tmpSidRef) = fetch_sid_data($sid);
			if ($return == 0) {
				next;
			}
			$sidDataRef->{$sid} = $tmpSidRef;
		}
	}
	print_active_data($sidDataRef);
	print_listener_data($adminRef->{STREAMCONFIGS}->{SERVERMAXLISTENERS}, $sidDataRef);
	return;
}

=head2 print_active_data

 Thie subroutine prints out the active graph values for each stream and ultimately for
 the entire shoutcast service. Should 1 Stream be active, but 5 streams available,
 the global graph should show the state as active for the service, but clicking into
 that graph, should give you a stream level view of which stream was in use during
 what time periods.

=cut

sub print_active_data {
	my ($sidDataRef) = (@_);
	my $globalActive = 0;
	foreach my $sid (sort keys %{$sidDataRef}) {
		print "multigraph shoutcast2_active.active_sid_$sid\n";
		foreach my $dsrc (@{$graphsRef->{sid_active}->{datasrc}}) {
			print "$dsrc->{name}.value $sidDataRef->{$sid}->{$dsrc->{xmlkey}}\n";	
			if ($sidDataRef->{$sid}->{$dsrc->{xmlkey}} == 1) {
				$globalActive = 1;
			}
		}
	}
	print "multigraph shoutcast2_active\n";
	foreach my $dsrc (@{$graphsRef->{active}->{datasrc}}) {
		print "$dsrc->{name}.value $globalActive\n";
	}
	return;
}

=head2 print_listener_data

 This subroutine prints out the listener graph values for each stream and ultimately
 adds all of the current users together to show that against the maxserver count in 
 the global graph. Clicking on the global graph will reveal a bit more information
 about the users on a stream by stream basis.

=cut

sub print_listener_data {
	my ($maxListeners,$sidDataRef) = (@_);
	my $globalListeners = 0;
	foreach my $sid (sort keys %{$sidDataRef}) {
		print "multigraph shoutcast2_listeners.listeners_sid_$sid\n";
		foreach my $dsrc (@{$graphsRef->{sid_listeners}->{datasrc}}) {
			print "$dsrc->{name}.value $sidDataRef->{$sid}->{$dsrc->{xmlkey}}\n";	
			if ($dsrc->{name} eq 'currlisteners') {
				$globalListeners += $sidDataRef->{$sid}->{$dsrc->{xmlkey}};
			}
		}
	}
	print "multigraph shoutcast2_listeners\n";
	foreach my $dsrc (@{$graphsRef->{listeners}->{datasrc}}) {
		if ($dsrc->{name} eq 'maxlisteners') {
			print "$dsrc->{name}.value $maxListeners\n";
		} else {
			print "$dsrc->{name}.value $globalListeners\n";
		}
	}
	return;
}

=head2 config

 The config subroutine can be seen as the main config routine, which
 will call up to your shoutcast server to figure out how many streams
 you have running, and then print out the appropriate multigraph info.
 Ultimately this subroutine will call two more routines to print out
 the graph args / configuration information.

=cut

sub config {
	my ($returnBit,$adminRef) = fetch_admin_data();
	if ($returnBit == 0) {
		# $adminRef returned a string, we'll just print it out.
		print "no (error response: $adminRef)\n";
		exit;
	}
	my $streamConfigRef = $adminRef->{STREAMCONFIGS}->{STREAMCONFIG};
	my $sidDataRef;
	if ($adminRef->{STREAMCONFIGS}->{TOTALCONFIGS} == 1) {
		my $sid = $streamConfigRef->{id};
		my ($return,$tmpSidRef) = fetch_sid_data($sid);
		if ($return == 0) {
			# Only one stream, and we didn't get a good response.
			exit;
		}
		$sidDataRef->{$sid} = $tmpSidRef;
	} else {
		foreach my $sid (keys %{$streamConfigRef}) {
			my ($return,$tmpSidRef) = fetch_sid_data($sid);
			if ($return == 0) {
				next;
			}
			$sidDataRef->{$sid} = $tmpSidRef;
		}
	}
	print_active_config($sidDataRef);
	print_listener_config($sidDataRef);
	return;	
}

=head2 print_active_config

 This subroutine prints out the graph information for our active graphs.
 It prints the sub-multigraphs first based on stream id, and finally the
 root active graph. Its not suggested that you mess with this routine
 unless you fully understand what its doing and how munin graph_args work.

=cut

sub print_active_config {
	my ($sidDataRef) = (@_);
	foreach my $sid (sort keys %{$sidDataRef}) {
		# Print the Config Info First
		print "multigraph shoutcast2_active.active\_sid\_$sid\n";
		print "graph_title ".$graphsRef->{sid_active}->{config}->{title}." for StreamID: $sid\n";
		print "graph_args ".$graphsRef->{sid_active}->{config}->{args}."\n";
		print "graph_vlabel ".$graphsRef->{sid_active}->{config}->{vlabel}."\n";
		print "graph_category streamid_$sid\n";
		print "graph_info ".$graphsRef->{sid_active}->{config}->{info}."\n";
		# Print the Data Value Info
		foreach my $dsrc (@{$graphsRef->{sid_active}->{datasrc}}) {
			while ( my ($key, $value) = each (%{$dsrc})) {
				next if ($key eq 'name');
				next if ($key eq 'xmlkey');
				print "$dsrc->{name}.$key $value\n";
			}
		}
	}
	print "multigraph shoutcast2_active\n";
	print "graph_title ".$graphsRef->{active}->{config}->{title}."\n";
	print "graph_args ".$graphsRef->{active}->{config}->{args}."\n";
	print "graph_vlabel ".$graphsRef->{active}->{config}->{vlabel}."\n";
	print "graph_category ".$graphsRef->{active}->{config}->{category}."\n";
	print "graph_info ".$graphsRef->{active}->{config}->{info}."\n";
	# Print the Data Value Info
	foreach my $dsrc (@{$graphsRef->{active}->{datasrc}}) {
		while ( my ($key, $value) = each (%{$dsrc})) {
			next if ($key eq 'name');
			print "$dsrc->{name}.$key $value\n";
		}
	}
	return;
}

=head2 print_listener_config

 This subroutine prints out the graph information for our listeners graphs.
 It prints the sub-multigraphs first based on stream id, and finally the
 root listeners graph. Its not suggested that you mess with this routine
 unless you fully understand what its doing and how munin graph_args work.

=cut

sub print_listener_config {
	my ($sidDataRef) = (@_);
	foreach my $sid (sort keys %{$sidDataRef}) {
		# Print the Config Info First
		print "multigraph shoutcast2_listeners.listeners\_sid\_$sid\n";
		print "graph_title ".$graphsRef->{sid_listeners}->{config}->{title}." for StreamID: $sid\n";
		print "graph_args ".$graphsRef->{sid_listeners}->{config}->{args}."\n";
		print "graph_vlabel ".$graphsRef->{sid_listeners}->{config}->{vlabel}."\n";
		print "graph_category streamid_$sid\n";
		print "graph_info ".$graphsRef->{sid_listeners}->{config}->{info}."\n";
		# Print the Data Value Info
		foreach my $dsrc (@{$graphsRef->{sid_listeners}->{datasrc}}) {
			while ( my ($key, $value) = each (%{$dsrc})) {
				next if ($key eq 'name');
				next if ($key eq 'xmlkey');
				print "$dsrc->{name}.$key $value\n";
			}
		}
	}
	print "multigraph shoutcast2_listeners\n";
	print "graph_title ".$graphsRef->{listeners}->{config}->{title}."\n";
	print "graph_args ".$graphsRef->{listeners}->{config}->{args}."\n";
	print "graph_vlabel ".$graphsRef->{listeners}->{config}->{vlabel}."\n";
	print "graph_category ".$graphsRef->{listeners}->{config}->{category}."\n";
	print "graph_info ".$graphsRef->{listeners}->{config}->{info}."\n";
	# Print the Data Value Info
	foreach my $dsrc (@{$graphsRef->{listeners}->{datasrc}}) {
		while ( my ($key, $value) = each (%{$dsrc})) {
			next if ($key eq 'name');
			print "$dsrc->{name}.$key $value\n";
		}
	}
	return;
}

=head2 check_autoconf

 This subroutine is called up when we intercept autoconf specified in ARGV[0]
 If we are able to connect to the shoutcast service as admin and fetch the main
 admin stats page, we will return ok, otherwise we will return no and the error
 response we got from LWP::UserAgent.

=cut

sub check_autoconf {
	my ($returnBit,$adminRef) = fetch_admin_data();
	if ($returnBit == 0) {
		# $adminRef returned a string, we'll just print it out.
		print "no (error response: $adminRef)\n";
	} else {
		print "yes\n";
	}
	return;
}

=head2 fetch_sid_data

 This subroutine is called up to fetch information on a per stream mentality.
 If we are able to connect to the shoutcast service and get the stats we will
 return 1 and a hashref of the de-coded xml information, otherwise we return 0
 so that we know that we have failed and can handle it gracefully.

=cut

sub fetch_sid_data {
	my ($sid) = (@_);
	my $url = 'http://'.$host.':'.$port.'/stats?sid='.$sid;
	my $response = $ua->get($url);
	if ($response->is_success) {
		my $returnRef = XMLin($response->decoded_content);
		return (1, $returnRef);	
	} else {
		return (0, $response->status_line);
	}
}

=head2 fetch_admin_data

 This subroutine is called up to fetch information from the admin page to get stream ids.
 This subroutine is also used to test that we can connect to the shoutcast service
 and if not we can fail gracefully. If we are able to connect to the shoutcast service
 and get the stats we will return 1 and a hashref of the de-coded xml information,
 otherwise we return 0 so that we know that we have failed and can handle it gracefully.

=cut

sub fetch_admin_data {
	my $url = 'http://'.$host.':'.$port.'/admin.cgi?sid=1&mode=viewxml&page=6';
	my $response = $ua->get($url);
	if ($response->is_success) {
		my $returnRef = XMLin($response->decoded_content);
		if (($returnRef->{STREAMCONFIGS}->{TOTALCONFIGS} > 0) && (defined($returnRef->{STREAMCONFIGS}->{STREAMCONFIG}))) {
			return (1, $returnRef);	
		} else {
			return (0, 'Unable to Detect any Stream Configurations');
		}
	} else {
		return (0, $response->status_line);
	}
}

# for Munin Plugin Gallery
# graph_category streaming
