# # The Serenade Audio plugin is a Slimserver plugin to expose Serenade # channels. It uses the PlayerAgentAccess COM object to get catalog # and channel information. It uses the SerenadeStreamer COM object to # play (via HTTP) and control (via COM) playback of channels. # # TODO: # - Expose Serenade Samples for browsing, addition, and playback. # - Copy protection (based on future Slimserver changes to allow # for protocol handlers). # - Skipping within a channel should work in Now Playing list # (based on future Slimserver changes to allow for protocol # handlers). package Plugins::AudioFeast::Plugin; use strict; use Config; use FindBin qw($Bin); use File::Spec::Functions qw(:ALL); sub getPluginDir { return catdir($Bin, "Plugins", "AudioFeast"); } use lib (catdir(getPluginDir(), "CPAN"), catdir(getPluginDir(), "CPAN", "arch", $Config::Config{archname})); use XML::Parser::Lite; use Time::Local; use Slim::Control::Command; use Slim::Utils::Strings qw (string); use Slim::Utils::Misc; use vars qw($VERSION); $VERSION = 0.7; my $debug = 1; # Print out logging/warning messages? my $playerAgent; # OLE Automation object to get agent information my $fatalError; # Unrecoverable error, if any my %catalogChannels; # Channels in complete catalog, keyed by UID my %catalogPrograms; # Programs in complete catalog, keyed by UID my $catalogLastUpdate = ""; # LastUpdate string from catalog my @myGroups = (); # Array of GUIDs representing My Channel groups my %myChannels = (); # Hash of arrays of channel GUIDs, keyed by group GUID. my $parser; # Cached XML parser my %modeContext; # Per-client menu state information my $urlbase; my $enabled = 1; my $streamer; my $streamerPort; #### # # Plugin initialization, shutdown, and callbacks. # #### sub initPlugin { if (!Slim::Utils::OSDetect::OS() eq 'win') { return; } $debug = Slim::Utils::Prefs::get("plugin_AudioFeast_debug") || 1; $streamerPort = Slim::Utils::Prefs::get("plugin_AudioFeast_port") || 9200; eval 'use Win32::Registry; use Win32::OLE; use Win32::OLE::Variant;'; my $s; if (!$::HKEY_CLASSES_ROOT->Open("SerenadeStreamer.Streamer", $s)) { my $streamerexe = catdir(getPluginDir(), "bin", "SerenadeStreamer.exe"); system($streamerexe, "/RegServer"); $debug && msg("Registering SerenadeStreamer server"); } $urlbase = 'http://localhost:' . $streamerPort . '/stream.wav'; # Initialize OLE using the default threading model. # We presume that anyone else doing initialization will choose # the same model. Win32::OLE->Initialize(); # Create an instance of the player agent $playerAgent = Win32::OLE->new("PlayerAccess.PlayerAgentAccess"); if (!$playerAgent) { $fatalError = 'PLUGIN_AUDIOFEAST_INSTALL_ERROR'; return; } # Get the singleton streaming server and tell it to start $streamer = Win32::OLE->new("SerenadeStreamer.Streamer"); if (!$playerAgent) { $fatalError = 'PLUGIN_AUDIOFEAST_INSTALL_ERROR'; return; } $streamer->Start($streamerPort, "SlimServer . $::VERSION"); $parser = Plugins::AudioFeast::Parser->new(); # Set up a callback so that we can listen for players commands # that may be relevant to us. Slim::Control::Command::setExecuteCallback(\&commandCallback); # Get catalog information. Since we don't know when it will change, # we'll do a polling refresh. fetchCatalog(); } sub shutdownPlugin { if (!Slim::Utils::OSDetect::OS() eq 'win') { return; } $streamer->Stop(); Win32::OLE->Uninitialize(); } sub commandCallback { my $client = shift; my $paramsRef = shift; if (!$playerAgent) { return; } # For any playlist methods that cause new tracks to be added, # check the new tracks to see if they are AudioFeast tracks (and # need their metadata updated) if ($paramsRef && $paramsRef->[0] eq 'playlist' && ($paramsRef->[1] eq 'play' || $paramsRef->[1] eq 'insert' || $paramsRef->[1] eq 'add' || $paramsRef->[1] eq 'load')) { $debug && msg("Checking playlist for AudioFeast tracks\n"); my $playlist = Slim::Player::Playlist::playList($client); for my $track (@$playlist) { if ($track =~ m|${urlbase}\?channel=(.*?)&|) { $debug && msg("Found a AudioFeast track. Providing metadata.\n"); my $guid = $1; my $cacheEntry = {}; $cacheEntry->{'CT'} = 'wav'; $cacheEntry->{'TITLE'} = $catalogChannels{$guid}->{name}; $cacheEntry->{'SECS'} = getChannelDuration($guid); Slim::Music::Info::updateCacheEntry($track, $cacheEntry); } } } } sub getStreamerPort { return $streamerPort; } sub getShowDebug { return $debug; } sub setupGroup { my %setupGroup = ( GroupHead => string('SETUP_GROUP_PLUGIN_AUDIOFEAST'), GroupDesc => string('SETUP_GROUP_PLUGIN_AUDIOFEAST_DESC'), GroupLine => 1, GroupSub => 1, PrefOrder => ['plugin_AudioFeast_port', 'plugin_AudioFeast_debug'], Suppress_PrefSub => 1, Suppress_PrefLine => 1 ); my %setupPrefs = ( 'plugin_AudioFeast_port' => { currentValue => sub { return getStreamerPort(); }, onChange => sub { my ($client,$changeref,$paramref,$pageref) = @_; $streamerPort = $changeref->{'plugin_AudioFeast_port'}{new}; Slim::Utils::Prefs::set('plugin_AudioFeast_port', $streamerPort); }, PrefSize => 'small', }, 'plugin_AudioFeast_debug' => { currentValue => sub { return getShowDebug(); }, onChange => sub { my ($client,$changeref,$paramref,$pageref) = @_; $debug = $changeref->{'plugin_AudioFeast_debug'}{new}; Slim::Utils::Prefs::set('plugin_AudioFeast_debug', $debug); }, options => { '0' => string('SETUP_GROUP_PLUGIN_AUDIOFEAST_NO'), '1' => string('SETUP_GROUP_PLUGIN_AUDIOFEAST_YES'), }, }, ); return (\%setupGroup,\%setupPrefs); } #### # # Catalog retrieval and parsing # #### sub parseChannelElement { my $element = shift; if (defined($element) && ref $element eq 'ARRAY') { my($name, $attrs, $children) = @$element; my $uid = $attrs->{UID}; $catalogChannels{$uid}->{name} = $attrs->{Name}; for my $child (@$children) { if ($child->[0] eq 'IncludeChannel') { push @{$catalogChannels{$uid}->{channels}}, $child->[1]->{UID}; } elsif ($child->[0] eq 'Program') { push @{$catalogChannels{$uid}->{programs}}, $child->[1]->{UID}; parseProgramElement($child, $attrs->{Name}); } } } } sub parseProgramElement { my $element = shift; my $channelName = shift; if (defined($element) && ref $element eq 'ARRAY') { my($name, $attrs, $children) = @$element; my $uid = $attrs->{UID}; $catalogPrograms{$uid}->{channelname} = $channelName; $catalogPrograms{$uid}->{name} = $attrs->{Name}; $catalogPrograms{$uid}->{timestamp} = $attrs->{TimeStamp}; $catalogPrograms{$uid}->{size} = $attrs->{VirtualSize}; $catalogPrograms{$uid}->{duration} = $attrs->{VirtualDuration}; } } sub fetchCatalog { my $c; eval '$c = Variant(VT_VARIANT | VT_BYREF, undef)'; $playerAgent->GetChannelCatalog($c, $catalogLastUpdate); if (my $hr = Win32::OLE->LastError()) { $debug && msg("Call to GetChannelCatalog failed with error $hr\n"); $fatalError = 'PLUGIN_AUDIOFEAST_CATALOG_ERROR'; } else { my $xml = $c->Value() || return; my $element = $parser->parse($xml); if (defined($element) && ref $element eq 'ARRAY') { my($name, $attrs, $children) = @$element; $catalogLastUpdate = $attrs->{LastUpdate}; foreach my $channel (@$children) { parseChannelElement($channel); } } } } sub fetchMyChannels { my $c; eval '$c = Variant(VT_VARIANT | VT_BYREF, undef)'; $playerAgent->GetMyChannelList($c); if (my $hr = Win32::OLE->LastError()) { $debug && msg("Call to GetMyChannels failed with error $hr\n"); $fatalError = 'PLUGIN_AUDIOFEAST_CATALOG_ERROR'; } else { my $xml = $c->Value() || return; my $current; @myGroups = (); $xml =~ s///; while ($xml =~ /<(.*?)channel(.*?)>/sg) { my $attrs = $2; my $end = $1; my ($uuid) = ($attrs =~ /GUID="(.*?)"/); if ($end) { $current = undef; } elsif ($current) { push @{$myChannels{$current}}, $uuid; } else { my ($name) = ($attrs =~ /NAME="(.*?)"/); push @myGroups, { name => $name, uuid => $uuid }; if ($attrs !~ /\/$/) { $current = $uuid; $myChannels{$current} = []; } } } } } sub channelHasAvailableContent { my $uid = shift; for my $program (@{$catalogChannels{$uid}->{programs}}) { if (programHasAvailableContent($program)) { return 1; } } return 0; } sub programHasAvailableContent { my $uid = shift; return 0 if !$catalogPrograms{$uid}->{size}; my ($ready, $total); eval '$ready = Variant(VT_I4|VT_BYREF, 0)'; eval '$total = Variant(VT_I4|VT_BYREF, 0)'; $playerAgent->GetProgramCompletionCount($uid, $ready, $total); return !Win32::OLE->LastError && ($ready != 0) && ($ready == $total); } sub getChannelDuration { my $uid = shift; my $duration = 0; for my $program (@{$catalogChannels{$uid}->{programs}}) { $duration += $catalogPrograms{$program}->{duration}; } return $duration; } #### # # Common methods for all modes # #### my %standard_functions = ( 'left' => \&standard_left, 'right' => \&standard_right, 'up' => \&standard_up, 'down' => \&standard_down, 'play' => \&standard_play, 'add' => \&standard_add, 'fwd' => \&standard_fwd, 'rew' => \&standard_rew, 'jump' => \&standard_jump, ); my %mode_info = ( "PLUGIN.AudioFeast::Plugin" => { title => 'PLUGIN_AUDIOFEAST', lookup_items => 0, lookup_title => 1, symbol => 'rightarrow', list => \&groupList, next => \&groupNext, playlist => \&groupPlaylist, lines => \&standard_lines, mode => \&standard_mode, functions => \%standard_functions, }, ); sub standard_mode { my $client = shift; my $ref = $modeContext{$client}; my $mode = Slim::Buttons::Common::mode($client); $ref->{$mode} ||= 0; my $mode_ref = $mode_info{$mode}; my $lines_func = $mode_ref->{lines}; $client->lines($lines_func); $client->update(); } sub standard_lines { if ($fatalError) { return (string($fatalError . "_1"), string($fatalError . "_2"), undef, undef); } my $client = shift; my $ref = $modeContext{$client}; my $mode = Slim::Buttons::Common::mode($client); my $mode_ref = $mode_info{$mode}; my $current = $ref->{$mode}; my $list; if (ref($mode_ref->{list}) eq 'CODE') { my $list_func = $mode_ref->{list}; $list = &$list_func($client); } else { $list = $mode_ref->{list}; } my $title; if (ref($mode_ref->{title}) eq 'CODE') { my $title_func = $mode_ref->{title}; $title = &$title_func($client); } else { $title = $mode_ref->{title}; } if ($mode_ref->{"lookup_title"}) { $title = string($mode_ref->{title}); } my $symbol; if (ref($mode_ref->{symbol}) eq 'CODE') { my $symbol_func = $mode_ref->{symbol}; $symbol = &$symbol_func($client); } else { $symbol = $mode_ref->{symbol}; } my $list_item = ""; if (!scalar(@$list)) { $list_item = string('PLUGIN_AUDIOFEAST_NONE'); } elsif ($current < scalar(@$list)) { $title .= ' (' . ($current + 1) . ' ' . string('OF') . ' ' . (scalar(@$list)) . ')'; $list_item = $list->[$current]; if ($mode_ref->{"lookup_items"}) { $list_item = string('PLUGIN_AUDIOFEAST_' . uc($list_item)); } } return ($title, $list_item, undef, Slim::Hardware::VFD::symbol($symbol)); } sub standard_left { my $client = shift; Slim::Buttons::Common::popModeRight($client); } sub standard_right { my $client = shift; my $ref = $modeContext{$client}; my $current_mode = Slim::Buttons::Common::mode($client); my $mode_ref = $mode_info{$current_mode}; my $current = $ref->{$current_mode}; my $nextfunc = $mode_ref->{next}; my $next; if (defined($nextfunc)) { $next = &$nextfunc($client, $current); } if ($next && Slim::Buttons::Common::validMode("PLUGIN.AudioFeast.$next")) { Slim::Buttons::Common::pushModeLeft($client, "PLUGIN.AudioFeast.$next"); } else { Slim::Display::Animation::bumpRight($client); } } sub standard_up { my $client = shift; my $ref = $modeContext{$client}; my $current_mode = Slim::Buttons::Common::mode($client); my $mode_ref = $mode_info{$current_mode}; my $current = $ref->{$current_mode}; my $list; if (ref($mode_ref->{list}) eq 'CODE') { my $list_func = $mode_ref->{list}; $list = &$list_func($client); } else { $list = $mode_ref->{list}; } $ref->{$current_mode} = Slim::Buttons::Common::scroll($client, -1, (scalar(@$list)), $current, ); $client->update(); } sub standard_down { my $client = shift; my $ref = $modeContext{$client}; my $current_mode = Slim::Buttons::Common::mode($client); my $mode_ref = $mode_info{$current_mode}; my $current = $ref->{$current_mode}; my $list; if (ref($mode_ref->{list}) eq 'CODE') { my $list_func = $mode_ref->{list}; $list = &$list_func($client); } else { $list = $mode_ref->{list}; } $ref->{$current_mode} = Slim::Buttons::Common::scroll($client, 1, (scalar(@$list)), $current, ); $client->update(); } sub standard_play { my $client = shift; my $button = shift; my $addorinsert = shift || 0; # For now, we know play or add my $ref = $modeContext{$client}; my $current_mode = Slim::Buttons::Common::mode($client); my $current = $ref->{$current_mode}; my $mode_ref = $mode_info{$current_mode}; my $playlist = []; my $playtitle; if (ref($mode_ref->{playlist}) eq 'CODE') { my $playlist_func = $mode_ref->{playlist}; ($playtitle, $playlist) = &$playlist_func($client, $current); } elsif (ref($mode_ref->{playlist}) eq 'ARRAY') { $playtitle = $mode_ref->{title}; $playlist = $mode_ref->{playlist}; } if (!$playlist || !scalar(@$playlist)) { return; } my $line1; my $shuffled = 0; if ($addorinsert == 1) { $line1 = string('ADDING_TO_PLAYLIST'); } elsif ($addorinsert == 2) { $line1 = string('INSERT_TO_PLAYLIST'); } else { if (Slim::Player::Playlist::shuffle($client)) { $line1 = string('PLAYING_RANDOMLY_FROM'); $shuffled = 1; } else { $line1 = string('NOW_PLAYING_FROM'); } } my $line2 = $playtitle; $client->showBriefly($client->renderOverlay($line1, $line2, undef, Slim::Hardware::VFD::symbol('notesymbol')),undef,1); if (!$addorinsert) { Slim::Control::Command::execute($client, ["playlist", "clear"]); Slim::Control::Command::execute($client, ["playlist", "shuffle" , 0]); } for my $uuid (@$playlist) { my $progUID = ''; for my $program (@{$catalogChannels{$uuid}->{programs}}) { $progUID .= $program . ','; } # Compose URL based on UUID my $url = $urlbase . '?channel=' . $uuid . '&progs=' . $progUID; Slim::Music::Info::setContentType($url, 'wav'); my $cacheEntry = {}; $cacheEntry->{'TITLE'} = $catalogChannels{$uuid}->{name}; $cacheEntry->{'SECS'} = getChannelDuration($uuid); $cacheEntry->{'VALID'} = 1; Slim::Music::Info::updateCacheEntry($url, $cacheEntry); Slim::Control::Command::execute($client, ["playlist", "append", $url]); } if (!$addorinsert) { if (defined($shuffled)) { Slim::Control::Command::execute($client, ["playlist", "shuffle" , $shuffled]); } Slim::Control::Command::execute($client, ["play"]); } } sub standard_add { my $client = shift; my $button = shift; standard_play($client, $button, 1); } sub standard_fwd { my $client = shift; $debug && msg("Forward button pressed\n"); my $track = Slim::Player::Playlist::song($client); if ($track =~ m|$urlbase| && $client->playmode() eq 'play') { $debug && msg("Current track is a AudioFeast track, so tell streamer to forward\n"); $streamer->Next($track); if (my $hr = Win32::OLE->LastError()) { $debug && msg("Error calling Next: $hr\n"); } } } sub standard_rew { my $client = shift; $debug && msg("Rewind button pressed\n"); my $track = Slim::Player::Playlist::song($client); if ($track =~ m|$urlbase| && $client->playmode() eq 'play') { $debug && msg("Current track is a AudioFeast track, so tell streamer to rewind\n"); $streamer->Previous($track); if (my $hr = Win32::OLE->LastError()) { $debug && msg("Error calling Previous: $hr\n"); } } } sub standard_jump { my $client = shift; my $funct = shift; my $functarg = shift; if ($functarg eq 'fwd') { standard_fwd($client); } elsif ($functarg eq 'rew') { standard_rew($client); } } #### # # Main mode methods # #### sub setMode { my $client = shift; # Refetch 'My Channels' list everytime we enter the top-level # plugin mode, since we don't get an event when it's updated. fetchMyChannels(); $modeContext{$client}->{initialized} = 1; standard_mode($client); } sub getFunctions { return \%standard_functions; } sub groupList { my @list = map { $_->{name} } @myGroups; return \@list; } sub groupNext { my ($client, $selection) = @_; my $uid = $myGroups[$selection]->{uuid}; my $name = $myGroups[$selection]->{name}; if (defined($catalogChannels{$uid}->{programs})) { return createChannelDetailsMode($uid, $name); } if (!Slim::Buttons::Common::validMode("PLUGIN.AudioFeast.$uid")) { $mode_info{"PLUGIN.AudioFeast.$uid"} = { title => $name, symbol => 'rightarrow', list => \&channelList, next => \&channelNext, playlist => \&channelPlaylist, lines => \&standard_lines, mode => \&standard_mode, functions => \%standard_functions, }; Slim::Buttons::Common::addMode("PLUGIN.AudioFeast.$uid", \%standard_functions, \&standard_mode); } return $uid; } sub groupPlaylist { my ($client, $selection) = @_; my $uid = $myGroups[$selection]->{uuid}; if (defined($catalogChannels{$uid}->{programs})) { if (channelHasAvailableContent($uid)) { return ($myGroups[$selection]->{name}, [$uid]); } $client->showBriefly( $client->renderOverlay($catalogChannels{$uid}->{name}, string('PLUGIN_AUDIOFEAST_CURRENTLY_DOWNLOADING'), undef, undef ,undef,1)); return; } my @list = grep { channelHasAvailableContent($_); } @{$myChannels{$uid}}; return ($myGroups[$selection]->{name}, \@list); } sub addMenu { my $menu = "RADIO"; return $menu; } sub getDisplayName { return 'PLUGIN_AUDIOFEAST'; } sub enabled { if (!$enabled && Slim::Utils::OSDetect::OS() eq 'win') { if (!eval 'use Win32::Registry;') { my $agent; if ($::HKEY_LOCAL_MACHINE->Open("Software\\AudioFeast" . "\\Content Agent", $agent)) { $enabled = 1; } } } return $enabled; } sub getChannelFromMode { my $client = shift; my $current_mode = Slim::Buttons::Common::mode($client); if ($current_mode =~ /^PLUGIN.AudioFeast\.(.*)$/) { return $1; } return undef; } sub channelList { my $client = shift; my $uid = getChannelFromMode($client); my @list = map { $catalogChannels{$_}->{name} } @{$myChannels{$uid}}; return \@list; } sub createChannelDetailsMode { my ($uid, $name) = @_; if (!Slim::Buttons::Common::validMode("PLUGIN.AudioFeast.$uid")) { $mode_info{"PLUGIN.AudioFeast.$uid"} = { title => $name, symbol => 'notesymbol', list => \&detailsList, playlist => \&detailsPlaylist, lines => \&standard_lines, mode => \&standard_mode, functions => \%standard_functions, }; Slim::Buttons::Common::addMode("PLUGIN.AudioFeast.$uid", \%standard_functions, \&standard_mode); } return $uid; } sub channelNext { my ($client, $selection) = @_; my $group = getChannelFromMode($client); my $uid = $myChannels{$group}->[$selection]; my $name = $catalogChannels{$uid}->{name}; return createChannelDetailsMode($uid, $name); } sub channelPlaylist { my ($client, $selection) = @_; my $group = getChannelFromMode($client); my $uid = $myChannels{$group}->[$selection]; if (channelHasAvailableContent($uid)) { return ($catalogChannels{$uid}->{name}, [$uid]); } $client->showBriefly( $client->renderOverlay($catalogChannels{$uid}->{name}, string('PLUGIN_AUDIOFEAST_CURRENTLY_DOWNLOADING'), undef, undef ,undef,1)); return []; } sub detailsList { my $client = shift; my $uid = getChannelFromMode($client); if (!channelHasAvailableContent($uid)) { return [string('PLUGIN_AUDIOFEAST_CURRENTLY_DOWNLOADING')]; } my $size = 0; my $duration = 0; my $last_epoch = 0; for my $program (@{$catalogChannels{$uid}->{programs}}) { $size += $catalogPrograms{$program}->{size}; $duration += $catalogPrograms{$program}->{duration}; my $timestamp = $catalogPrograms{$program}->{timestamp}; my ($day, $month, $year, $hour, $min, $sec) = ($timestamp =~ m|^(\d+)/(\d+)/(\d+) (\d+):(\d+):(\d+)$|); my $epoch = timelocal($sec, $min, $hour, $day, $month, $year); if ($epoch > $last_epoch) { $last_epoch = $epoch } } my ($s, $m, $h); my $duration_str; $s = $duration % 60; if ($duration = int($duration/60)) { $m = $duration % 60; if ($duration = int($duration/60)) { $duration_str = $duration . ':' . sprintf("%02u", $m) . ' ' . string('PLUGIN_AUDIOFEAST_HOURS'); } else { $duration_str = $m . ':' . sprintf("%02u", $s) . ' ' . string('PLUGIN_AUDIOFEAST_MINUTES'); } } else { $duration_str = $s . ' ' . string('PLUGIN_AUDIOFEAST_SECONDS'); } my @list = (); push @list, (string('PLUGIN_AUDIOFEAST_PLAY_TIME') . ': ' . $duration_str); push @list, (string('PLUGIN_AUDIOFEAST_UPDATED') . ': ' . Slim::Utils::Misc::shortDateF($last_epoch) . ' ' . Slim::Utils::Misc::timeF($last_epoch)); push @list, (string('PLUGIN_AUDIOFEAST_SIZE') . ': ' . sprintf("%.2f", ($size/(0x100000))) . ' ' . string('PLUGIN_AUDIOFEAST_MEGABYTES')); return \@list; } sub detailsPlaylist { my $client = shift; my $uid = getChannelFromMode($client); if (channelHasAvailableContent($uid)) { return ($catalogChannels{$uid}->{name}, [$uid]); } $client->showBriefly( $client->renderOverlay($catalogChannels{$uid}->{name}, string('PLUGIN_AUDIOFEAST_CURRENTLY_DOWNLOADING'), undef, undef ,undef,1)); return []; } sub strings { return " PLUGIN_AUDIOFEAST EN AudioFeast PLUGIN_AUDIOFEAST_NONE EN No Channels Available PLUGIN_AUDIOFEAST_INSTALL_ERROR_1 EN Couldn't find required components PLUGIN_AUDIOFEAST_INSTALL_ERROR_2 EN Please reinstall the AudioFeast application PLUGIN_AUDIOFEAST_CATALOG_ERROR_1 EN Couldn't get AudioFeast channel information PLUGIN_AUDIOFEAST_CATALOG_ERROR_2 EN Please restart AudioFeast PLUGIN_AUDIOFEAST_PLAY_TIME EN Play time PLUGIN_AUDIOFEAST_UPDATED EN Updated on PLUGIN_AUDIOFEAST_SIZE EN Size on disk PLUGIN_AUDIOFEAST_HOURS EN hours PLUGIN_AUDIOFEAST_MINUTES EN minutes PLUGIN_AUDIOFEAST_SECONDS EN seconds PLUGIN_AUDIOFEAST_MEGABYTES EN Mb PLUGIN_AUDIOFEAST_CURRENTLY_DOWNLOADING EN Currently downloading and refreshing SETUP_GROUP_PLUGIN_AUDIOFEAST EN AudioFeast SETUP_GROUP_PLUGIN_AUDIOFEAST_DESC EN AudioFeast is a plugin that allows you to browse and play channels downloaded for you by the AudioFeast application and content service. The following preference options control its behavior. To save your changes, click Change. SETUP_GROUP_PLUGIN_AUDIOFEAST_NO EN No SETUP_GROUP_PLUGIN_AUDIOFEAST_YES EN Yes SETUP_PLUGIN_AUDIOFEAST_PORT EN HTTP Port SETUP_PLUGIN_AUDIOFEAST_PORT_DESC EN Port to use for communication between the plugin and AudioFeast. Do not change this unless you are getting port conflicts with other applications. You will have to restart SlimServer for this preference to go into effect. SETUP_PLUGIN_AUDIOFEAST_DEBUG EN Show debugging information SETUP_PLUGIN_AUDIOFEAST_DEBUG_DESC EN Print debugging information out to the console. This can be used for diagnosing errors. "} 1; package Plugins::AudioFeast::Parser; use XML::Parser::Lite; # Parser code borrowed from SOAP::Lite. This package uses the # event-driven XML::Parser::Lite parser to construct a nested data # structure - a poor man's DOM. Each XML element in the data structure # is represented by an array ref, with the values (listed by subscript # below) corresponding with: # 0 - The element name. # 1 - A hash ref representing the element attributes. # 2 - An array ref holding either child elements or concatenated # character data. sub new { my $class = shift; return bless { _parser => XML::Parser::Lite->new }, $class; } sub parse { my $self = shift; my $parser = $self->{_parser}; $parser->setHandlers(Final => sub { shift; $self->final(@_) }, Start => sub { shift; $self->start(@_) }, End => sub { shift; $self->end(@_) }, Char => sub { shift; $self->char(@_) },); $parser->parse(shift); } sub final { my $self = shift; my $parser = $self->{_parser}; # clean handlers, otherwise ControlPoint::Parser won't be deleted: # it refers to XML::Parser which refers to subs from ControlPoint::Parser undef $self->{_values}; $parser->setHandlers(Final => undef, Start => undef, End => undef, Char => undef,); $self->{_done}; } sub start { push @{shift->{_values}}, [shift, {@_}] } sub char { push @{shift->{_values}->[-1]->[3]}, shift } sub end { my $self = shift; my $done = pop @{$self->{_values}}; $done->[2] = defined $done->[3] ? join('',@{$done->[3]}) : '' unless ref $done->[2]; undef $done->[3]; @{$self->{_values}} ? (push @{$self->{_values}->[-1]->[2]}, $done) : ($self->{_done} = $done); } 1; __END__ # Local Variables: # tab-width:4 # indent-tabs-mode:t # End: