# Plugin.pm # # Copyright (c) 2004-6 SlimScrobbler Team: # # Stewart Loving-Gibbard (sloving-gibbard@uswest.net) -- Original author # Mike Scott (slimscrobbler@hindsight.it) -- SlimServer 5.x changes # Ian Parkinson (iwp@iwparkinson.plus.com) -- Background submission, # SlimServer 6.x changes, # multiuser support # and other tweaks # Hakan Tandogan (hakan@gurkensalat.com) -- MusicBrainz support # Eric Gauthier, Dean Blacketter: !$client fix # Eric Koldinger: Configuration panel # Néstor Spedalieri: Spanish Translation # Malcolm Wotton -- Tag submission # James Craig (james.craig@london.com) -- SlimServer 6.5 changes # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. use strict; package Plugins::SlimScrobbler::Plugin; $Plugins::SlimScrobbler::Plugin::CVS_VERSION = substr(q$Revision: 1.14 $,10); # NOTE TO USERS # You used to have to edit this source file to provide your audioscrobbler # userid and password. In most cases you should no longer edit this file; # instead configure the plugin via the Plugins menu on the slimserver # admin web pages. # If you use different audioscrobbler accounts for different players, # you still need to edit the playerAccounts hashtable below; but you must # still provide a default userid/password on the admin web pages. # TODO Rationalise the use list. Much work has been offloaded to # Session.pm and Track.pm, so some of these may no longer be necessary. use Slim::Player::Playlist; use Slim::Control::Request; use Slim::Player::Source; use Slim::Player::Sync; use Slim::Utils::Misc; use Slim::Utils::Timers; use Slim::Utils::Strings qw (string); use Slim::Player::Client; use Slim::Music::Info; use Slim::Utils::OSDetect; use Data::Dumper; use Time::HiRes; use MP3::Info; use Class::Struct; # For temp directory use File::Spec; #this should work on all OS's BEGIN { for (Slim::Utils::OSDetect::dirsFor('Plugins')) { unshift @INC, File::Spec->catdir($_,"SlimScrobbler","SlimScrobbleSupport"); } } use Scrobbler::Session; use Scrobbler::Track; # If Perl version less than 5.8, use old Unicode stuff if ($^V lt v5.8) { # print "Perl version less than 5.8!\n"; # Haven't been able to get this working yet; no Unicode on Perl 5.6 for now. # This means non-ASCII characters in song & track titles may be submitted # incorrectly. # require Unicode::MapUTF8; # import Unicode::MapUTF8; } else { #print "Perl version 5.8 or greater.\n"; # Requires Perl 5.8.0 or greater. Needed for UTF-8 encoding. require Encode; import Encode; } # Requires Perl 5.8.0 or greater. Needed for UTF-8 encoding. #use Encode; ################################################# ### Global constants - do not change casually ### ################################################# # There are multiple different conditions which # influence whether a track is considered played: # # - A minimum number of seconds a track must be # played to be considered a play. Note that # if set too high it can prevent a track from # ever being noted as played - it is effectively # a minimum track length. Overrides other conditions! # # - A percentage play threshold. For example, if 50% # of a track is played, it will be considered played. # # - A time played threshold. After this number of # seconds playing, the track will be considered played. # Identity of this plug-in my $SCROBBLE_PLUGIN_NAME = "SlimScrobbler"; my $SCROBBLE_PLUGIN_VERSION = "0.37"; my $trackVars = { # After this much of a track is played, mark as played. SCROBBLE_PERCENT_PLAY_THRESHOLD => .50, # After this number of seconds of a track is played, mark as played. SCROBBLE_TIME_PLAY_THRESHOLD => 240, # The minimum length (in seconds) of tracks allowed by AudioScrobbler SCROBBLE_MINIMUM_LENGTH => 30, # The maximum length (in seconds) of tracks allowed by AudioScrobbler # (30 minutes) SCROBBLE_MAXIMUM_LENGTH => 1800, # String/character to separate fields on submission # TODO this is defined here and in sessionVars; fix it. SCROBBLE_SUBMIT_DELIMITER => "&", }; $trackVars->{STREAM_PLAY_DURATION} = sprintf("%d",$trackVars->{SCROBBLE_PERCENT_PLAY_THRESHOLD} * $trackVars->{SCROBBLE_TIME_PLAY_THRESHOLD}); my $sessionVars = { # After a submission error, the number of seconds before retrying, # as used by the non-background submitter SCROBBLE_RETRY_DELAY => 1800, # After a submission error, minimum and maximum number of seconds before # retrying, as used by the background submitter # After the first failure, it retries after MIN_DELAY, and doubles # each time, maxing out at MAX_DELAY. SCROBBLE_RETRY_MIN_DELAY => 60, SCROBBLE_RETRY_MAX_DELAY => 7200, # Length of time, in seconds, to wait for a response from AudioScrobbler # (ignored by the background submitter) SCROBBLE_COMMS_TIMEOUT => 10, # Length of time, in seconds, to wait for certain network operations # Should be short enough to not cause pauses in playback SCROBBLE_NB_COMMS_TIMEOUT => 5, # Identity of this plug-in SCROBBLE_PLUGIN_NAME => $SCROBBLE_PLUGIN_NAME, SCROBBLE_PLUGIN_VERSION => $SCROBBLE_PLUGIN_VERSION, # Base URL to the AudioScrobbler server SCROBBLE_SERVER => "post.audioscrobbler.com/", # URL for testing # SCROBBLE_SERVER => "audioscrobbler.sourceforge.net/submissiontest.php", # User/password for testing with above test URL #my $TEST_USER = "test"; #my $TEST_PASSWORD = "testpass"; # Client ID for our plugin SCROBBLE_CLIENT_ID => "slm", # String/character to separate fields on submission SCROBBLE_SUBMIT_DELIMITER => "&", }; # Indicator if scrobbler is hooked or not # 0= No # 1= Yes my $SCROBBLE_HOOK = 0; # Whether we should, by default, use the background submitter. # Owners of older (SliMP3) players may want to disable this, as it # can cause occasional breaks while playing music. However, when disabled, # you'll see occasional lengthy breaks between tracks and (probably) more # spam warnings from AudioScrobbler. If you do want to disable this, set # scrobbler-background-submit = 0 in the slimserver.conf file. my $SCROBBLE_BACKGROUND_DEFAULT = 1; my $STREAM_TO_TITLE_MAP = {}; # Export the version to the server use vars qw($VERSION); $VERSION = $SCROBBLE_PLUGIN_VERSION; ################################################## ### SlimServer Plugin API ### ################################################## # This section needs a lot of help. At the time # that I wrote this, I had no SlimDevices hardware, # and so couldn't test it. # Current menu state for the clients my %mainModeSelection; my %userModeSelection; sub getDisplayName() { # Slim changed this at slimserver v6. Seems an odd thing to change # to me, given that it must break every plugin out there, and makes it # impossible to write plugins that support both pre- and post-v6 servers. # return string('PLUGIN_SCROBBLER') return "PLUGIN_SCROBBLER"; } sub strings { local $/ = undef; ; } sub setMode() { my $client = shift; unless (defined($mainModeSelection{$client})) { $mainModeSelection{$client} = 0; } $client->lines(\&mainModeLines); } sub enabled() { my $client = shift; return 1; } sub initPlugin() { # do _debug first, so that anything that traces will work initSetting("plugin_scrobbler_debug", 0); initSetting("plugin_scrobbler_accounts", []); initSetting("plugin_scrobbler_auto_submit", 1); initSetting("plugin_scrobbler_submit_tags", 0); initSetting("plugin_scrobbler_background_submit", $SCROBBLE_BACKGROUND_DEFAULT); initSetting("plugin_scrobbler_max_pending", 1); initSetting("plugin_scrobbler_track_history_size", 10); $Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory=[]; # This callback experimental so we comment out - we can leave the code in place though # Slim::Music::Info::setCurrentTitleChangeCallback(\&malcolmTitleCallback); # Add the player mode for account selection (multi-user support) Slim::Buttons::Common::addMode('plugins_scrobbler_users', userModeGetFunctions(), \&setUserMode); my ($groupRef,$prefRef) = &setupPlayerPrefs(); Slim::Web::Setup::addGroup('PLAYER_PLUGINS', 'slimscrobbler', $groupRef, undef, $prefRef); hookScrobbler(); } sub shutdownPlugin() { unHookScrobbler(); } # The Plugin's main Mode my %functions = ( 'up' => sub { my $client = shift; my @choices = getMainModeChoices($client); my $newposition = Slim::Buttons::Common::scroll($client, -1, scalar(@choices), $mainModeSelection{$client}); if ($newposition != $mainModeSelection{$client}) { $mainModeSelection{$client} = $newposition; $client->pushUp(); } }, 'down' => sub { my $client = shift; my @choices = getMainModeChoices($client); my $newposition = Slim::Buttons::Common::scroll($client, +1, scalar(@choices), $mainModeSelection{$client}); if ($newposition != $mainModeSelection{$client}) { $mainModeSelection{$client} = $newposition; $client->pushDown(); } }, 'left' => sub { my $client = shift; Slim::Buttons::Common::popModeRight($client); }, 'right' => sub { my $client = shift; my @choices = getMainModeChoices($client); my $rightSub = $choices[$mainModeSelection{$client}][1]; if ($rightSub) { &$rightSub($client); } }, 'play' => sub { my $client = shift; my @choices = getMainModeChoices($client); my $playSub = $choices[$mainModeSelection{$client}][2]; if ($playSub) { &$playSub($client); } }, #for direct access via remote 'nextAccount' => sub { if (isSlimScrobblerConfiguredForMultiUser()) { my $client = shift; my $accPassHash = retrieveAccountPasswordHash(); my @userList = sort keys %$accPassHash; $userModeSelection{$client} = initUserMode($client) unless ($userModeSelection{$client}); $userModeSelection{$client}++; $userModeSelection{$client} = ($userModeSelection{$client} % scalar(@userList)); my $msg = changeClientsSelectedUserId($client, \@userList, $userModeSelection{$client}); $client->showBriefly(undef,$msg); } }, 'submitNow' => sub { my $client = shift; mainModeSubmitNow($client); }, 'toggleStatus' => sub { my $client = shift; my $msg = mainModeToggleScrobblerForPlayer($client); $client->showBriefly(undef,$msg); }, ); sub isSlimScrobblerConfiguredForMultiUser { my $accArrayRef = Slim::Utils::Prefs::get("plugin_scrobbler_accounts"); return (scalar(@$accArrayRef) > 1); } # Gives the options available on the main mode menu. # If SlimScrobbler is not configured, there is just a warning # Otherwise, the menu includes current enabled state of slimscrobbler # Returned is a two-dimensional array, one top-level entry for each # option. The entry for each option contains the string to display, # the action to take when right is pressed, and the action to take when # play is pressed. sub getMainModeChoices($) { my $client = shift; my @choices; # If multi-user, option to change the account in use if (isSlimScrobblerConfiguredForMultiUser()) { my $c = [ 'PLUGIN_SCROBBLER_SELECT_ACCOUNT', \&mainModeToUserMode, undef ]; push @choices, $c; } # Option to enable/disable the entire plugin if ($SCROBBLE_HOOK == 0) { my $c = [ 'PLUGIN_SCROBBLER_DISABLED', \&mainModeToggleScrobbler, undef ]; push @choices, $c; } else { # Option to enable/disable the plugin on this player if (!Slim::Utils::Prefs::clientGet($client, "plugin_scrobbler_enabled")) { my $c = [ 'PLUGIN_SCROBBLER_CLIENT_DISABLED', \&mainModeToggleScrobblerForPlayer, undef ]; push @choices, $c; } else { my $c = [ 'PLUGIN_SCROBBLER_HIT_PLAY_TO_SUBMIT', undef, \&mainModeSubmitNow ] ; push @choices, $c; my $c = [ 'PLUGIN_SCROBBLER_CLIENT_ENABLED', \&mainModeToggleScrobblerForPlayer, undef ]; push @choices, $c; } my $c = [ 'PLUGIN_SCROBBLER_ENABLED', \&mainModeToggleScrobbler, undef ]; push @choices, $c; } return @choices; } sub mainModeToggleScrobbler() { my $client=shift; my $msg; if ($SCROBBLE_HOOK == 0) { $msg = string('PLUGIN_SCROBBLER_ACTIVATED'); Plugins::SlimScrobbler::Plugin::hookScrobbler(); } else { $msg = string('PLUGIN_SCROBBLER_DEACTIVATED'); Plugins::SlimScrobbler::Plugin::unHookScrobbler(); } $client->update(); scrobbleMsg("$msg\n"); return $msg; } sub mainModeToggleScrobblerForPlayer() { my $client=shift; my $msg; if (!Slim::Utils::Prefs::clientGet($client, "plugin_scrobbler_enabled")) { $msg = string('PLUGIN_SCROBBLER_ACTIVATED'); Slim::Utils::Prefs::clientSet($client, "plugin_scrobbler_enabled",1); } else { $msg = string('PLUGIN_SCROBBLER_DEACTIVATED'); Slim::Utils::Prefs::clientSet($client, "plugin_scrobbler_enabled",0); } $client->update(); scrobbleMsg("$msg\n"); return $msg; } sub mainModeSubmitNow() { my $client = shift; my $session = getSessionForClient($client); if ($session) { my $line = string('PLUGIN_SCROBBLER_SUBMITTING'); $client->showBriefly($line, undef); $session->attemptToSubmitToAudioScrobbler(); } } sub mainModeToUserMode() { my $client = shift; initUserMode($client); Slim::Buttons::Common::pushModeLeft($client, "plugins_scrobbler_users"); } sub mainModeLines() { my $client=shift; my @choices=getMainModeChoices($client); # Do a patch-up here. If the client is beyond the end of the menu # (the menu might change), reset it if ($mainModeSelection{$client} >= scalar(@choices)) { $mainModeSelection{$client} = 0; } my $line1=string('PLUGIN_SCROBBLER'); if (scalar(@choices) > 1) { $line1 = $line1 . " (" . ($mainModeSelection{$client}+1) . " " . string("PLUGIN_SCROBBLER_OF") . " " . (scalar(@choices)) . ")"; } my $line2 = string($choices[$mainModeSelection{$client}][0]); my $br=undef; my $rightFunc = $choices[$mainModeSelection{$client}][1]; if ($rightFunc) { $br = Slim::Display::Display::symbol('rightarrow'); } return ($line1, $line2, undef, $br); } ### # The users list mode sub initUserMode($) { my $client = shift; # Calculate the index of the currently selected userid for this client # First, get the currently selected userid # TODO this is very similar to some code below, can it be refactored? my $userid = Slim::Utils::Prefs::clientGet($client, "plugin_scrobbler_userid"); my $accPassHash = retrieveAccountPasswordHash(); my @userids = sort keys %$accPassHash; my $index = 0; if (defined($userid) and $userid ne '') { # We have a userid - check it hasn't been deleted and locate its index for (my $i=0; $i<@userids; $i++) { if ($userids[$i] eq $userid) { # We do the +1 because the displayed list will have # a "Use default userid" at position 0 - also we rely on # $i == 0 if not found in the test below $index= $i + 1; } } if ($index == 0) { scrobbleMsg("Configured userid $userid not found - removing setting\n"); Slim::Utils::Prefs::clientDelete($client, "plugin_scrobbler_userid"); $userid=undef; } } $userModeSelection{$client} = $index; } my %userModeFunctions = ( 'up' => sub { my $accPassHash = retrieveAccountPasswordHash(); my @userList = sort keys %$accPassHash; my $client = shift; my $newposition = Slim::Buttons::Common::scroll($client, -1, ($#userList + 2), $userModeSelection{$client}); if ($newposition != $userModeSelection{$client}) { $userModeSelection{$client} = $newposition; $client->pushUp(); changeClientsSelectedUserId($client, \@userList, $newposition); } }, 'down' => sub { my $accPassHash = retrieveAccountPasswordHash(); my @userList = sort keys %$accPassHash; my $client = shift; my $newposition = Slim::Buttons::Common::scroll($client, +1, ($#userList + 2), $userModeSelection{$client}); if ($newposition != $userModeSelection{$client}) { $userModeSelection{$client} = $newposition; $client->pushDown(); changeClientsSelectedUserId($client, \@userList, $newposition); } }, 'left' => sub { my $client = shift; Slim::Buttons::Common::popModeRight($client); }, 'right' => sub { my $client = shift; $client->bumpRight(); }, ); # Helper function, used by userModeFunctions sub changeClientsSelectedUserId { my $client = shift; my $userListR = shift; my $chosen = shift; my $msg; my @userList = @$userListR; if ($chosen == 0) { # Become server default - delete the property $msg = "Client will now use server default account"; Slim::Utils::Prefs::clientDelete($client, "plugin_scrobbler_userid"); } else { my $newuser=$userList[$chosen-1]; $msg = "Client will now use account $newuser"; scrobbleMsg("$msg\n"); Slim::Utils::Prefs::clientSet($client, "plugin_scrobbler_userid", $newuser); } $client->update(); scrobbleMsg("$msg\n"); return $msg; } sub userModeGetFunctions() { return \%userModeFunctions; } sub userModeLines { my $client = shift; my $accPassHash = retrieveAccountPasswordHash(); my @userList = sort keys %$accPassHash; my $line1 = string('PLUGIN_SCROBBLER_SELECT_ACCOUNT'); $line1 = $line1 . " (" . ($userModeSelection{$client}+1) . " " . string('PLUGIN_SCROBBLER_OF') . " " . ($#userList+2) . ")"; my $line2; if ($userModeSelection{$client} == 0) { $line2 = string('PLUGIN_SCROBBLER_USE_DEFAULT_ACCOUNT'); } else { $line2 = @userList[$userModeSelection{$client}-1] || ''; } return ($line1, $line2); } sub setUserMode { my $client = shift; $client->lines(\&userModeLines); } ################################################## ### Scrobbler per-client Data ### ################################################## # Each client's playStatus structure. # Start's empty; as new players appear we add them using getPlayerStatusForClient(). my %playerStatusHash = (); # Each user's Session object. # As with playerStatusHash, starts empty. Sessions are added as we need them, # indexed by userid my %sessionHash = (); # The master structure, one per Slim client. Matches the client to the # currently playing track # (This is now of very little use, but we keep it around just in case # we ever have any more per-client state to track) struct PlayTrackStatus => { # Client Name & ID # Used for debugging at the moment clientName => '$', clientID => '$', # The track currently playing. May be undef. currentTrack => 'Scrobbler::Track', # Is this player on or off? # (HEY SlimDevices staff: The "power" commandCallbck DOES NOT return "on" or "off", so I must # track this myself! Very error-prone, and redundant.) isOn => '$', }; # Convert the array of password entries into a hash sub retrieveAccountPasswordHash { my $accArrayRef = Slim::Utils::Prefs::get("plugin_scrobbler_accounts"); my $accHashRef = {}; map { my $entry = $_; my ($acc,$pass) = ($entry =~ /(.*?):(.*)/); $accHashRef->{$acc} = $pass; } @$accArrayRef; return $accHashRef; } # Get the appropriate user ID & password for the given # client. Returns (undef, undef) if no userid available sub getUserIDPasswordForClient($) { my $client = shift; my $accArrayRef = Slim::Utils::Prefs::get("plugin_scrobbler_accounts"); my ($defaultID,$defaultPass) = ($accArrayRef->[0] =~ /(.*?):(.*)/); my $accPassHash = retrieveAccountPasswordHash(); # if no client we default to first on the list unless ($client) { scrobbleMsg("Not passed a client so userid defaulting to $defaultID\n"); return ($defaultID,$defaultPass); } # we try for a client specific version, if it doesn't exist or # then we default to the single account case anyway. my $userid = Slim::Utils::Prefs::clientGet($client, "plugin_scrobbler_userid"); $userid = undef if ($userid and $userid eq ''); # we need check the password (undef tells us the user id is dead or account is badly configured unless ( !defined $userid || $accPassHash->{$userid}) { scrobbleMsg("Configured userid $userid not found - removing setting\n"); Slim::Utils::Prefs::clientDelete($client, "plugin_scrobbler_userid"); $userid=undef; } unless (defined($userid)) { # Use the default userid scrobbleMsg("Can't find a valid client userid, defaulting to $defaultID\n"); return ($defaultID,$defaultPass); } return ($userid,$accPassHash->{$userid}); } # Obtain a Session given a userid and password; re-use an existing # one if possible. Updates the password on the Session, this should # only happen after a config change. As a side-effect, registers any # new Session with the background submitter sub getSessionForUserIDPassword($$) { my $userID = shift; my $password = shift; my $sess=$sessionHash{$userID}; if (!defined($sess)) { scrobbleMsg("Creating new Session for $userID\n"); # Create the session $sess=Scrobbler::Session->new( userid => $userID, password => $password); $sessionHash{$userID} = $sess; addSessionToSubmitter($sess); } else { scrobbleMsg("Found existing Session for $userID\n"); $sess->password($password); } return $sess; } # Set the appropriate default values for this playerStatus struct sub setPlayerStatusDefaults($$$$) { # Parameter - client my $client = shift; # Parameter - Player status structure. # Uses pass-by-reference my $playerStatusToSetRef = shift; # Parameters -- Client name & ID my $clientName = shift; my $clientID = shift; # Set client name & ID (used for debugging at the moment) $playerStatusToSetRef->clientName($clientName); $playerStatusToSetRef->clientID($clientID); # Are we on? (If we've heard about the player, it is on..) $playerStatusToSetRef->isOn('true'); # new players are on by default if (!Slim::Utils::Prefs::clientGet($client, "plugin_scrobbler_enabled")) { Slim::Utils::Prefs::clientSet($client, "plugin_scrobbler_enabled",1); } } # Obtain a Session for this client; either re-use an existing one # (indexed by userid) or create a new one. # If we are using an existing Session, update the password as # specified for this player - this will be triggered by a # configuration change sub getSessionForClient($) { my $client = shift; # Extract userId and password for the client my ($userID, $password) = getUserIDPasswordForClient($client); if (!($userID)) { scrobbleMsg("No userid found for client\n"); return undef; } elsif (!Slim::Utils::Prefs::clientGet($client, "plugin_scrobbler_enabled")) { scrobbleMsg("scrobbling disabled for this client\n"); return undef; }else { scrobbleMsg("User ID: $userID\n"); return getSessionForUserIDPassword($userID, $password); } } # Get the player state for the given client. # Will create one for new clients. sub getPlayerStatusForClient($) { # Parameter - Client structure my $client = shift; # Get the friendly name for this client my $clientName = Slim::Player::Client::name($client); # Get the ID (IP) for this client my $clientID = Slim::Player::Client::id($client); # These messages get pretty tedious when debugging, even for a chatty client. #scrobbleMsg("Asking about client $clientName ($clientID)\n"); # If we haven't seen this client before, create a new per-client # playState structure. if (!defined($playerStatusHash{$client})) { scrobbleMsg("Creating new PlayerStatus for $clientName ($clientID)\n"); # Create new playState structure $playerStatusHash{$client} = PlayTrackStatus->new(); # Set appropriate defaults setPlayerStatusDefaults($client, $playerStatusHash{$client}, $clientName, $clientID); } else { # These messages get pretty tedious when debugging, even for a chatty client. #Plugins::SlimScrobbler::Plugin::scrobbleMsg("Already knew about $clientName ($clientID)\n"); } # If it didn't exist, it does now - # return the playerStatus structure for the client. return $playerStatusHash{$client}; } ################################################ ### AudioScrobbler main routines ### ################################################ # A wrapper to allow us to uniformly turn on & off Scrobbler debug messages sub scrobbleMsg($) { # Parameter - Message to be displayed my $scrobbleMessage = shift; if ($::d_plugins || (Slim::Utils::Prefs::get("plugin_scrobbler_debug") eq 1)) { msg("Scrobbler: $scrobbleMessage"); } } # Hook the scrobbler to the play events. # Do this as soon as possible during startup. # Only call this if config is valid. sub hookScrobbler() { if ($SCROBBLE_HOOK == 0) { scrobbleMsg("hookScrobbler() engaged, SlimScrobbler V$SCROBBLE_PLUGIN_VERSION activated.\n"); scrobbleMsg("CVS: Plugins::SlimScrobbler::Plugin ($Plugins::SlimScrobbler::Plugin::CVS_VERSION)\n"); scrobbleMsg("CVS: Scrobbler::Track ($Scrobbler::Track::CVS_VERSION)\n"); scrobbleMsg("CVS: Scrobbler::Session ($Scrobbler::Session::CVS_VERSION)\n"); scrobbleMsg("Subscribing on 'open'\n"); Slim::Control::Request::subscribe( \&Plugins::SlimScrobbler::Plugin::openCommand, [['playlist'],['open']]); scrobbleMsg("Subscribing on 'play', 'pause', 'stop', 'power', 'mode'\n"); Slim::Control::Request::subscribe( \&Plugins::SlimScrobbler::Plugin::otherCommand, [['play', 'pause', 'stop', 'power', 'mode']]); $SCROBBLE_HOOK=1; if (useBackgroundSubmitter()) { setSubmitterTimer(); # initialise a Session for all known userids, so that we can # submit old data immediately upon startup my $accPassHash = retrieveAccountPasswordHash(); my $userid; my $password; while (($userid, $password) = each %$accPassHash) { getSessionForUserIDPassword($userid, $password); } } } else { scrobbleMsg("SlimScrobbler already active, ignoring hookScrobbler() call"); } } # Unhook the Scrobbler's play event callback function. # Do this as the plugin shuts down, if possible. sub unHookScrobbler() { if ($SCROBBLE_HOOK == 1) { if (useBackgroundSubmitter()) { cancelSubmitterTimer(); } scrobbleMsg("Unsubscribing on 'play', 'pause', 'stop', 'power', 'mode'\n"); Slim::Control::Request::unsubscribe( \&Plugins::SlimScrobbler::Plugin::otherCommand); scrobbleMsg("Unsubscribing on 'open'\n"); Slim::Control::Request::unsubscribe( \&Plugins::SlimScrobbler::Plugin::openCommand); scrobbleMsg("unHookScrobbler() engaged, SlimScrobbler V$SCROBBLE_PLUGIN_VERSION deactivated.\n"); $SCROBBLE_HOOK=0; } else { scrobbleMsg("SlimScrobbler not active, ignoring unHookScrobbler() call\n"); } } sub openCommand($) { ###################################### ### Open command ###################################### my $request = shift; my $client = $request->client(); # Parameter - filename of track being played my $filename = $request->getParam('_path'); # Get name & ID of this player my $playStatus = getPlayerStatusForClient($client); my $clientName = $playStatus->clientName(); my $clientID = $playStatus->clientID(); scrobbleMsg("*----------------------------\n"); scrobbleMsg("Open command [$clientName ($clientID)]\n"); scrobbleMsg("*----------------------------\n"); # Stop old song, if needed if (defined($playStatus->currentTrack())) { stopTimingSong($client, $playStatus); } # Get new song data my $totalLength; my $artistName; my $trackTitle; my $albumName; my $musicbrainz_id; my $trackGenreRef; my $trackObj = Slim::Schema->resultset('Track')->objectForUrl($filename); if ($trackObj) { $totalLength = $trackObj->durationSeconds(); $trackTitle = $trackObj->title(); my $artist = $trackObj->artist(); if ($artist) { $artistName = $artist->name(); } my $album = $trackObj->album(); if ($album) { $albumName = $album->title(); } $musicbrainz_id = $trackObj->{musicbrainz_id}; $trackGenreRef = [map {my $gen = $_; $gen->name()} ($trackObj->genres())]; } # Start timing new song startTimingNewSong($client, $playStatus, $filename, $artistName, $trackTitle, $albumName, $totalLength, $musicbrainz_id, $trackGenreRef); showCurrentVariables($playStatus); } sub otherCommand($) { # These are the two passed parameters my $request = shift; my $client = $request->client(); return unless($client); # This is a strange occurrence that only appears to happen when upgrading firmware my $paramsRef = [$request->renderAsArray()]; # I had hoped that this would be accurate enough # to use in the place of all this ugly state tracking, but # sometimes it says "stop" when the *next* command # is about to put it into play mode. # In the end, you need to watch commands anyhow, just # a different set of them. So, sticking with original # implementation. #my $playMode = Slim::Player::Source::playmode($client); #scrobbleMsg("[****} Playmode according to Slim Code: $playMode\n"); #scrobbleMsg("====New commands:\n"); #foreach my $param (@$paramsRef) #{ # scrobbleMsg(" command: $param\n"); #} # showCurrentVariables($playStatus); my $slimCommand = @$paramsRef[0]; my $paramOne = @$paramsRef[1]; ###################################### ### Play command ###################################### if( ($slimCommand eq "play") || (($slimCommand eq "mode") && ($paramOne eq "play")) ) { playCommand($client); } ###################################### ### Pause command ###################################### if ($slimCommand eq "pause") { # This second parameter may not exist, # and so this may be undef. Routine expects this possibility, # so all should be well. pauseCommand($client, $paramOne); } if (($slimCommand eq "mode") && ($paramOne eq "pause")) { # "mode pause" will always put us into pause mode, so fake a "pause 1". pauseCommand($client, 1); } ###################################### ### Sleep command ###################################### if ($slimCommand eq "sleep") { # Sleep has no effect on streamed players; is this correct for SlimDevices units? # I can't test it. #scrobbleMsg("===> Sleep activated! Be sure this works!\n"); #pauseCommand($playStatus, undef()); } ###################################### ### Stop command ###################################### if ( ($slimCommand eq "stop") || (($slimCommand eq "mode") && ($paramOne eq "stop")) ) { stopCommand($client); } ###################################### ### Stop command ###################################### if ( ($slimCommand eq "playlist") && ($paramOne eq "sync") ) { # If this player syncs with another, we treat it as a stop, # since whatever it is presently playing (if anything) will end. stopCommand($client); } ###################################### ## Power command ###################################### # If we received a potential "on"/"off" parameter.. if ($paramOne) { # Power off if ( (($slimCommand eq "power") && ($paramOne eq "off" or $paramOne == 0)) || (($slimCommand eq "mode") && ($paramOne eq "off")) ) { #scrobbleMsg("===> Power Off with explicit \"Off\" parameter\n"); powerOffCommand($client); } # Power on elsif ( (($slimCommand eq "power") && ($paramOne eq "on" or $paramOne == 1)) || (($slimCommand eq "mode") && ($paramOne eq "on")) ) { #scrobbleMsg("===> Power On with explicit \"On\" parameter\n"); powerOnCommand($client); } } # Unfortunately, the second parameter is optional, and we often don't get it. else { # TODO this is a little ugly - we get PlayerStatus both here and in # the powerO[n|ff]Command() function. my $playStatus = getPlayerStatusForClient($client); # Power off if ( ($slimCommand eq "power") && ($playStatus->isOn() eq "true") ) { #scrobbleMsg("===> Power Off, since playStatus appears to be On.\n"); powerOffCommand($client); } # Power on elsif ( ($slimCommand eq "power") && ($playStatus->isOn() eq "false") ) { #scrobbleMsg("===> Power On, since playStatus appears to be Off.\n"); powerOnCommand($client); } } } sub playCommand($) { ###################################### ### Play command ###################################### # Parameter - current client my $client = shift; # Get name & ID of this player my $playStatus = getPlayerStatusForClient($client); my $clientName = $playStatus->clientName(); my $clientID = $playStatus->clientID(); scrobbleMsg("*----------------------------\n"); scrobbleMsg("Play command [$clientName ($clientID)]\n"); scrobbleMsg("*----------------------------\n"); my $track = $playStatus->currentTrack(); if (defined($track)) { $track->play(); } # If we just got a play command, but we think the player is off, we need to toggle the # player status back to "on" again. (We don't get a 'power' command when someone just hits # the play button from a power-off state. Sigh.) # We used to lose the currently-playing track if someone did this, but I don't think we do # any more. if ($playStatus->isOn() eq "false") { scrobbleMsg("Looks like someone forgot to tell us power was back on for $clientName ($clientID)]...\n"); # scrobbleMsg("NOTE: Right now we lose track of the current track's play in this circumstance, sorry! ($clientID)]\n"); $playStatus->isOn("true"); } showCurrentVariables($playStatus); } sub pauseCommand($$) { ###################################### ### Pause command ###################################### # Parameter - current client my $client = shift; # Get name & ID of this player my $playStatus = getPlayerStatusForClient($client); my $clientName = $playStatus->clientName(); my $clientID = $playStatus->clientID(); # Parameter - Optional second parameter in command # (This is for the case ). # If user said "pause 0" or "pause 1", this will be 0 or 1. Otherwise, undef. my $secondParm = shift; scrobbleMsg("*----------------------------\n"); scrobbleMsg("Pause command [$clientName ($clientID)]\n"); scrobbleMsg("*----------------------------\n"); my $track=$playStatus->currentTrack(); if (defined($track)) { # Just a plain "pause" if (!defined($secondParm)) { scrobbleMsg("Vanilla Pause\n"); $track->togglePause(); } # "pause 1" means "pause true", so pause and stop timing, if not already paused. elsif ( ($secondParm eq 1) ) { scrobbleMsg("Pausing (1 case)\n"); $track->pause(); } # "pause 0" means "pause false", so unpause and resume timing, if not already timing. elsif ( ($secondParm eq 0) ) { scrobbleMsg("Pausing (0 case)\n"); $track->play(); } else { scrobbleMsg("Pause command ignored, assumed redundant.\n"); } } showCurrentVariables($playStatus); } sub stopCommand($) { ###################################### ### Stop command ###################################### # Parameter - current client my $client = shift; return unless($client); # This is a strange occurrence that only appears to happen when upgrading firmware # Get name & ID of this player my $playStatus = getPlayerStatusForClient($client); my $clientName = $playStatus->clientName(); my $clientID = $playStatus->clientID(); scrobbleMsg("*----------------------------\n"); scrobbleMsg("Stop command [$clientName ($clientID)]\n"); scrobbleMsg("*----------------------------\n"); if (defined($playStatus->currentTrack())) { stopTimingSong($client, $playStatus); } showCurrentVariables($playStatus); } sub powerOnCommand ($) { ###################################### ## Power on command ###################################### # Parameter - current client my $client = shift; # Get name & ID of this player my $playStatus = getPlayerStatusForClient($client); my $clientName = $playStatus->clientName(); my $clientID = $playStatus->clientID(); scrobbleMsg("*----------------------------\n"); scrobbleMsg("Power on command [$clientName ($clientID)]\n"); scrobbleMsg("*----------------------------\n"); # Set our on/off flag $playStatus->isOn('true'); # I think I've seen times when the power state tracking has got confused, # so I'm going to pause any playing song here. The end result will be correct # whether we've just turned power off or on; either way, the track is paused. my $track=$playStatus->currentTrack(); if (defined($track)) { $track->pause(); } showCurrentVariables($playStatus); } # We used to treat power-off as a hard stop, but I always find tracks resume # where they left off after power-on. So we now treat power-off as a pause. sub powerOffCommand($) { ###################################### ## Power off command ###################################### # Parameter - current client my $client = shift; # Get name & ID of this player my $playStatus = getPlayerStatusForClient($client); my $clientName = $playStatus->clientName(); my $clientID = $playStatus->clientID(); scrobbleMsg("*----------------------------\n"); scrobbleMsg("Power off command [$clientName ($clientID)]\n"); scrobbleMsg("*----------------------------\n"); my $track=$playStatus->currentTrack(); if (defined($track)) { $track->pause(); } # Set our on/off flag $playStatus->isOn('false'); showCurrentVariables($playStatus); } # This gets called during playback events. # We look for events we are interested in, and start and stop our various # timers accordingly. # Create a UTC time string for now sub getUTCTimeRightNow() { # Using an abbreviated UTC Time format: # # YYYY-MM-DD hh:mm:ss my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday); # Get the time variables ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime(time); # Adjust for human-readable output $mday = sprintf("%02u", $mday); # months numbered from 0; $mon += 1; $mon = sprintf("%02u", $mon); # Year is number of years since 1900; adjust $year += 1900; # Time $hour = sprintf("%02u", $hour); $min = sprintf("%02u", $min); $sec = sprintf("%02u", $sec); # Build the UTC string # Year first my $UTCString = $year . "-" . $mon . "-" . $mday; # Space $UTCString .= " "; # Time second $UTCString .= $hour . ":" . $min . ":" . $sec; # Return the time we built return($UTCString); } # A new song has begun playing. Reset the current song # timer and set new Artist and Track. sub startTimingNewSong($$$$$$$$) { # Parameter - the client my $client = shift; # Parameter - PlayTrackStatus for current client my $playStatus = shift; # Parameters: Artist name & Track title my $filename = shift; my $artistName = shift; my $trackTitle = shift; my $albumName = shift; my $totalLength = shift; my $musicbrainz_id = shift; my $trackGenre = shift; if (!$artistName) { $artistName = ""; } if (!$trackTitle) { $trackTitle = ""; } if (!$albumName) { $albumName = ""; } scrobbleMsg("=======================================\n"); scrobbleMsg("Starting to time: $artistName - $trackTitle (from $albumName)\n"); scrobbleMsg("Genres: @$trackGenre\n") if ($trackGenre); my $newTrack = Scrobbler::Track->new( filename => $filename, artist => $artistName, album => $albumName, title => $trackTitle, totalLength => $totalLength, musicBrainzId => $musicbrainz_id, genre => $trackGenre, ); # Start the new track timing $newTrack->play(); # Register our track with the background submitter if (useBackgroundSubmitter()) { addTrackToSubmitter($client, $newTrack); } $playStatus->currentTrack($newTrack); } sub submitTrack { my $client = shift; my $track = shift; scrobbleMsg("Track was played long enough to count as listen\n"); my $session = getSessionForClient($client); if (defined($session)) { # Log it to Audioscrobbler my $UTCtime = getUTCTimeRightNow(); $session->logTrackToAudioScrobblerAsPlayed($track, $UTCtime); $track->markRegistered(); # If we're autosubmit, submit now if (Slim::Utils::Prefs::get("plugin_scrobbler_auto_submit")) { $session->tryToSubmitToAudioscrobblerIfNeeded(); } } else { scrobbleMsg("No session - scrobbling is disabled for this client\n"); } # We could also log to history at this point as well.. } # Stop timing the current song # (Either stop was hit or we are about to play another one) sub stopTimingSong($$) { # Parameter - client my $client = shift; # Parameter - PlayTrackStatus for current client my $playStatus = shift; my $track = $playStatus->currentTrack(); if (!defined($track)) { msg("Programmer error in Scrobbler::stopTimingSong() - not already timing!\n"); } else { # Attempt to stop the track timing $track->cancel(); if (!useBackgroundSubmitter()) { # If the track was played long enough to count as a listen, it will # be marked 'Ready' if ($track->isReady()) { submitTrack($client,$track); } else { scrobbleMsg("Track was NOT played long enough to count as listen\n"); } } } } # Debugging routine - shows current variable values for the given playStatus sub showCurrentVariables($) { # Parameter - PlayTrackStatus for current client my $playStatus = shift; scrobbleMsg("======= showCurrentVariables() ========\n"); # Call Track to display artist, track name, etc. if (defined($playStatus->currentTrack())) { $playStatus->currentTrack()->showCurrentVariables(); } else { scrobbleMsg("No track currently timing\n"); } my $tmpIsPowerOn = $playStatus->isOn(); scrobbleMsg("Is power on? : $tmpIsPowerOn\n"); scrobbleMsg("=======================================\n"); } sub getFunctions() { return \%functions; } sub sessionVars() { return $sessionVars; } sub trackVars() { return $trackVars; } ######################################################################## ## Background submission functions and data ## my @tracks = (); my @sessions = (); sub submitter { my @toDelete = (); # First, go through the tracks looking for any to register or delete my $i=0; my $client; my $session; my $track; foreach my $p (@tracks) { $client=$p->{client}; $track=$p->{track}; if ($track->isReady()) { scrobbleMsg("Following track is ready to submit:\n"); $track->showCurrentVariables(); # Register it my $UTCtime = getUTCTimeRightNow(); $session = getSessionForClient($client); if (defined($session)) { $session->logTrackToAudioScrobblerAsPlayed($track, $UTCtime); $track->markRegistered(); } else { scrobbleMsg("Client has no available Session - cancelling track\n"); $track->cancel(); } push(@toDelete, $i); } elsif ($track->isRegistered()) { # Probably shouldn't happen, means the track has already # been registered. Oh well, delete it scrobbleMsg("Fogetting about following track:\n"); $track->showCurrentVariables(); push(@toDelete, $i); } elsif ($track->isCancelled()) { # We can forget about this track now scrobbleMsg("Forgetting about following track:\n"); $track->showCurrentVariables(); push(@toDelete, $i); } $i=$i+1; } # Delete those marked for deletion (I suspect this wouldn't be a good # idea during the previous foreach) my $offset=0; foreach my $d (@toDelete) { splice(@tracks, $d - $offset, 1); $offset=$offset+1; } # Now pass over the sessions, see if they need to submit if (Slim::Utils::Prefs::get("plugin_scrobbler_auto_submit")) { foreach $session (@sessions) { $session->tryToBackgroundSubmitToAudioscrobblerIfNeeded(); } } setSubmitterTimer(); } sub setSubmitterTimer() { Slim::Utils::Timers::setTimer("SlimScrobbler", Time::HiRes::time() + 10, \&submitter); } sub cancelSubmitterTimer() { Slim::Utils::Timers::killOneTimer("SlimScrobbler", \&submitter); # Drop any tracks currently being timed. Otherwise, should the Plugin # become active again later, the track will be submitted regardless of # whether it was left playing for long enough. foreach my $p (@tracks) { my $track=$p->{track}; $track->cancel(); } } sub addTrackToSubmitter($$) { my $client=shift; my $track=shift; if (defined($client) && defined($track)) { my $p = { client => $client, track => $track, }; if (!$track->isCancelled()) { push(@tracks, $p); } } } sub addSessionToSubmitter($) { push(@sessions, shift); } sub useBackgroundSubmitter { return Slim::Utils::Prefs::get("plugin_scrobbler_background_submit"); } ######################################################################## ## Interface functions ## sub setupGroup { my %group = ( PrefOrder => [ 'plugin_scrobbler_accounts', 'plugin_scrobbler_auto_submit', 'plugin_scrobbler_submit_tags', 'plugin_scrobbler_track_history_size', ], PrefsInTable => 0, GroupHead => string('SETUP_GROUP_PLUGIN_SCROBBLER'), GroupDesc => string('SETUP_GROUP_PLUGIN_SCROBBLER_DESC'), GroupLine => 1, GroupSub => 1, Suppress_PrefSub => 1, Suppress_PrefLine => 1, Suppress_PrefHead => 1 ); my %prefs = ( 'plugin_scrobbler_accounts' => { 'isArray' => 1, 'arrayAddExtra' => 1, 'arrayDeleteNull' => 1, 'arrayDeleteValue' => '', 'arrayBasicValue' => 0, 'PrefSize' => 'large', 'inputTemplate' => 'setup_input_array_txt.html', 'PrefInTable' => 1, 'showTextExtValue' => 1, 'externalValue' => sub { my ($client, $value, $key) = @_; #scrobbleMsg("externalValue: $value, $key\n"); #extract the pref Id and return it for array numbering if ($key =~ /^(\D*)(\d+)$/) { return ' '.($2 + 1).' '; } return ' '; }, currentValue => sub { my ($client, $key, $ind) = @_; my $val = Slim::Utils::Prefs::getInd($key,$ind); #scrobbleMsg("currentValue: $key,$ind,$val\n"); #hide the password initially - but this doesn't get called when values change! $val =~ m/^(.+:)(.+)$/; return $1.'*' x length($2); }, #'onChange' => sub { # my ($client,$changeref,$paramref,$pageref) = @_; # my $key = 'plugin_scrobbler_accounts'; # if (exists($changeref->{$key}{'Processed'})) { # return; # } # Slim::Web::Setup::processArrayChange($client, $key, $paramref, $pageref); # $changeref->{'plugin_scrobbler_accounts'}{'Processed'} = 1; #}, 'PrefChoose' => string('SETUP_PLUGIN_SCROBBLER_ACCOUNTS'), }, 'plugin_scrobbler_auto_submit' => { 'validate' => \&Slim::Utils::Validate::trueFalse , 'options' => { '1' => string('ON'), '0' => string('OFF') }, 'prefHead' => '', }, 'plugin_scrobbler_submit_tags' => { 'validate' => \&Slim::Utils::Validate::trueFalse , 'options' => { '1' => string('ON'), '0' => string('OFF') }, }, 'plugin_scrobbler_track_history_size' => { 'validate' => \&Slim::Utils::Validate::isInt, 'prefHead' => '', }, ); return (\%group, \%prefs); } sub setupPlayerPrefs { my %group = ( PrefOrder => [ 'plugin_scrobbler_userid', 'plugin_scrobbler_enabled', ], PrefsInTable => 1, GroupHead => string('SETUP_GROUP_PLUGIN_SCROBBLER'), GroupDesc => string('SETUP_GROUP_PLUGIN_SCROBBLER_PLAYER_DESC'), GroupLine => 1, GroupSub => 1, Suppress_PrefSub => 1, Suppress_PrefLine => 1, Suppress_PrefHead => 1, ); my $options = getAccountNameHash(); my %prefs = ( 'plugin_scrobbler_userid' => { options => $options, optionsSort => 'KR', inputTemplate => 'setup_input_sel.html', 'PrefChoose' => string('SETUP_PLUGIN_SCROBBLER_ACCOUNTS'), }, 'plugin_scrobbler_enabled' => { 'validate' => \&Slim::Utils::Validate::trueFalse ,'options' => { '1' => string('ON') ,'0' => string('OFF') }, 'PrefChoose' => string('SETUP_PLUGIN_SCROBBLER_ENABLE_PLAYER'), }, ); return (\%group, \%prefs); } sub getAccountNameHash { my $optionsRef = Slim::Utils::Prefs::get("plugin_scrobbler_accounts"); my $options = {}; $options->{""} = "Default"; map { my ($acc,$pass) = ($_ =~ /(.*?):(.*)/); $options->{$acc} = $acc; } @$optionsRef; #print Dumper $options; return $options; } # Hack to workaround slimserver's memory of recently-set preferences my $freshUseridHack=0; sub initSetting { my $setting = shift; my $default = shift; if (!Slim::Utils::Prefs::isDefined($setting)) { my $oldSetting = $setting; $oldSetting =~ s/^plugin_//; $oldSetting =~ s/_/-/g; if (Slim::Utils::Prefs::isDefined($oldSetting)) { my $value = Slim::Utils::Prefs::get($oldSetting); Slim::Utils::Prefs::set($setting, $value); Slim::Utils::Prefs::delete($oldSetting); scrobbleMsg("Migrating $oldSetting to $setting - value is $value\n"); } else { Slim::Utils::Prefs::set($setting, $default); scrobbleMsg("Setting $setting to default value $default\n"); } } } # Handshake to keep a record of recent tracks sub noteNewTrack { my ($artist,$title) = @_; #scrobbleMsg("Noting new track $artist $title\n"); return unless ($artist); if (@$Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory and ($artist eq $Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory->[0]->[0])) { # Not a new artist, possible track change, so just record that scrobbleMsg("Current artist just updating $title\n"); $Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory->[0]->[1] = $title; return; } #scrobbleMsg("Adding to list\n"); unshift @{$Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory}, [$artist,$title]; # only keep configured number of tracks while (scalar(@{$Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory}) > Slim::Utils::Prefs::get('plugin_scrobbler_track_history_size')) { pop @{$Plugins::SlimScrobbler::Plugin::inMemoryTrackHistory}; #scrobbleMsg("Removed item from list\n"); } } # For remote streams we register on changes in the title string sub malcolmTitleCallback { my ($url,$title) = @_; my $lastTitle = $Plugins::SlimScrobbler::Plugin::STREAM_TO_TITLE_MAP->{$url}->{TITLE}; if ($lastTitle ne $title) { # Title has changed my $currentTime = time(); my $secsSinceLastTitleChange = $currentTime - $Plugins::SlimScrobbler::Plugin::STREAM_TO_TITLE_MAP->{$url}->{TIME}; $Plugins::SlimScrobbler::Plugin::STREAM_TO_TITLE_MAP->{$url}->{TITLE} = $title; $Plugins::SlimScrobbler::Plugin::STREAM_TO_TITLE_MAP->{$url}->{TIME} = $currentTime; scrobbleMsg("Url: $url\n"); scrobbleMsg("Title: $lastTitle\n"); scrobbleMsg("Duration: $secsSinceLastTitleChange\n"); if ($secsSinceLastTitleChange > $trackVars->{STREAM_PLAY_DURATION} && $secsSinceLastTitleChange < $trackVars->{SCROBBLE_MAXIMUM_LENGTH}) { scrobbleMsg("READY TO SUBMIT\n"); # Find all valid user id and password combinations for players using this url my $accPass = {}; for my $client (Slim::Player::Client::clients()) { if ((Slim::Player::Playlist::song($client) || '') eq $url) { my ($userID, $password) = getUserIDPasswordForClient($client); $accPass->{$userID} = $password; } } # now loop through the users to submit the tracks while ( my ($userID, $password) = each %$accPass) { my $session = getSessionForUserIDPassword($userID, $password); # make a track object if ($lastTitle =~ /(.*) - (.*)/) { my $newTrack = Scrobbler::Track->new( artist => $1, title => $2, totalLength => $secsSinceLastTitleChange, ); # by definition we are ready to submit if we are here so # force the state machine to READY. $newTrack->{state} = 'READY'; scrobbleMsg("Going to submit track: $lastTitle\n"); my $UTCtime = getUTCTimeRightNow(); $session->logTrackToAudioScrobblerAsPlayed($newTrack, $UTCtime); $newTrack->markRegistered(); # If we're autosubmit, submit now if (Slim::Utils::Prefs::get("plugin_scrobbler_auto_submit")) { $session->tryToSubmitToAudioscrobblerIfNeeded(); } } } } scrobbleMsg("Started Timing: $title\n"); } } 1; __DATA__ PLUGIN_SCROBBLER EN Audioscrobbler Submitter PLUGIN_SCROBBLER_ACTIVATED EN Audioscrobbler activated... ES Audioscrobbler activado... PLUGIN_SCROBBLER_DEACTIVATED EN Audioscrobbler deactivated... ES Audioscrobbler desactivado... PLUGIN_SCROBBLER_ENABLED EN Audioscrobbler is ON ES Audioscrobbler está ENCENDIDO PLUGIN_SCROBBLER_DISABLED EN Audioscrobbler is OFF ES Audioscrobbler está APAGADO PLUGIN_SCROBBLER_CLIENT_ENABLED EN Audioscrobbler is ON for this player ES Audioscrobbler está ENCENDIDO for this player PLUGIN_SCROBBLER_CLIENT_DISABLED EN Audioscrobbler is OFF for this player ES Audioscrobbler está APAGADO for this player PLUGIN_SCROBBLER_HIT_PLAY_TO_SUBMIT EN Press PLAY to submit now ES Presionar PLAY para enviar ahora PLUGIN_SCROBBLER_SUBMITTING EN Submitting data to Audioscrobbler... ES Enviando datos a Audioscrobbler... PLUGIN_SCROBBLER_USERNAME EN Last.FM/Audioscrobbler Username ES Nombre de Usuario de Last.FM/Audioscrobbler PLUGIN_SCROBBLER_MAX_PENDING EN Max Pending Requests ES Máx Pedidos Pendientes PLUGIN_SCROBBLER_MAX_PENDING_2 EN Audioscrobbler - Max Pending Requests ES Audioscrobbler - Máx Pedidos Pendientes PLUGIN_SCROBBLER_BAD_CONFIG EN Please set username and password ES Por favor, indicar nombre de usuario y contraseña PLUGIN_SCROBBLER_OF EN of ES de PLUGIN_SCROBBLER_SELECT_ACCOUNT EN Select Audioscrobbler Account ES Elegir Cuenta de Audioscrobbler PLUGIN_SCROBBLER_USE_DEFAULT_ACCOUNT EN Use Server Default ES Utilizar Cuenta por Defecto del Servidor PLUGIN_SCROBBLER_NONE EN None ES Ninguno PLUGIN_SCROBBLER_PASSWORD_FOR EN Password for ES Contraseña para PLUGIN_SCROBBLER_DEFAULT_ACCOUNT EN Account to use for new players ES Cuenta para usar con nuevos reproductores PLUGIN_SCROBBLER_NEW_USERID EN Add new username ES Añadir Nuevo Nombre de Usuario PLUGIN_SCROBBLER_NEW_USERID_2 EN Audioscrobbler - new username added ES Audioscrobbler - nuevo nombre de usuario añadido PLUGIN_SCROBBLER_NEW_PASSWORD EN Password for new username ES Contraseña para el nuevo Nombre de usuario PLUGIN_SCROBBLER_NEW_PASSWORD_2 EN Audioscrobbler - Password for new username ES Audioscrobbler - Contraseña para el nuevo Nombre de usuario PLUGIN_SCROBBLER_DESC_MULTIUSERID EN Use the 'Add new username' field to configure an Audioscrobbler account. You can then use the player's on-screen menu to select which account to use for that player. Delete an account by clearing the password field. ES Utilizar el campo 'Añadir Nuevo Nombre de Usuario' para configurar una cuenta de Audioscrobbler. Luego, se puede utilizar el menú en pantalla del reproductor para elegir qué cuenta utilizar para ese reproductor. Para borrar una cuenta, dejar en blanco el campo Contraseña. SETUP_GROUP_PLUGIN_SCROBBLER EN Last.FM / Audioscrobbler SETUP_GROUP_PLUGIN_SCROBBLER_DESC EN Choose your settings for the SlimScrobbler. ES Elija su configuración para SlimScrobbler SETUP_PLUGIN_SCROBBLER_ACCOUNTS EN Account SETUP_PLUGIN_SCROBBLER_ACCOUNTS_DESC EN Accounts should be entered as 'account:password', the first account entered is the default in all cases. SETUP_GROUP_PLUGIN_SCROBBLER_PLAYER_DESC EN Select the account to use on this player SETUP_PLUGIN_SCROBBLER_ACCOUNTS_OK EN SlimScrobbler User accounts and passwords changed SETUP_PLUGIN_SCROBBLER_AUTO_SUBMIT EN Submit Automatically ES Enviar Automáticamente SETUP_PLUGIN_SCROBBLER_AUTO_SUBMIT_CHOOSE EN Submit Automatically ES Enviar Automáticamente SETUP_PLUGIN_SCROBBLER_AUTO_SUBMIT_OK EN Audioscrobbler - AutoSubmit ES Audioscrobbler - AutoEnviar SETUP_PLUGIN_SCROBBLER_TRACK_HISTORY_SIZE EN History Size SETUP_PLUGIN_SCROBBLER_TRACK_HISTORY_SIZE_CHOOSE EN Number of artists to hold in memory for similar artist selection SETUP_PLUGIN_SCROBBLER_SUBMIT_TAGS EN Submit Tags SETUP_PLUGIN_SCROBBLER_SUBMIT_TAGS_DESC EN Track genres can be submitted as LastFM tags. This functionality requires background submitting. SETUP_PLUGIN_SCROBBLER_SUBMIT_TAGS_CHOOSE EN Submit Genre Tags SETUP_PLUGIN_SCROBBLER_ENABLE_PLAYER EN Enable SlimScrobbler on this player