Index: Slim/Buttons/TrackInfo.pm
===================================================================
--- Slim/Buttons/TrackInfo.pm	(revision 17118)
+++ Slim/Buttons/TrackInfo.pm	(working copy)
@@ -30,6 +30,9 @@
 
 our %functions = ();
 
+my %infoProvider;
+my @infoOrdering;
+
 # button functions for track info screens
 sub init {
 
@@ -50,6 +53,11 @@
 			playOrAdd($client,$addOrInsert);
 		},
 	);
+	
+	# Our information providers are pluggable, call the 
+	# registerInfoProvider function to extend the details
+	# provided in the track info menu.
+	registerDefaultInfoProviders();
 }
 
 sub cliQuery {
@@ -229,184 +237,28 @@
 		return;
 	}
 	
-	# If Audioscrobbler is enabled and the current track can be scrobbled,
-	# add 'Last.fm: Love this track' as the first item
-	if ( Slim::Utils::PluginManager->isEnabled( 'Slim::Plugin::AudioScrobbler::Plugin' ) ) {
-		if ( Slim::Plugin::AudioScrobbler::Plugin->canScrobble( $client, $track ) ) {
-			push @{$client->trackInfoLines}, $client->string('PLUGIN_AUDIOSCROBBLER_LOVE_TRACK');
-			push @{$client->trackInfoContent}, sub {
-				my $client = shift;
-				
-				$client->execute( [ 'audioscrobbler', 'loveTrack', $track->url ] );
-				
-				$client->showBriefly( {
-					line => [ 
-						$client->string('PLUGIN_AUDIOSCROBBLER_LOVE_TRACK'), 
-						$client->string('PLUGIN_AUDIOSCROBBLER_TRACK_LOVED'),
-					],
-				} );
-			};
-		}
+	# If we don't have an ordering, generate one.
+	# This will be triggered every time a change is made to the
+	# registered information providers, but only then. After
+	# that, we will have our ordering and only need to step
+	# through it.
+	if (scalar(@infoOrdering) == 0)
+	{
+		# We don't know what order the entries should be in,
+		# so work that out.
+		generateInfoOrderingItem($client, 'top',    undef);
+		generateInfoOrderingItem($client, 'middle', undef);
+		generateInfoOrderingItem($client, 'bottom', undef);
 	}
-
-	if (my $title = $track->title) {
-		push (@{$client->trackInfoLines}, $client->string('TITLE') . ": $title");
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	# Loop through the contributor types and append
-	for my $role (sort $track->contributorRoles) {
-
-		for my $contributor ($track->contributorsOfType($role)) {
-
-			push (@{$client->trackInfoLines}, sprintf('%s: %s', $client->string(uc($role)), $contributor->name));
-			push (@{$client->trackInfoContent}, {
-				'type' => uc($role),
-				'obj'  => $contributor,
-			});
-		}
-	}
-
-	# Used below for ReplayGain
-	my $album = $track->album;
-
-	if ($album) {
-		push (@{$client->trackInfoLines}, join(': ', $client->string('ALBUM'), $album->name));
-		push (@{$client->trackInfoContent}, {
-			'type' => 'ALBUM',
-			'obj'  => $album,
-		});
-	}
-
-	if (my $tracknum = $track->tracknum) {
-		push (@{$client->trackInfoLines}, $client->string('TRACK') . ": $tracknum");
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (my $year = $track->year) {
-		push (@{$client->trackInfoLines}, $client->string('YEAR') . ": $year");
-		push (@{$client->trackInfoContent}, {
-			'type' => 'YEAR',
-			'obj'  => $year,
-		});
-	}
-
-	for my $genre ($track->genres) {
-
-		push (@{$client->trackInfoLines}, join(': ', $client->string('GENRE'), $genre->name));
-		push (@{$client->trackInfoContent}, {
-			'type' => 'GENRE',
-			'obj'  => $genre,
-		});
-	}
-
-	if (my $ct = Slim::Schema->contentType($track)) {
-		push (@{$client->trackInfoLines}, $client->string('TYPE') . ": " . $client->string(uc($ct)));
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (my $comment = $track->comment) {
-		push (@{$client->trackInfoLines}, $client->string('COMMENT') . ": $comment");
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (my $duration = $track->duration) {
-		push (@{$client->trackInfoLines}, $client->string('LENGTH') . ": $duration");
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (my $replaygain = $track->replay_gain) {
-		push (@{$client->trackInfoLines}, $client->string('REPLAYGAIN') . ": " . sprintf("%2.2f",$replaygain) . " dB");
-		push (@{$client->trackInfoContent}, undef);
-	}
 	
-	if (my $rating = $track->rating) {
-		push (@{$client->trackInfoLines}, $client->string('RATING') . ": " . sprintf("%d",$rating) . " /100");
-		push (@{$client->trackInfoContent}, undef);
-	}
-	
-	if (blessed($album) && $album->can('replay_gain')) {
-
-		if (my $albumreplaygain = $album->replay_gain) {
-			push (@{$client->trackInfoLines}, $client->string('ALBUMREPLAYGAIN') . ": " . sprintf("%2.2f",$albumreplaygain) . " dB");
-			push (@{$client->trackInfoContent}, undef);
+	# Now run the order, which generates all the items we need.
+	for my $ref (@infoOrdering)
+	{
+		if (defined $ref->{'func'})
+		{
+			$ref->{'func'}($client, $ref->{'private'}, $url, $track);
 		}
 	}
-
-	if ( my $bitrate = ( Slim::Music::Info::getCurrentBitrate($track->url) || $track->prettyBitRate ) ) {
-		
-		# A bitrate of -1 is set by Scanner::scanBitrate or Formats::*::scanBitrate when the
-		# bitrate of a remote stream can't be determined
-		if ( $bitrate ne '-1' ) {
-			my $undermax = Slim::Player::TranscodingHelper::underMax($client, $track->url);
-			my $rate     = $bitrate;
-			my $convert  = '';
-
-			if (!$undermax) {
-
-				$rate = Slim::Utils::Prefs::maxRate($client) . $client->string('KBPS') . " ABR";
-			}
-
-			if ($client->modeParam('current') && (defined $undermax && !$undermax)) { 
-
-				$convert = sprintf('(%s %s)', $client->string('CONVERTED_TO'), $rate);
-			}
-
-			push (@{$client->trackInfoLines}, sprintf("%s: %s %s",
-				$client->string('BITRATE'), $bitrate, $convert,
-			));
-
-			push (@{$client->trackInfoContent}, undef);
-		}
-	}
-
-	if ($track->samplerate) {
-		push (@{$client->trackInfoLines}, $client->string('SAMPLERATE') . ": " . $track->prettySampleRate);
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if ($track->samplesize) {
-		push (@{$client->trackInfoLines}, $client->string('SAMPLESIZE') . ": " . $track->samplesize . " " . $client->string('BITS'));
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (my $len = $track->filesize) {
-		push (@{$client->trackInfoLines}, $client->string('FILELENGTH') . ": " . Slim::Utils::Misc::delimitThousands($len));
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if ( !Slim::Music::Info::isRemoteURL($track->url) ) {
-		if (my $age = $track->modificationTime) {
-			push (@{$client->trackInfoLines}, $client->string('MODTIME').": $age");
-			push (@{$client->trackInfoContent}, undef);
-		}
-	}
-
-	if (my $url = $track->url) {
-		push (@{$client->trackInfoLines}, "URL: ". Slim::Utils::Misc::unescape($url));
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (my $tag = $track->tagversion) {
-		push (@{$client->trackInfoLines}, $client->string('TAGVERSION') . ": $tag");
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if ($track->drm) {
-		push (@{$client->trackInfoLines}, $client->string('DRM'));
-		push (@{$client->trackInfoContent}, undef);
-	}
-
-	if (Slim::Music::Info::isURL($track->url) && Slim::Utils::Favorites->enabled) {
-
-		$client->modeParam( 'favorite', Slim::Utils::Favorites->new($client)->findUrl($track->url) );
-
-		push (@{$client->trackInfoLines}, 'FAVORITE'); # replaced in lines()
-		push (@{$client->trackInfoContent}, {
-			'type' => 'FAVORITE',
-			'obj'  => '',
-		});
-	}
 }
 
 sub listExitHandler {
@@ -518,34 +370,6 @@
 				'selectionCriteria' => $selectionCriteria,
 			});
 
-		} elsif ($curType eq 'FAVORITE') {
-
-			my $favorites = Slim::Utils::Favorites->new($client);
-			my $favIndex = $client->modeParam('favorite');
-
-			if (!defined $favIndex) {
-
-				$favIndex = $favorites->add(track($client), $track->title || $track->url);
-
-				$client->showBriefly( {
-					'line' => [ $client->string('FAVORITES_ADDING'), $track->title || $track->url ]
-				   });
-
-				$client->modeParam('favorite', $favIndex);
-
-			} else {
-
-				# Bug 6177, Menu to confirm favorite removal
-				Slim::Buttons::Common::pushModeLeft( $client, 'favorites.delete', {
-					title => $track->title || $track->url,
-					index => $favIndex,
-					depth => 2,
-				} );
-				
-			}
-
-			$push = 0;
-
 		} else {
 
 			$push = 0;
@@ -563,21 +387,11 @@
 
 	# 2nd line's content is provided entirely by trackInfoLines, which returns an array of information lines
 	my $line2 = $client->trackInfoLines->[$index];
-
-	# special case favorites line, which must be determined dynamically
-	if ($line2 eq 'FAVORITE') {
-		my $favIndex = $client->modeParam('favorite');
-		if (!defined $favIndex) {
-			$line2 = $client->string('FAVORITES_RIGHT_TO_ADD');
-		} else {
-			if ($favIndex =~ /^\d+$/) {
-				# existing favorite at top level - display favorite number starting at 1 (favs are zero based)
-				$line2 = $client->string('FAVORITES_FAVORITE') . ' ' . ($favIndex + 1);
-			} else {
-				# existing favorite not at top level - don't display number
-				$line2 = $client->string('FAVORITES_FAVORITE');
-			}
-		}
+	
+	if (ref $line2 eq 'CODE')
+	{
+		# Dynamic line; so call the function to obtain the details
+		$line2 = &$line2($client);
 	}
 
 	return $line2;
@@ -598,6 +412,633 @@
 	return ($overlay1, $overlay2);
 }
 
+##
+# Register an information provider with the current providers
+#
+# @param[in]  $name     The name of the provider
+# @param[in]  %details  A hashref of the information required by the
+#                       provider
+#                         after   => any provider which this info is after
+#                         before  => any provider which this info is before
+#                         isa     => any provider which this is a member of
+#                         func    => the function to call to provide info
+#                         private => a private value to pass to the function
+sub registerInfoProvider
+{
+	my ($name, %details) = @_;
+
+	$details{'name'} = $name; # For diagnostic purposes
+	if (!defined $details{'after'} &&
+	    !defined $details{'before'} &&
+	    !defined $details{'isa'})
+	{
+		# If they didn't say anything about where it goes,
+		# place it in the middle.
+		$details{'isa'} = 'middle';
+	}
+	$infoProvider{$name} = \%details;
+
+	# Clear the array to force it to be rebuilt
+	@infoOrdering = ();
+}
+
+
+##
+# Deregister an information provider
+#
+# @param[in]  $name     The name of the provider
+sub deregisterInfoProvider
+{
+	my ($name);
+	delete $infoProvider{$name};
+
+	# Clear the array to force it to be rebuilt
+	@infoOrdering = ();
+}
+
+
+##
+# Add information to the current list of items.
+#
+# @param[in]  $client  The client this request is coming from
+# @param[in]  $line    The text line to add, or may be a coderef in order
+#                      that the line be determined on each execution
+# @param[in]  $content The operation to perform when this item is selected.
+#                      May be a hashref of information to search for:
+#                         'type' => a type of search:
+#                                       ALBUM = album+track search
+#                                       <role> = contributor role search
+#                                       GENRE = genre+contrib+album+track search
+#                                       YEAR  = year+album+track search
+#                         'obj' => the value searched
+#                     Or may be a code ref to execute the code in the form
+#                         func($client);
+#                     Or may be undef to never push right.
+sub addInfo
+{
+	my $client = shift;
+	my $line = shift;
+	my $content = shift;
+	
+	push (@{$client->trackInfoLines}, $line);
+	push (@{$client->trackInfoContent}, $content);
+}
+
+##
+# Adds an item to the ordering list, following any
+# 'after', 'before' and 'isa' requirements that the
+# registered providers have requested.
+#
+# @param[in]  $client   The client we're ordering for
+# @param[in]  $name     The name of the item to add
+# @param[in]  $previous The item before this one, for 'before' processing
+sub generateInfoOrderingItem
+{
+	my $client = shift;
+	my $name = shift;
+	my $previous = shift;
+	my $item;
+	
+	# Check for the 'before' items which are 'after' the last item
+	if (defined $previous)
+	{
+		for $item (sort { $a cmp $b }
+				grep {
+					defined $infoProvider{$_}->{'after'} &&
+					$infoProvider{$_}->{'after'} eq $previous &&
+					defined $infoProvider{$_}->{'before'} &&
+					$infoProvider{$_}->{'before'} eq $name
+				} keys %infoProvider)
+		{
+			&generateInfoOrderingItem($client, $item, $previous);
+		}
+	}
+	
+	# Now the before items which are just before this item
+	for $item (sort { $a cmp $b }
+			grep {
+				!defined $infoProvider{$_}->{'after'} &&
+				defined $infoProvider{$_}->{'before'} &&
+				$infoProvider{$_}->{'before'} eq $name
+			} keys %infoProvider)
+	{
+		&generateInfoOrderingItem($client, $item, $previous);
+	}
+
+	# Add the item itself
+	push @infoOrdering, $infoProvider{$name};
+	
+	# Now any items that are members of the group
+	for $item (sort { $a cmp $b }
+			grep {
+				defined $infoProvider{$_}->{'isa'} &&
+				$infoProvider{$_}->{'isa'} eq $name
+			} keys %infoProvider)
+	{
+		&generateInfoOrderingItem($client, $item, undef);
+	}
+	
+	# Any 'after' items
+	for $item (sort { $a cmp $b }
+			grep {
+				defined $infoProvider{$_}->{'after'} &&
+				$infoProvider{$_}->{'after'} eq $name &&
+				!defined $infoProvider{$_}->{'before'}
+			} keys %infoProvider)
+	{
+		&generateInfoOrderingItem($client, $item, $name);
+	}
+}
+
+
+##
+# Add the 'title' information to the information.
+#
+# Should call addInfo to add information to the menu.
+#
+# @param[in]  $client  The client this request is coming from
+# @param[in]  $private A private value passed on registration
+# @param[in]  $url     The URL to which this information refers
+# @param[in]  $track   The track reference
+sub infoTitle
+{
+	my ($client, $private, $url, $track) = @_;
+
+	if (my $title = $track->title)
+	{
+		addInfo($client, 
+			$client->string('TITLE') . ": $title",
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoAlbum
+{
+	my ($client, $private, $url, $track) = @_;
+	my $album = $track->album;
+
+	if ($album) {
+		addInfo($client,
+			join(': ', $client->string('ALBUM'), $album->name),
+			{
+				'type' => 'ALBUM',
+				'obj'  => $album,
+			});
+	}
+}
+
+##
+# See infoTitle
+sub infoContributors
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	# Loop through the contributor types and append
+	for my $role (sort $track->contributorRoles) {
+
+		for my $contributor ($track->contributorsOfType($role)) {
+
+			addInfo($client,
+				sprintf('%s: %s', $client->string(uc($role)), $contributor->name),
+				{
+					'type' => uc($role),
+					'obj'  => $contributor,
+				});
+		}
+	}
+}
+
+##
+# See infoTitle
+sub infoTrackNum
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $tracknum = $track->tracknum) {
+		addInfo($client,
+			$client->string('TRACK') . ": $tracknum",
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoYear
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $year = $track->year) {
+		addInfo($client,
+			$client->string('YEAR') . ": $year",
+			{
+				'type' => 'YEAR',
+				'obj'  => $year,
+			});
+	}
+}
+
+##
+# See infoTitle
+sub infoGenres
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	for my $genre ($track->genres) {
+		addInfo($client,
+			join(': ', $client->string('GENRE'), $genre->name),
+			{
+				'type' => 'GENRE',
+				'obj'  => $genre,
+			});
+	}
+}
+
+##
+# See infoTitle
+sub infoContentType
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $ct = Slim::Schema->contentType($track)) {
+		addInfo($client,
+			$client->string('TYPE') . ": " . $client->string(uc($ct)),
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoComment
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $comment = $track->comment) {
+		addInfo($client,
+			$client->string('COMMENT') . ": $comment",
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoDuration
+{
+	my ($client, $private, $url, $track) = @_;
+
+	if (my $duration = $track->duration) {
+		addInfo($client,
+			$client->string('LENGTH') . ": $duration",
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoReplayGain
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	# It makes sense to keep the replaygain and album replaygain
+	# together. Under earlier vrsions they were separated by the
+	# rating, which looked strange.
+	my $album = $track->album;
+
+	if (my $replaygain = $track->replay_gain) {
+		addInfo($client,
+			$client->string('REPLAYGAIN') . ": " . sprintf("%2.2f",$replaygain) . " dB",
+			undef);
+	}
+	
+	if (blessed($album) && $album->can('replay_gain')) {
+		if (my $albumreplaygain = $album->replay_gain) {
+			addInfo($client,
+				$client->string('ALBUMREPLAYGAIN') . ": " . sprintf("%2.2f",$albumreplaygain) . " dB",
+				undef);
+		}
+	}
+}
+
+##
+# See infoTitle
+sub infoRating
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $rating = $track->rating) {
+		addInfo($client,
+			$client->string('RATING') . ": " . sprintf("%d",$rating) . " /100",
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoBitRate
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if ( my $bitrate = ( Slim::Music::Info::getCurrentBitrate($track->url) || $track->prettyBitRate ) ) {
+		
+		# A bitrate of -1 is set by Scanner::scanBitrate or Formats::*::scanBitrate when the
+		# bitrate of a remote stream can't be determined
+		if ( $bitrate ne '-1' ) {
+			my $undermax = Slim::Player::TranscodingHelper::underMax($client, $track->url);
+			my $rate     = $bitrate;
+			my $convert  = '';
+
+			if (!$undermax) {
+
+				$rate = Slim::Utils::Prefs::maxRate($client) . $client->string('KBPS') . " ABR";
+			}
+
+			if ($client->modeParam('current') && (defined $undermax && !$undermax)) { 
+
+				$convert = sprintf('(%s %s)', $client->string('CONVERTED_TO'), $rate);
+			}
+
+			addInfo($client,
+				sprintf("%s: %s %s", $client->string('BITRATE'), $bitrate, $convert),
+				undef);
+		}
+	}
+}
+
+##
+# See infoTitle
+sub infoSampleRate
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if ($track->samplerate) {
+		addInfo($client,
+			$client->string('SAMPLERATE') . ": " . $track->prettySampleRate,
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoSampleSize
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if ($track->samplesize) {
+		addInfo($client,
+			$client->string('SAMPLESIZE') . ": " . $track->samplesize . " " . $client->string('BITS'),
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoFileSize
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $len = $track->filesize) {
+		addInfo($client,
+			$client->string('FILELENGTH') . ": " . Slim::Utils::Misc::delimitThousands($len),
+			undef);
+	}
+}
+
+##
+# See infoTitle
+sub infoFileModTime
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if ( !Slim::Music::Info::isRemoteURL($track->url) ) {
+		if (my $age = $track->modificationTime) {
+			addInfo($client,
+				$client->string('MODTIME').": $age",
+				undef);
+		}
+	}
+}
+
+##
+# See infoTitle
+sub infoUrl
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (my $turl = $track->url) {
+		addInfo($client,
+			"URL: ". Slim::Utils::Misc::unescape($turl),
+			undef);
+	}
+}
+
+
+##
+# See infoTitle
+sub infoTagVersion
+{
+	my ($client, $private, $url, $track) = @_;
+
+	if (my $tag = $track->tagversion) {
+		addInfo($client,
+			$client->string('TAGVERSION') . ": $tag",
+			undef);
+	}
+}
+
+
+##
+# See infoTitle
+sub infoDRM
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if ($track->drm) {
+		addInfo($client,
+			$client->string('DRM'),
+			undef);
+	}
+}
+
+
+##
+# See infoTitle
+# (it's possible this function should live in the
+# Favorites plugin, not here).
+sub infoFavorite
+{
+	my ($client, $private, $url, $track) = @_;
+	
+	if (Slim::Music::Info::isURL($track->url) && Slim::Utils::Favorites->enabled) {
+
+		$client->modeParam( 'favorite', Slim::Utils::Favorites->new($client)->findUrl($track->url) );
+
+		addInfo($client,
+		
+			# We dynamically determine the line's text for favorites
+			sub {
+				my $client = shift;
+				my $line;
+				my $favIndex = $client->modeParam('favorite');
+				if (!defined $favIndex) {
+					$line = $client->string('FAVORITES_RIGHT_TO_ADD');
+				} else {
+					if ($favIndex =~ /^\d+$/) {
+						# existing favorite at top level - display favorite number starting at 1 (favs are zero based)
+						$line = $client->string('FAVORITES_FAVORITE') . ' ' . ($favIndex + 1);
+					} else {
+						# existing favorite not at top level - don't display number
+						$line = $client->string('FAVORITES_FAVORITE');
+					}
+				}
+				
+				return $line;
+			},
+
+			sub {
+				my $client = shift;
+				my $favorites = Slim::Utils::Favorites->new($client);
+				my $favIndex = $client->modeParam('favorite');
+
+				if (!defined $favIndex) {
+
+					$favIndex = $favorites->add(track($client), $track->title || $track->url);
+  
+					$client->showBriefly( {
+						'line' => [ $client->string('FAVORITES_ADDING'), $track->title || $track->url ]
+					   });
+  
+					$client->modeParam('favorite', $favIndex);
+  
+				} else {
+  
+					# Bug 6177, Menu to confirm favorite removal
+					Slim::Buttons::Common::pushModeLeft( $client, 'favorites.delete', {
+						title => $track->title || $track->url,
+						index => $favIndex,
+						depth => 2,
+					} );
+					
+				}
+			} );
+	}
+}
+
+
+##
+# Register all the information providers that we provide.
+#
+sub registerDefaultInfoProviders
+{
+	# The 'top', 'middle' and 'bottom' groups, so that we can add items in absolute
+	# positions
+	registerInfoProvider('top',    ('isa' => '') );
+	registerInfoProvider('middle', ('isa' => '') );
+	registerInfoProvider('bottom', ('isa' => '') );
+	
+	registerInfoProvider('title', (
+					'after'  => 'top',
+					'func'   => \&infoTitle
+				));
+	
+	registerInfoProvider('contributors', (
+					'after' => 'title',
+					'func'  => \&infoContributors
+				));
+	
+	registerInfoProvider('album', (
+					'after' => 'contributors',
+					'func'  => \&infoAlbum
+				));
+	
+	registerInfoProvider('tracknum', (
+					'after' => 'album',
+					'func'  => \&infoTrackNum
+				));
+	
+	registerInfoProvider('year', (
+					'after' => 'tracknum',
+					'func'  => \&infoYear
+				));
+	
+	registerInfoProvider('genres', (
+					'after' => 'year',
+					'func'  => \&infoGenres
+				));
+	
+	registerInfoProvider('type', (
+					'after' => 'genres',
+					'func'  => \&infoContentType
+				));
+	
+	registerInfoProvider('comment', (
+					'after' => 'type',
+					'func'  => \&infoComment
+				));
+	
+	registerInfoProvider('duration', (
+					'after' => 'comment',
+					'func'  => \&infoDuration
+				));
+	
+	registerInfoProvider('replaygain', (
+					'after' => 'duration',
+					'func'  => \&infoReplayGain
+				));
+	
+	registerInfoProvider('rating', (
+					'after' => 'replaygain',
+					'func'  => \&infoRating
+				));
+	
+	registerInfoProvider('formatinfo', (
+					'after' => 'rating'
+				));
+	
+	registerInfoProvider('fileinfo', (
+					'after' => 'formatinfo'
+				));
+	
+	registerInfoProvider('favorite', (
+					'after' => 'fileinfo',
+					'func'  => \&infoFavorite
+				));
+	
+	# File information
+	registerInfoProvider('filesize', (
+					'isa' => 'fileinfo',
+					'func'  => \&infoFileSize
+				));
+	
+	registerInfoProvider('modtime', (
+					'isa' => 'fileinfo',
+					'func'  => \&infoFileModTime
+				));
+	
+	registerInfoProvider('url', (
+					'isa' => 'fileinfo',
+					'func'  => \&infoUrl
+				));
+	
+	# Format information
+	registerInfoProvider('bitrate', (
+					'isa'   => 'formatinfo',
+					'func'  => \&infoBitRate
+				));
+	registerInfoProvider('samplerate', (
+					'isa'   => 'formatinfo',
+					'func'  => \&infoSampleRate
+				));
+	registerInfoProvider('samplesize', (
+					'isa'   => 'formatinfo',
+					'func'  => \&infoSampleSize
+				));
+	registerInfoProvider('drm', (
+					'isa'   => 'formatinfo',
+					'func'  => \&infoDRM
+				));
+}
+
 =head1 SEE ALSO
 
 L<Slim::Buttons::Common>
Index: Slim/Plugin/AudioScrobbler/Plugin.pm
===================================================================
--- Slim/Plugin/AudioScrobbler/Plugin.pm	(revision 17118)
+++ Slim/Plugin/AudioScrobbler/Plugin.pm	(working copy)
@@ -79,6 +79,13 @@
 	
 	Slim::Control::Request::addDispatch(['audioscrobbler', 'banTrack', '_url'],
 		[0, 1, 1, \&banTrack]);
+	
+	# Register the 'love this track' option in the TrackInfo menu
+	Slim::Buttons::TrackInfo::registerInfoProvider('audioscrobbler',
+		(
+			'isa' => 'top',
+			'func' => \&infoLoveTrack
+		));
 }
 
 sub shutdownPlugin {
@@ -1095,4 +1102,41 @@
 	return;
 }
 
+
+
+##
+# Mark the current track as being 'loved'.
+#
+# Should call addInfo to add information to the menu.
+#
+# @param[in]  $client  The client this request is coming from
+# @param[in]  $private A private value passed on registration
+# @param[in]  $url     The URL to which this information refers
+# @param[in]  $track   The track reference
+sub infoLoveTrack
+{
+	my ($client, $private, $url, $track) = @_;
+	# If Audioscrobbler is enabled and the current track can be scrobbled,
+	# add 'Last.fm: Love this track' as the first item
+	if ( Slim::Utils::PluginManager->isEnabled( 'Slim::Plugin::AudioScrobbler::Plugin' ) ) {
+		if ( Slim::Plugin::AudioScrobbler::Plugin->canScrobble( $client, $track ) ) {
+			Slim::Buttons::TrackInfo::addInfo($client,
+				$client->string('PLUGIN_AUDIOSCROBBLER_LOVE_TRACK'),
+				sub {
+					my $client = shift;
+				
+					$client->execute( [ 'audioscrobbler', 'loveTrack', $track->url ] );
+					
+					$client->showBriefly( {
+						line => [ 
+							$client->string('PLUGIN_AUDIOSCROBBLER_LOVE_TRACK'), 
+							$client->string('PLUGIN_AUDIOSCROBBLER_TRACK_LOVED'),
+						],
+					} );
+				}
+				);
+		}
+	}
+}
+
 1;
