# LastFM.pm # # Copyright (c) 2005,2006 James Craig (james.craig@london.com) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # package Plugins::LastFM::Plugin; use Slim::Utils::Strings qw(string); use Slim::Utils::Prefs; use Slim::Utils::Misc; use Slim::Control::Request; use Class::Struct; use Digest::MD5 qw(md5_hex); use Data::Dumper; use File::Spec::Functions qw(:ALL); #use warnings; #use strict; ## Global variables ### my @lastfm_commands = qw/skip love ban rtp/; #discovery gets added later my @lastfm_subscriber_options = qw/neighbours recommended personal loved/; #are there any others? my $lastfm_history_size = 5; # make this a pref? my $lastfm_timeout = 60; # how long between scheduled updates my $lastfm_command_timeout = 10; # how long to wait for an update after sending a command #my $lastfm_server_address = 'http://wsdev.audioscrobbler.com'; my $lastfm_server_address = 'http://ws.audioscrobbler.com'; my %players; my $cacheFolder; # SLIMSERVER API ################### use vars qw($VERSION); $VERSION = substr(q$Revision: 1.27 $,10); sub strings { return " PLUGIN_LASTFM_MODULE_NAME EN LastFM PLUGIN_LASTFM_CHOOSE_PLAYER EN Please choose a player and return to this page! ES Por favor, elegir un reproductor y regresar a esta página! PLUGIN_LASTFM_SETUP_LOGIN EN Please setup your LastFM login in SlimServer settings pages PLUGIN_LASTFM_CONNECTING EN Connecting to LastFM... ES Conectando a LastFM... PLUGIN_LASTFM_CONNECTION_ERROR EN Error connecting to LastFM! Check network connection and www.Last.fm status ES Error al conectarse a LastFM! Verfificar la conexión de red y el status de www.LastFM.com PLUGIN_LASTFM_SESSION_ERROR EN Error connecting to LastFM! Check username & password ES Error al conectarse a LastFM! Verificar el nombre de usuario & contraseña PLUGIN_LASTFM_UPDATING EN Updating LastFM... ES Actualizando LastFm... PLUGIN_LASTFM_UPDATE_FAILED EN Error sending update to LastFM! ES Error al enviar la actualización a LastFM! PLUGIN_LASTFM_WARNING EN Warning: LastFM error detected. PLUGIN_LASTFM_PRESS_PLAY EN Press play to start LastFM... ES Presionar play para iniciar LastFM... PLUGIN_LASTFM_WAIT EN Waiting for LastFM track details... ES Esperando detalles de la pista de LastFM... PLUGIN_LASTFM_UPDATE_SUCCESS EN Update sent to LastFM! ES Actualización enviada a LastFM! PLUGIN_LASTFM_SKIP EN SKIP current track ES SALTEAR pista actual PLUGIN_LASTFM_LOVE EN LOVE current track ES AMAR pista actual PLUGIN_LASTFM_BAN EN BAN current track ES PROHIBIR la pista actual PLUGIN_LASTFM_PERSONAL EN Personal PLUGIN_LASTFM_DISCOVERY EN Discovery Mode PLUGIN_LASTFM_NEIGHBOURS EN Neighbours ES Vecinos PLUGIN_LASTFM_LOVED EN Loved ES Amada PLUGIN_LASTFM_RTP EN Record to Profile PLUGIN_LASTFM_RANDOM EN Random ES Al Azar PLUGIN_LASTFM_SIMILAR_ARTISTS EN Similar Artists To ES Artistas Similares A PLUGIN_LASTFM_STATION EN Station ES Estación PLUGIN_LASTFM_CHANGE_STATION EN Change LastFM Station ES Cambiar la Estación LastFM SETUP_GROUP_PLUGIN_LASTFM EN LastFM Internet Radio ES Radio por Internet LastFM SETUP_GROUP_PLUGIN_LASTFM_DESC EN Listen to and control the LastFM internet radio station ES Escuchar y controlar la estación de radio por internet de LastFM SETUP_PLUGIN_LASTFM_PASSWORD_CHANGED EN Your LastFM password has been changed ES Se ha cambiado la contraseña para LastFM SETUP_PLUGIN_LASTFM_PASSWORD_CHOOSE EN LastFM Password ES Contraseña de LastFM SETUP_PLUGIN_LASTFM_USERNAME_CHOOSE EN LastFM Username ES Nombre de Usuario de LastFM SETUP_PLUGIN_LASTFM_ALBUMSIZE_CHOOSE EN LastFM Album art size ES Tamaño del Arte de Tapa de LastFM SETUP_PLUGIN_LASTFM_STATIONS_DESC EN
Enter your favourite LastFM Stations

e.g. group/groupname for \"group\" radio,
user/username for \"user\" radio,
globaltags/tagname for \"tag\" radio,
artist/artistname/similarartists for \"similar artist\" radio.
prefix the station name with \"nickname;\" for a display-friendly nickname. SETUP_PLUGIN_LASTFM_STATIONS_CHOOSE EN Station - ES Estación - SETUP_PLUGIN_LASTFM_STATIONS_CHANGED EN LastFM Stations changed ES Estaciones de LastFM cambiadas SETUP_PLUGIN_LASTFM_USESCROBBLER_CHOOSE EN Use login details from SlimScrobbler plugin ES Utilizar los detalles de conexión del plugin SlimScrobbler SETUP_PLUGIN_LASTFM_USESCROBBLER_CHANGED EN Use SlimScrobbler login setting changed PLUGIN_LASTFM_DUMMY EN \" \" ";} sub addMenu { my $menu = "RADIO"; return $menu; } sub enabled { return ($::VERSION ge '6.5'); } sub getDisplayName { return 'PLUGIN_LASTFM_MODULE_NAME'; } sub initPlugin { # web setup doesn't seem to work if array pref is empty if (! Slim::Utils::Prefs::getArray("plugin_lastfm_stations")) { Slim::Utils::Prefs::set('plugin_lastfm_stations', ['group/SlimScrobbler']); } if (! Slim::Utils::Prefs::get('plugin_lastfm_albumsize')) { Slim::Utils::Prefs::set('plugin_lastfm_albumsize','albumcover_small'); } # if this is run for the first time if (! defined Slim::Utils::Prefs::get('plugin_lastfm_usescrobbler')) { # if there's a SlimScrobbler pref set and the plugin is enabled... if (grep {$_ eq 'SlimScrobbler::Plugin'} Slim::Utils::PluginManager::enabledPlugins()) { Slim::Utils::Prefs::set('plugin_lastfm_usescrobbler',1); } else { Slim::Utils::Prefs::set('plugin_lastfm_usescrobbler',0); } } Slim::Control::Request::addDispatch(['play_lastFM_station'], [1, 0, 0, \&Plugins::LastFM::Plugin::playLastFMCommand]); Slim::Player::ProtocolHandlers->registerHandler('lastfm', qw/Slim::Player::Protocols::HTTP/); #$cacheFolder = initCacheFolder(); lastFMmsg("Plugin initialised CVS ($VERSION)\n"); } sub setupGroup { my @prefOrder = ( 'plugin_lastfm_albumsize', 'plugin_lastfm_stations', ); if (grep {$_ eq 'SlimScrobbler::Plugin'} Slim::Utils::PluginManager::enabledPlugins()) { unshift @prefOrder, 'plugin_lastfm_usescrobbler'; } if (!Slim::Utils::Prefs::get('plugin_lastfm_usescrobbler')) { unshift @prefOrder, 'plugin_lastfm_password'; unshift @prefOrder, 'plugin_lastfm_username'; } my %Group = ( PrefOrder => \@prefOrder, GroupHead => string( 'SETUP_GROUP_PLUGIN_LASTFM' ), GroupDesc => string( 'SETUP_GROUP_PLUGIN_LASTFM_DESC' ), GroupLine => 1, GroupSub => 1, Suppress_PrefSub => 1, Suppress_PrefLine => 0, Suppress_PrefHead => 1, ); my %Prefs = ( plugin_lastfm_username => { }, plugin_lastfm_password => { 'onChange' => sub { my $encoded = md5_hex($_[1]->{plugin_lastfm_password}->{new} ); Slim::Utils::Prefs::set( 'plugin_lastfm_password', $encoded ); refreshSessions(); } ,'inputTemplate' => 'setup_input_passwd.html' ,'changeMsg' => string('SETUP_PLUGIN_LASTFM_PASSWORD_CHANGED') }, plugin_lastfm_stations => { 'isArray' => 1 ,'arrayAddExtra' => 1 ,'arrayDeleteNull' => 1 ,'arrayDeleteValue' => '' ,'arrayBasicValue' => 0 ,'PrefSize' => 'large' ,'inputTemplate' => 'setup_input_array_txt.html' ,'PrefInTable' => 1 ,'showTextExtValue' => 0 ,'onChange' => sub { my ($client,$changeref,$paramref,$pageref) = @_; if (exists($changeref->{'plugin_lastfm_stations'}{'Processed'})) { return; } Slim::Web::Setup::processArrayChange($client, 'plugin_lastfm_stations', $paramref, $pageref); $changeref->{'plugin_lastfm_stations'}{'Processed'} = 1; } ,'changeMsg' => string('SETUP_PLUGIN_LASTFM_STATIONS_CHANGED') }, plugin_lastfm_albumsize => { 'options' => { none => 'none', albumcover_small => 'small', albumcover_medium => 'medium', albumcover_large => 'large', }, 'optionSort' => 'V', }, 'plugin_lastfm_usescrobbler' => { 'validate' => \&Slim::Utils::Validate::trueFalse , 'options' => { '1' => string('ON'), '0' => string('OFF') } } ); return( \%Group, \%Prefs ); } ## WEB ####################### sub webPages { my %pages = ( "index\.htm" => \&handleWebIndex, "help\.htm" => \&handleWebHelp ); if (grep {$_ eq 'LastFM::Plugin'} Slim::Utils::PluginManager::enabledPlugins()) { Slim::Web::Pages->addPageLinks("radio", { 'PLUGIN_LASTFM_MODULE_NAME' => "plugins/LastFM/index.html" }); } else { Slim::Web::Pages->addPageLinks("radio", { 'PLUGIN_LASTFM_MODULE_NAME' => undef }); } return (\%pages); } sub handleWebHelp { my ($client, $params) = @_; return Slim::Web::HTTP::filltemplatefile('plugins/LastFM/help.html', $params); } sub handleWebIndex { my ($client, $params) = @_; #print Dumper @_; # without a player, don't do anything if ($client = Slim::Player::Client::getClient($params->{player})) { my $macaddress = getPlayerKey($client); if (defined $params->{lastfmp1}) { if ( $params->{lastfm} eq 'delete' ) { deleteStation($params->{lastfmp1}); } elsif ( $params->{lastfm} eq 'addlfm' or defined $params->{"addlfm.x"}) { addStation($params->{lastfmp1}); } elsif ($params->{lastfm} eq 'playlfm' or defined $params->{"playlfm.x"}) { playLastFM($client,$params->{lastfmp1}); } elsif (!$params->{lastfm}) { #user hit enter in ie! #firefox submits add on enter addStation($params->{lastfmp1}); } } if (getLastFMStatus($client)) { if (defined $params->{lastfm}) { if ( $params->{lastfm} eq 'command') { commandLastFM($params->{player},$params->{lastfmp1}); } } #possible commands $params->{commands} = $players{$macaddress}->commands; $params->{rtp} = $players{$macaddress}->rtp; $params->{disco} = $players{$macaddress}->disco; # show any track data we may have my %data = %{$players{$macaddress}->datahash}; @$params{keys %data} = values %data; #add the selected album art size #web page status is keyed off the presence of this field $params->{albumart} = $data{Slim::Utils::Prefs::get("plugin_lastfm_albumsize")}; } #status flag, if any if (my $status = $players{$macaddress}->status) { $params->{status} = $status; } #split out the aliases my @stationShortNameList; my @stationRealNameList; for ( genStationNames($client)) { my ($nickname) = m/^(.*?);/; $nickname = $_ unless $nickname; push @stationShortNameList, $nickname; push @stationRealNameList, $_; } $params->{stationnames} = \@stationShortNameList; $params->{stationlinks} = \@stationRealNameList; # add the recently played history $params->{history} = $players{$macaddress}->history; # add the user' name my ($user) = getUserDetails($client); $params->{user} = $user; # calculate refresh interval my $check_time = time(); my $elapsed = $check_time - $players{$macaddress}->last_check_time; $params->{refresh} = ($players{$macaddress}->remaining - $elapsed) + 5; } $params->{refresh} = $lastfm_timeout unless ($params->{refresh} and $params->{refresh} > 5); lastFMmsg("web page refresh: ".$params->{refresh}."\n"); return Slim::Web::HTTP::filltemplatefile('plugins/LastFM/index.html', $params); } # Main mode ################################################################ my %mainModeFunctions = ( 'left' => sub { my $client = shift; #blank screen 2 on exit $client->update({screen2 => {}}); Slim::Buttons::Common::popModeRight($client); }, 'right' => sub { my $client = shift; action($client,'right'); }, ,'knob' => sub { my ($client,$funct,$functarg) = @_; changePos($client,$client->knobPos() - $players{$client->macaddress}->listitem); }, 'down' => sub { my $client = shift; my $button = shift; if ($button !~ /repeat/) { changePos($client, 1); } }, 'up' => sub { my $client = shift; my $button = shift; if ($button !~ /repeat/) { changePos($client, -1); } }, 'play' => sub { my $client = shift; action($client,'play'); }, # direct function access for IR map 'lastfm_skip' => sub { my $client = shift; if (getLastFMStatus($client)) { commandLastFM($client->macaddress, 'skip'); } }, 'lastfm_love' => sub { my $client = shift; if (getLastFMStatus($client)) { commandLastFM($client->macaddress, 'love'); } }, 'lastfm_ban' => sub { my $client = shift; if (getLastFMStatus($client)) { commandLastFM($client->macaddress, 'ban'); } }, ); sub changePos { my $client = shift; my $count = shift; if (getLastFMStatus($client)) { $players{$client->macaddress}->listitem($players{$client->macaddress}->listitem + $count); if ($count < 0) { $client->pushUp(); } else { $client->pushDown(); } } else { if ($count > 0) { $client->bumpUp(); } else { $client->bumpDown(); } } } sub getFunctions { return \%mainModeFunctions; } sub action { my $client = shift; my $button = shift; # connected if (getLastFMStatus($client)) { my $player = $players{$client->macaddress}; my $pos = $player->listitem; $pos++ if ($client->display->isa('Slim::Display::Transporter')); # choose station my @commands = @{$player->commands}; if ($pos > @commands) { chooseStations($client); # commands } elsif ($pos != 0 ) { $client->showBriefly( $client->string('PLUGIN_LASTFM_MODULE_NAME'), $client->string('PLUGIN_LASTFM_UPDATING'), 2); commandLastFM($client->macaddress, $commands[$pos-1]); $player->listitem(0); $client->update(); # 0 - INFO line } else { if ($button eq 'play' and $player->status eq 'WARN') { playLastFM($client); } elsif ($button eq 'right') { # push on another mode displaying the track/station info my $data = $player->datahash; my @infoStringList = ( $client->string('PLUGIN_LASTFM_STATION').': '.$data->{station}, $client->string('TRACK') .': '.$data->{track}, $client->string('ARTIST').': '.$data->{artist}, $client->string('ALBUM') .': '.$data->{album}, $client->string('LENGTH') .': '.$data->{trackduration}, 'Feed: '.$data->{stationfeed}, ); my %params = ( header => '{PLUGIN_LASTFM_MODULE_NAME} {count}', listRef => \@infoStringList, parentMode => Slim::Buttons::Common::mode($client), ); Slim::Buttons::Common::pushModeLeft($client,'INPUT.Choice',\%params); } else { $client->bumpRight($client); } } # not connected } else { if ($button eq 'play') { playLastFM($client); } else { chooseStations($client); } } } sub setMode() { my $client = shift; my $method = shift; if ($method eq 'pop' and $context{$client}->{blocking}) { return; } $client->lines(\&lines); $client->param('screen2', 'LastFM'); } sub lines { my $client = shift; my ($line1, $line2, $overlay1, $overlay2); my ($station, $track); $line1 = $client->string('PLUGIN_LASTFM_MODULE_NAME'); if (getLastFMStatus($client)) { #set up the array of possible menu items my @items = (); push @items,"Info" unless($client->display->isa('Slim::Display::Transporter')); my @commands = @{$players{$client->macaddress}->commands}; for (@commands) { push @items, $client->string('PLUGIN_LASTFM_'.(uc $_)); } push @items, $client->string('PLUGIN_LASTFM_CHANGE_STATION'); $players{$client->macaddress}->listitem($players{$client->macaddress}->listitem % scalar(@items)); $line1 .= " (".($players{$client->macaddress}->listitem + 1)." OF ".scalar(@items).")"; if ($players{$client->macaddress}->status eq 'WARN') { $overlay1 = $client->string('PLUGIN_LASTFM_WARNING'); } else { $station = $players{$client->macaddress}->station; #$infooverlay1 .=" : ".$players{$client->macaddress}->stationfeed; $overlay1 = $station unless ($client->display->isa('Slim::Display::Transporter')); } $line2 = $items[$players{$client->macaddress}->listitem]; $overlay2 = $client->symbols('rightarrow'); $track = Slim::Music::Info::getCurrentTitle($client,$players{$client->macaddress}->stream_url); $time = $players{$client->macaddress}->duration." s"; if ($line2 eq 'Info' ) { $line2 = $track; } elsif ($line2 eq $client->string('PLUGIN_LASTFM_RTP')) { $overlay2 = Slim::Buttons::Common::checkBoxOverlay($client,$players{$client->macaddress}->rtp); } elsif ($line2 eq $client->string('PLUGIN_LASTFM_DISCOVERY')) { $overlay2 = Slim::Buttons::Common::checkBoxOverlay($client,$players{$client->macaddress}->disco); } } else { if ($players{$client->macaddress}->status eq 'NO_LOGIN' ) { $line2 = $client->string('PLUGIN_LASTFM_SETUP_LOGIN') } elsif ($players{$client->macaddress}->status eq 'FAILEDCONNECT' ) { $line2 = $client->string('PLUGIN_LASTFM_CONNECTION_ERROR') } elsif ($players{$client->macaddress}->status eq 'FAILEDLOGIN' ) { $line2 = $client->string('PLUGIN_LASTFM_SESSION_ERROR') } elsif ($players{$client->macaddress}->status eq 'STARTING' ) { $line2 = $client->string('PLUGIN_LASTFM_WAIT'); } elsif ($players{$client->macaddress}->status eq 'WARN') { $line2 = $client->string('PLUGIN_LASTFM_WARNING'); } else { $line2 = $client->string('PLUGIN_LASTFM_PRESS_PLAY'); $overlay2 = $client->symbols('rightarrow'); } } return {screen1 => { line => [$line1, $line2], overlay => [$overlay1, $overlay2], }, screen2 => { line => [$station, $track], overlay => [$time,$client->symbols('notesymbol')], } }; } #### STATION MODE - for the selection of stations sub chooseStations { my $client = shift; #split out the aliases my @stationShortNameList; my @stationRealNameList; for ( genStationNamesLevel1($client)) { my ($nickname) = m/^(.*?);/; $nickname = $_ unless $nickname; push @stationShortNameList, $client->string('PLAY')." $nickname"; push @stationRealNameList, $_; } my %params = ( header => '{PLUGIN_LASTFM_CHANGE_STATION} {count}', listRef => \@stationShortNameList, callback => \&stationModeCallback, onPlay => sub { my $client = shift; stationModeCallback($client,'RIGHT'); }, #valueRef => \$context{$client}->{stationModeIndex}, parentMode => Slim::Buttons::Common::mode($client), overlayRef => [undef,$client->symbols('rightarrow')], stations => \@stationRealNameList, screen2 => 'LastFM', ); Slim::Buttons::Common::pushModeLeft($client,'INPUT.Choice',\%params); } sub stationModeCallback { my ($client,$exittype) = @_; $exittype = uc($exittype); lastFMmsg("Station mode: $exittype\n"); if ($exittype eq 'LEFT') { Slim::Buttons::Common::popModeRight($client); } elsif ($exittype eq 'RIGHT') { my $listIndex = Slim::Buttons::Common::param($client, 'listIndex'); my $items = Slim::Buttons::Common::param($client, 'stations'); #split out the aliases my @stationShortNameList; my @stationRealNameList; for (genStationNames($client,$items->[$listIndex])) { my ($nickname) = m/^(.*?);/; $nickname = $_ unless $nickname; push @stationShortNameList, $client->string('PLAY')." $nickname"; push @stationRealNameList, $_; } if (@stationRealNameList> 1) { my %params = ( header => '{PLUGIN_LASTFM_CHANGE_STATION} {count}', listRef => \@stationShortNameList, callback => \&stationTypeCallback, onPlay => sub { my $client = shift; stationTypeCallback($client,'RIGHT'); }, #valueRef => \$context{$client}->{stationTypeIndex}, parentMode => Slim::Buttons::Common::mode($client), overlayRef => [undef,$client->symbols('rightarrow')], stations => \@stationRealNameList, screen2 => 'LastFM', ); Slim::Buttons::Common::pushModeLeft($client,'INPUT.Choice',\%params); } else { lastFMmsg("Playing $stationRealNameList[0]\n"); playLastFM($client, $stationRealNameList[0]); $players{$client->macaddress}->listitem(0); Slim::Buttons::Common::popMode($client); $client->update(); } } else { $client->bumpRight(); } } #### STATION TYPE MODE - for the selection of station subtypes sub stationTypeCallback { my ($client,$exittype) = @_; $exittype = uc($exittype); lastFMmsg("Station type: $exittype\n"); if ($exittype eq 'LEFT') { Slim::Buttons::Common::popModeRight($client); } elsif ($exittype eq 'RIGHT') { my $listIndex = Slim::Buttons::Common::param($client, 'listIndex'); my $items = Slim::Buttons::Common::param($client, 'stations'); my $selectedStation = $items->[$listIndex]; playLastFM($client, $selectedStation); $players{$client->macaddress}->listitem(0); Slim::Buttons::Common::popMode($client); Slim::Buttons::Common::popMode($client); $client->update(); } else { $client->bumpRight(); } } ### start of actual code struct LastFMStatus => { session => '$', #lastfm session id stream_url => '$', #stream url streaming => '$', #1/0 - stream status from LastFM rtp => '$', #1/0 - record to profile status disco => '$', #1/0 - discovery status subscriber => '$', #1/0 - true/false station => '$', #station name stationfeed => '$', #station feed name artist => '$', #artist name last_check_time => '$', # duration => '$', #length of current track progress => '$', #position in current track remaining => '$', #time remaining in current track listitem => '$', #position in top-level menu base_url => '$', #lastfm streaming server address history => '$', #array - recently played tracks status => '$', #our status datahash => '$', #hashref - everything we get from LastFM in an update goes here commands => '$', #arrayref of commands }; sub getLastFMStatus { my $client = shift; my $player=createPlayer(getPlayerKey($client)); # definitely not streaming if this is true - timer may not have been called yet so don't change status if (Slim::Player::Source::playmode($client) eq "stop") { $player->streaming(0); } return $player->streaming; } sub updateLastFMStatus { my $client = shift; my $player=createPlayer(getPlayerKey($client)); # update the last checked time and remaining time my $check_time = time(); my $elapsed = $check_time - $player->last_check_time; $player->last_check_time($check_time); $player->remaining($player->remaining - $elapsed); $player->remaining($lastfm_timeout) if ($player->remaining > $lastfm_timeout or $player->remaining < 1); #lastfm have stopped sending progress so let's try and guess it! $player->progress($player->progress + $elapsed); # we're playing (probably) so connect to LastFM for an update if (Slim::Player::Source::playmode($client) ne "stop" and defined $player->stream_url and Slim::Player::Playlist::song($client)->url eq $player->stream_url) { my $lastfm_url = $player->base_url."/np.php?session=".$player->session; lastFMmsg("Connecting to $lastfm_url\n"); if (my $http = Slim::Networking::SimpleAsyncHTTP->new( \&gotLastFMUpdate, \&failedLastFMUpdate, {client => $client} )) { $http->get($lastfm_url); } else { lastFMmsg("Error creating SimpleAsyncHTTP!\n"); } return 1; } else { #lastFMmsg("Not Connecting to LastFM - playMode:".Slim::Player::Source::playmode($client) # ." playing: ".Slim::Player::Playlist::song($client)->url."\n"); } # we're not playing if we got here, but set the status & timer for next round $player->streaming(0); $player->status(undef); setLastFMTimer($client); } sub gotLastFMUpdate { my $http = shift; my $client = $http->params('client'); my $macaddress = getPlayerKey($client); my $lastFMData = $http->content(); $http->close(); my %data = parseLastFMData($lastFMData); $players{$macaddress}->streaming( 0 ); $players{$macaddress}->datahash(\%data); if ($data{ERROR} and $data{ERROR} =~ /Invalid Session/) { lastFMmsg("Invalid Session: resetting session/stream!\n"); $players{$macaddress}->session(undef); $players{$macaddress}->stream_url(undef); } elsif ($data{streaming} eq 'true') { $players{$macaddress}->streaming( 1 ); $players{$macaddress}->status(undef); $players{$macaddress}->station( $data{station}); $players{$macaddress}->stationfeed( $data{stationfeed}); $players{$macaddress}->duration( $data{trackduration}); $players{$macaddress}->progress( $data{trackprogress}) if (defined $data{trackprogress}); $players{$macaddress}->artist( $data{artist}); $players{$macaddress}->rtp( $data{recordtoprofile}); $players{$macaddress}->disco( $data{discovery}); my $title = "$data{track} ".$client->string('BY')." $data{artist} ".$client->string('FROM')." $data{album}"; if ($title ne Slim::Music::Info::getCurrentTitle($client,$players{$macaddress}->stream_url)) { if (Slim::Music::Info::getCurrentTitle($client,$players{$macaddress}->stream_url) ne $players{$macaddress}->stream_url and Slim::Music::Info::getCurrentTitle($client,$players{$macaddress}->stream_url) ne $client->string('PLUGIN_LASTFM_MODULE_NAME')) { # add the current track to history my @history = @{$players{$macaddress}->history}; unshift @history,Slim::Music::Info::getCurrentTitle($client,$players{$macaddress}->stream_url); pop @history if (@history > $lastfm_history_size); $players{$macaddress}->history(\@history); } # update the current track Slim::Music::Info::setCurrentTitle($players{$macaddress}->stream_url,$title); # note the newTrack if it is new # we include this in an eval block so it fails silently # if SlimScrobbler is not installed eval { Plugins::SlimScrobbler::Plugin::noteNewTrack($data{artist},$data{track}); }; #lastfm have stopped sending progress so let's reset it on a new track $players{$macaddress}->progress(0); #download the artwork #my $artFile = saveArtworkURLToCache($macaddress,$data{albumcover_large}); } # set the remaining time if there's a duration available if (defined $players{$macaddress}->duration) { lastFMmsg("Progress: ". $players{$macaddress}->progress()."\n"); $players{$macaddress}->remaining( ($players{$macaddress}->duration - $players{$macaddress}->progress) + 10); if ($players{$macaddress}->progress > $players{$macaddress}->duration ) { $players{$macaddress}->status('WARN'); lastFMmsg("Track Overrun!\n"); } #if ($players{$macaddress}->progress < 0) { # $players{$macaddress}->status('WARN'); # lastFMmsg("Track underrun!"); #} } $client->update(); } # timeout if we're not streaming $players{$macaddress}->remaining($lastfm_timeout) unless ($players{$macaddress}->streaming); $players{$macaddress}->remaining($lastfm_timeout) if ($players{$macaddress}->remaining > $lastfm_timeout or $players{$macaddress}->remaining < 0) ; # refresh the info in n seconds setLastFMTimer($client); return $players{$macaddress}->streaming; } sub failedLastFMUpdate { my $http = shift; lastFMmsg ("Connection failed!\n"); if (my $client = $http->params('client') || Slim::Player::Client::getClient($http->params('playername'))) { my $macaddress = getPlayerKey($client); $players{$macaddress}->status('WARN'); $client->showBriefly( $client->string('PLUGIN_LASTFM_MODULE_NAME'), $client->string('PLUGIN_LASTFM_UPDATE_FAILED'), 2); # refresh the info in n seconds setLastFMTimer($client); } } sub commandLastFM { my $playername = shift; my $command = shift; my $page = 'control.php'; # discovery mode not compatible with personal radio if ($command eq 'discovery') { if ($players{$playername}->station !~ m/personal/i) { $command = 'settings/discovery/'; if ($players{$playername}->disco == 1) { $command .= 'off'; $players{$playername}->disco(0); } else { $command .= 'on'; $players{$playername}->disco(1); } switchStation($playername,$command); } } else { if ($command eq 'rtp') { #toggle rtp/nortp #also temporarily change values for correct user feedback #(these will be overwritten by the scheduled update) if ($players{$playername}->rtp) { $command = 'nortp'; $players{$playername}->rtp(0); } else { $players{$playername}->rtp(1); } } postToLastFM($playername,$page, {command => $command} ); } } sub postToLastFM { my $playername = shift; my $page = shift; my $params = shift; my $lastfm_url = $players{$playername}->base_url."/$page"; my $post = "session=".$players{$playername}->session; for (keys %{$params}) { $post .= "\&$_=".Slim::Utils::Misc::escape($params->{$_}); } lastFMmsg("Sending $lastfm_url\?$post\n"); my $http = Slim::Networking::SimpleAsyncHTTP->new( \&gotLastFMControl, \&failedLastFMUpdate, {playername => $playername} ); $http->get("$lastfm_url\?$post"); } sub gotLastFMControl { my $http = shift; my $client = Slim::Player::Client::getClient($http->params('playername')); my $lastFMData = $http->content(); $http->close(); lastFMmsg("$lastFMData\n"); if ($lastFMData =~ /FAILED/) { if ($client) { $client->showBriefly( $client->string('PLUGIN_LASTFM_MODULE_NAME'), $client->string('PLUGIN_LASTFM_UPDATE_FAILED'), 2); } return 0; } if ($client) { $client->showBriefly( $client->string('PLUGIN_LASTFM_MODULE_NAME'), $client->string('PLUGIN_LASTFM_UPDATE_SUCCESS'), 2); } # refresh the info in n seconds setLastFMTimer($client,$lastfm_command_timeout); return 1; } sub getLastFMStreamURL { my $client = shift; my $force = shift; my $player = createPlayer(getPlayerKey($client)); my ($lastfm_user, $lastfm_password_md5sum) = getUserDetails($client,1); return 0 unless (defined $lastfm_user and defined $lastfm_password_md5sum); return 1 if ($player->stream_url and !$force); $player->status(undef); my $lastfm_url = "$lastfm_server_address/radio/handshake.php?version=1.0.1&platform=slim&username=$lastfm_user&passwordmd5=$lastfm_password_md5sum"; lastFMmsg("Connecting to: $lastfm_url\n"); # we don't use async here, because when we exit, I want to be sure the new session url has been populated my $http = Slim::Player::Protocols::HTTP->new({ 'url' => $lastfm_url, 'create' => 0, }); if (defined $http) { my $lastFMData = $http->content(); $http->close(); my %data = parseLastFMData($lastFMData); $player->session($data{session}); if ($lastFMData =~ /FAILED/) { $player->status('FAILEDLOGIN'); return 0; } #need to do this as SS will remove the port if 80, and we need to match with its url my ($server, $port, $path, $user, $password) = Slim::Utils::Misc::crackURL($data{stream_url}); my $host = $port == 80 ? $server : "$server:$port"; #use custom lastfm: handler $player->stream_url("lastfm://$host$path"); $player->base_url("http://$data{base_url}$data{base_path}"); $player->remaining($lastfm_timeout/2); $player->last_check_time(time()); $player->subscriber($data{subscriber}); if ($data{subscriber} == 1) { $player->commands([@lastfm_commands,'discovery']); } else { $player->commands([@lastfm_commands]); } return 1; } lastFMmsg("Handshake connection failed!\n"); $player->status('FAILEDCONNECT'); $client->update(); return 0; } sub playLastFMCommand { my $request = shift; # get the parameters my $client = $request->client(); my $station = $request->getParam('_p1'); $::d_commands && msg("LastFM::playLastFMCommand($client,$station)\n"); playLastFM($client,$station); } sub playLastFM { my $client = shift; my $station = shift; lastFMmsg("playing $client $station\n"); my $macaddress = getPlayerKey($client); if (!getLastFMStatus($client)) { if (refreshSessions($macaddress)) { setLastFMTimer($client); $players{$macaddress}->status('STARTING'); Slim::Music::Info::setTitle($players{$macaddress}->stream_url, $client->string('PLUGIN_LASTFM_MODULE_NAME') ); $client->execute(['playlist','play', $players{$macaddress}->stream_url]); $client->update(); setLastFMArt($macaddress); } else { return 0; } } if ($station and $station ne '') { switchStation($macaddress,$station); } } sub refreshSessions { my $player = shift; my $status = 0; for ($player or keys %players) { my $client = Slim::Player::Client::getClient($_); $status = getLastFMStreamURL($client,1) if $client; } # return the true status if only 1 player return $status if ($player); # otherwise always success return 1; } ## utility functions ############## sub getPlayerKey { my $client = shift; return $client->macaddress || $client->ip; } sub getUserDetails{ my $client = shift; my $want_password = shift; my ($lastfm_user, $lastfm_password); if (Slim::Utils::Prefs::get('plugin_lastfm_usescrobbler')) { eval { ($lastfm_user,$lastfm_password) = Plugins::SlimScrobbler::Plugin::getUserIDPasswordForClient($client); # only encrypt the password if we wanted it - does md5_hex cause performance hit? $lastfm_password = md5_hex($lastfm_password) if (defined $want_password); } } # try ours as well if we didn't get anything from scrobbler if (!$lastfm_user) { $lastfm_user = Slim::Utils::Prefs::get( 'plugin_lastfm_username' ); $lastfm_password = Slim::Utils::Prefs::get( 'plugin_lastfm_password' ); } return ($lastfm_user,$lastfm_password); } sub lastFMmsg{ my $text = shift; if ($::d_plugins) { msg("LastFM: $text"); } } sub createPlayer { my $name = shift; if (!$players{$name}) { $players{$name} = LastFMStatus->new(); $players{$name}->last_check_time(time()); $players{$name}->remaining($lastfm_timeout/2); $players{$name}->streaming(0); $players{$name}->listitem(0); $players{$name}->subscriber(0); $players{$name}->session("UNSET"); } if ((getUserDetails())[0] eq '') { $players{$name}->status("NO_LOGIN"); } return $players{$name}; } sub parseLastFMData { my $lastFMData = Slim::Utils::Misc::unescape(shift); my %data; for (split /\n/, $lastFMData) { # strip whitespace from end of line m/^(.+?)=(.*?)\s*$/; $data{$1} = $2; lastFMmsg("$1 = $2\n"); } return %data; } sub getLastFMStatusTimer{ my $client = shift; updateLastFMStatus($client); } sub setLastFMTimer { my $client = shift; my $interval = shift; my $macaddress = getPlayerKey($client); $interval = $players{$macaddress}->remaining unless ($interval); # timer to check alarms on an interval lastFMmsg("Setting timer for ".$interval." seconds for ".$macaddress."\n"); Slim::Utils::Timers::killTimers($client, \&getLastFMStatusTimer); Slim::Utils::Timers::setTimer($client, (Time::HiRes::time() + $interval), \&getLastFMStatusTimer); } ### artwork stuff #### sub setLastFMArt { my $playername = shift; my $artFile = shift; if (!$artFile) { lastFMmsg("Looking for default artwork...\n"); #set the artwork for my $plugindir (Slim::Utils::OSDetect::dirsFor('Plugins')) { opendir(DIR, catdir($plugindir,"LastFM")) || next; $artFile = catdir($plugindir,"LastFM", "lastfm.gif"); } closedir(DIR); } if ($artFile and -f $artFile) { lastFMmsg("Setting artwork: $artFile\n"); my $trackHandle = Slim::Schema->resultset('Track')->objectForUrl($players{$playername}->stream_url); $trackHandle->set('thumb' => $artFile); $trackHandle->set('cover' => $artFile); $trackHandle->update(); } } sub initCacheFolder { my $purge = shift; my $cacheFolder = Slim::Utils::Prefs::get('cachedir'); my $cacheAge = 7; mkdir($cacheFolder) unless (-d $cacheFolder); $cacheFolder .= "/.lastfm-artwork-cache"; mkdir($cacheFolder) unless (-d $cacheFolder); # purge the cache if (opendir(DIR, $cacheFolder)) { while (defined(my $cachedFile = readdir(DIR))) { $cachedFile = "$cacheFolder/$cachedFile"; if (-M $cachedFile > $cacheAge || $purge) { unlink $cachedFile; } } closedir(DIR); } else { lastFMmsg("can't opendir $cacheFolder: $!\n"); } # set timer to purge cache once a day Slim::Utils::Timers::setTimer(0, Time::HiRes::time() + 60*60*24, \&initCacheFolder); return $cacheFolder; } sub saveArtworkURLToCache { my $playername = shift; my $url = shift; my $cachedFile = getCachedLastFMArtwork($url); if (!$cachedFile) { #download the url lastFMmsg("downloading $url\n"); my $http = Slim::Networking::SimpleAsyncHTTP->new( \&gotLastFMArtwork, \&failedLastFMUpdate, {playername => $playername}, ); $http->get("$url"); } else { setLastFMArt($playername,$cachedFile); } } sub getCachedLastFMArtwork { my $cachedFile = shift; $cachedFile =~ s/^.*\///; if (-f "$cacheFolder/$cachedFile") { return $cachedFile; } else { return undef; } } sub gotLastFMArtwork { my $http = shift; my $playername = $http->params('playername'); my $cachedContent = $http->content(); my ($cachedFile) = $http->{'url'}; $cachedFile =~ s/^.*\///; $http->close(); if ($cachedContent) { lastFMmsg("writing $cachedFile to cache\n"); open(CACHE, ">$cacheFolder/$cachedFile") or msg("Could not open $cachedFile for writing: $!\n"); binmode(CACHE); print CACHE $cachedContent; close(CACHE); setLastFMArt($playername,"$cacheFolder/$cachedFile"); } } ## station related stuff ########### sub switchStation { my $playername = shift; my $selectedstation = shift; #remove anything before the semi-colon $selectedstation =~ s/^.*?\;//; #remove lastfm:// $selectedstation =~ s/^lastfm:\/\///i; my $page = "adjust.php"; $selectedstation = "lastfm://$selectedstation"; postToLastFM($playername,$page,{url => $selectedstation}); } sub deleteStation { my $station = shift; #remove sub-options my $regex = join '|',@lastfm_subscriber_options; $station =~ s/\/($regex)$//i; #only consider the address when deleting #remove nickname $station =~ s/^.*;//; my @newStations = grep {$_ !~ m/$station$/ } Slim::Utils::Prefs::getArray('plugin_lastfm_stations'); Slim::Utils::Prefs::set('plugin_lastfm_stations',\@newStations); } sub addStation { my $station = shift; #don't allow blanks return if ($station eq ""); #tidy up stations coming from LastFM details $station =~ s/^http:\/\/www.last.fm\///i; $station =~ s/^lastfm:\/\///i; #remove sub-options my $regex = join '|',@lastfm_subscriber_options; $station =~ s/\/($regex)$//i; $station = addNickName($station); #don't allow duplicates my @newStations = grep {$_ ne $station} Slim::Utils::Prefs::getArray('plugin_lastfm_stations'); push @newStations, $station; Slim::Utils::Prefs::set('plugin_lastfm_stations',\@newStations); } sub addNickName { my $station = shift; #don't add anything if there's already a nickname return $station if ($station =~ m/;/); #add a nickname for user stations if ( $station =~ m/user\/(.*)$/) { return "$1's;$station"; } #add similar artists nickname if (my $station =~ m/^artist\/(.+?)\/similarartists$/) { return string('PLUGIN_LASTFM_SIMILAR_ARTISTS')." $1;$station"; } #add group nickname if ($station =~ m/^group\/(.+?)$/) { return "$1 Group Radio;$station"; } #add group nickname if ($station =~ m/^group\/(.+?)$/) { return "$1 Group Radio;$station"; } #add globaltags nickname if ($station =~ m/^globaltags\/(.+?)$/) { return "$1 Tag Radio;$station"; } return $station; } #return a top-level list of stations sub genStationNamesLevel1 { my $client = shift; my $macaddress = getPlayerKey($client); my @stations = Slim::Utils::Prefs::getArray("plugin_lastfm_stations"); my ($user) = getUserDetails($client); #always add user's station unshift @stations, "user/$user"; #add similar artist stations from current track/Scrobbler history my @artists; my %seen; eval { # use in memory history to generate a list of stations @artists = @{$Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory}; }; push(@artists,[$players{$macaddress}->artist]) if (getLastFMStatus($client)); foreach (@artists) { my $artist = $_->[0]; next if ($seen{$artist}); $seen{$artist}++; push @stations, "artist/$artist/similarartists"; } #add nicknames for (@stations) { $_ = addNickName($_); } return @stations; } #return flat station list sub genStationNames { my $client = shift; my $selectedstation = shift; my $macaddress = getPlayerKey($client); my @stations; my %allstations; if (!$selectedstation) { #we want all of them @stations = genStationNamesLevel1($client); } else { #provided one push @stations, $selectedstation; } for my $station (@stations) { if ( $station =~ m/user\//) { my @options; if ($players{$macaddress}->subscriber) { #subscribers get all options for (@lastfm_subscriber_options) { #personal not allowed when in discovery mode if ($_ ne 'personal' or !$players{$macaddress}->disco) { push @options,$_; } } } else { #neighbours & recommended are allowed for non-subscribers push @options,$lastfm_subscriber_options[0]; push @options,$lastfm_subscriber_options[1]; } # add the suffix to nickname and address my ($nick, $address) = split /;/,$station; if (!$address) { $address = $nick; } for (@options) { $allstations{"$nick $_;$address/$_"} = 1;; } } else { $allstations{$station} = 1; } } #using a hash to remove any duplicates return sort keys %allstations; } 1; # Local Variables: # tab-width:4 # indent-tabs-mode:t # End: