# 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: