Index: Slim/Control/Queries.pm
===================================================================
--- Slim/Control/Queries.pm	(revision 17616)
+++ Slim/Control/Queries.pm	(working copy)
@@ -41,6 +41,7 @@
 use Slim::Utils::Log;
 use Slim::Utils::Unicode;
 use Slim::Utils::Prefs;
+use Slim::Schema::ResultSet::Contributor;
 
 {
 	if ($^O =~ /Win32/) {
@@ -160,7 +161,9 @@
 	my $menu          = $request->getParam('menu');
 	my $insert        = $request->getParam('menu_all');
 	my $to_cache      = $request->getParam('cache');
-	
+	my $includeYear   = 1; # should be defined somehow
+	my $textkeyByYear = 0;
+
 	if ($request->paramNotOneOfIfDefined($sort, ['new', 'album', 'artflow'])) {
 		$request->setStatusBadParams();
 		return;
@@ -218,9 +221,31 @@
 				$where->{'contributorAlbums.contributor'} = $contributorID;
 				push @{$attr->{'join'}}, 'contributorAlbums';
 				$attr->{'distinct'} = 1;
-			}			
+			
+				# If no other specific sort has been specified, order the albums
+				# by year according to the player settings.
+				if (!$sort)
+				{
+					my $sortByYear = $Slim::Schema::ResultSet::Contributor::prefs->get('sortAlbumsByYear') || 0;
+					my $sort = $Slim::Schema::ResultSet::Contributor::albumSortRules[$sortByYear] ||
+					           $Slim::Schema::ResultSet::Contributor::albumSortRules[0];
+					$attr->{'order_by'} = $sort;
+
+					# Fix up the sort key to use the 'me' name, rather than 'album'
+					# table name which was used by the contributor album sort code.
+					$attr->{'order_by'} =~ s/album\./me./g;
+					
+					# If we are using the year, then we
+					# need to ensure that the key that's
+					# used (the background display on Jive)
+					# will be based on the year.
+					if ($attr->{'order_by'} =~ /year/) {
+						$textkeyByYear = 1;
+					}
+				}
+			}
 		}
-	
+
 		if (defined $genreID){
 			$where->{'genreTracks.genre'} = $genreID;
 			push @{$attr->{'join'}}, {'tracks' => 'genreTracks'};
@@ -240,7 +265,11 @@
 	# Jive menu mode, needs contributor data and only a subset of columns
 	if ( $menuMode ) {
 		push @{ $attr->{'join'} }, 'contributor';
-		$attr->{'cols'} = [ qw(id artwork title contributor.name titlesort) ];
+		$attr->{'cols'} = [ qw(id artwork title contributor.id contributor.name titlesort) ];
+		if ($includeYear)
+		{
+		    push @{$attr->{'cols'}}, 'year';
+		}
 	}
 	
 	# Flatten request for lookup in cache, only for Jive menu queries
@@ -355,6 +384,12 @@
 			$chunkCount = _playAll(start => $start, end => $end, chunkCount => $chunkCount, request => $request, loopname => $loopname, includeArt => 1);
 		}
 
+		# We need to know the 'No album' name so that those items
+		# which have been grouped together under it do not get the
+		# album art of the first album.
+		# It looks silly to go to Madonna->No album and see the
+		# picture of '2 Unlimited'.
+		my $noAlbumName = Slim::Utils::Strings::string('NO_ALBUM');
 
 		for my $eachitem ($rs->slice($start, $end)) {
 
@@ -364,12 +399,32 @@
 				# we want the text to be album\nartist
 				my $artist = $eachitem->contributor->name;
 				my $text   = $eachitem->title;
+
+				# If we're looking at <artist>->'No album'
+				# then the description on Jive should say
+				# that's what we're looking at, not 'Various
+				# artists'
+				if ($eachitem->contributor->id == Slim::Schema->variousArtistsObject->id && 
+				    defined $contributorID &&
+				    $contributorID != Slim::Schema->variousArtistsObject->id) {
+					my $obj = Slim::Schema->single('Contributor', { 'id' => $contributorID });
+					if (defined $obj) {
+						$artist = $obj->name;
+					}
+				}
 				if (defined $artist) {
 					$text = $text . "\n" . $artist;
 				}
 
 				my $favorites_title = $text;
 				$favorites_title =~ s/\n/ - /g;
+				
+				if (defined $includeYear &&
+				    defined $eachitem->year &&
+				    $eachitem->year != 0)
+				{
+					$text .= "\n(" . $eachitem->year . ")";
+				}
 
 				$request->addResultLoop($loopname, $chunkCount, 'text', $text);
 				
@@ -381,6 +436,31 @@
 				# title is a really stupid thing to use, since there's no assurance it's unique
 				my $url = 'db:album.titlesearch=' . $eachitem->title;
 
+				# If they specified a contributor, we need to restrict to JUST that contributor
+				# otherwise 'Madonna' -> 'No album' will show everything that doesn't have an album,
+				# rather than everything that's by Madonna but which isn't in an album.
+				if (defined $contributorID)
+				{
+					my $obj = Slim::Schema->single('Contributor', { 'id' => $contributorID });
+					if (defined $obj)
+					{
+						$url .= "&contributor.namesearch=" . $obj->name;
+					}
+				}
+				if (defined $genreID)
+				{
+					my $obj = Slim::Schema->single('Genre', { 'id' => $genreID });
+					if (defined $obj)
+					{
+						$url .= "&genre.namesearch=" . $obj->name;
+					}
+				}
+				if (defined $year)
+				{
+					# The year id is the year
+					$url .= "&year.id=$year";
+				}
+
 				my $params = {
 					'album_id'        => $id,
 					'favorites_url'   => $url,
@@ -390,15 +470,28 @@
 				if (defined $contributorID) {
 					$params->{artist_id} = $contributorID;
 				}
+				if (defined $year) {
+					$params->{year} = $year;
+				}
 
 				unless ($sort && $sort eq 'new') {
-					$params->{textkey} = substr($eachitem->titlesort, 0, 1),
+					if ($textkeyByYear)
+					{
+						if ($eachitem->year != 0) {
+							$params->{textkey} = substr($eachitem->year, 2, 2);
+						}
+					}
+					else
+					{
+						$params->{textkey} = substr($eachitem->titlesort, 0, 1),
+					}
 				}
 
 				$request->addResultLoop($loopname, $chunkCount, 'params', $params);
 
 				# artwork if we have it
-				if (defined(my $iconId = $eachitem->artwork())) {
+				if ($eachitem->title ne $noAlbumName &&
+				    defined(my $iconId = $eachitem->artwork())) {
 					$iconId += 0;
 					$request->addResultLoop($loopname, $chunkCount, 'icon-id', $iconId);
 				}
@@ -3767,6 +3860,7 @@
 	$request->setStatusDone();
 }
 
+# might also be known as tracksQuery
 sub titlesQuery {
 	my $request = shift;
 
@@ -4148,6 +4242,11 @@
 					'favorites_title' => $id,
 				};
 
+				if ($id != 0)
+				{
+					$params->{textkey} = substr($id, 2, 2);
+				}
+
 				$request->addResultLoop($loopname, $chunkCount, 'params', $params);
 			}
 			else {
Index: Slim/Control/Commands.pm
===================================================================
--- Slim/Control/Commands.pm	(revision 17616)
+++ Slim/Control/Commands.pm	(working copy)
@@ -2821,7 +2821,8 @@
 	my $client  = shift;
 	my $url     = shift;
 
-	my $class   = 'Track';
+	my %classes;
+	my $class   = undef;
 	my $obj     = undef;
 
 	$log->debug("Begin Function");
@@ -2833,37 +2834,63 @@
 	# db:contributor.namesearch=BEATLES
 	#
 	# Remote playlists are Track objects, not Playlist objects.
-	if ($url =~ /^db:(\w+)\.(\w+)=(.+)/) {
+	if ($url =~ /^db:(\w+\.\w+=.+)$/) {
+	  
+		for my $term (split '&', $1) {
 
-		$class = ucfirst($1);
-		$obj   = Slim::Schema->single($class, { $2 => Slim::Utils::Misc::unescape($3) });
+			# If $terms has a leading &, split will generate an initial empty string
+			next if !$term;
 
+			if ($term =~ /^(\w+)\.(\w+)=(.*)$/) {
+
+				my $key   = URI::Escape::uri_unescape($2);
+				my $value = URI::Escape::uri_unescape($3);
+
+				$class = ucfirst($1);
+				$obj   = Slim::Schema->single($class, { $key => $value });
+				
+				$classes{$class} = $obj;
+			}
+		}
+
 	} elsif (Slim::Music::Info::isPlaylist($url) && !Slim::Music::Info::isRemoteURL($url)) {
 
-		$class = 'Playlist';
+		%classes = (
+			'Playlist' => Slim::Schema->rs($class)->objectForUrl({
+						'url'      => $url,
+					})
+			);
 	}
+	else {
 
-	# else we assume it's a track
-	if ($class eq 'Track' || $class eq 'Playlist') {
-
-		$obj = Slim::Schema->rs($class)->objectForUrl({
-			'url'      => $url,
-		});
+		# else we assume it's a track
+		%classes = (
+			'Track' => Slim::Schema->rs($class)->objectForUrl({
+						'url'      => $url,
+					})
+			);
 	}
 
 	# Bug 4790: we get a track object of content type 'dir' if a fileurl for a directory is passed
 	# this needs scanning so pass empty list back to playlistXitemCommand in this case
-	if (blessed($obj) && (
-		$class eq 'Album' || 
-		$class eq 'Contributor' || 
-		$class eq 'Genre' ||
-		$class eq 'Year' ||
-		($obj->can('content_type') && $obj->content_type ne 'dir'))) {
-
-		my $terms = sprintf('%s.id=%d', lc($class), $obj->id);
-
-		return _playlistXtracksCommand_parseSearchTerms($client, $terms);
-
+	my $terms = "";
+	while (($class, $obj) = each %classes)
+	{
+		if (blessed($obj) && (
+			$class eq 'Album' || 
+			$class eq 'Contributor' || 
+			$class eq 'Genre' ||
+			$class eq 'Year' ||
+			($obj->can('content_type') && $obj->content_type ne 'dir'))) {
+  
+			$terms .= "&" if ($terms ne "");
+			$terms .= sprintf('%s.id=%d', lc($class), $obj->id);
+		}
+	}
+	
+	if ($terms ne "") {
+	
+			return _playlistXtracksCommand_parseSearchTerms($client, $terms);
 	} else {
 
 		return ();
Index: Slim/Formats.pm
===================================================================
--- Slim/Formats.pm	(revision 17616)
+++ Slim/Formats.pm	(working copy)
@@ -216,6 +216,49 @@
 
 			$tags->{$key} ||= 0;
 		}
+		
+		# If there is no lyric content present in the file,
+		# see if there is a local lyric file we can use
+		# instead.
+		if (!defined($tags->{'LYRICS'}) ||
+		    $tags->{'LYRICS'} eq "")
+		{
+			my $lyricfilepath = $filepath;
+			if ($lyricfilepath =~ s/\..{2,4}$/.txt/)
+			{
+				if (open(IN, "< $lyricfilepath"))
+				{
+					my $lyrics = "";
+					while (<IN>)
+					{
+						$lyrics .= $_;
+					}
+					close(IN);
+					
+					# Try and guess the encoding, otherwise just use latin1
+					my $dec = Encode::Guess->guess($lyrics);
+
+					if (ref $dec) {
+						$lyrics = $dec->decode($lyrics);
+					} else {
+						# Best try
+						$lyrics = Encode::decode('iso-8859-1', $lyrics);
+					}
+
+					# Normalise to LF terminations
+					$lyrics =~ s/\r\n/\n/g;
+					
+					# Trim leading newlines
+					$lyrics =~ s/^\n+//;
+					
+					# Expand tabs
+					$lyrics =~ s/^(.*?)\t/$1 . (' ' x (8-(length($1) % 8)))/meg;
+
+					# Finally, assign
+					$tags->{'LYRICS'} = $lyrics;
+				}
+			}
+		}
 	}
 
 	# Last resort
Index: Slim/Plugin/AudioScrobbler/Plugin.pm
===================================================================
--- Slim/Plugin/AudioScrobbler/Plugin.pm	(revision 17616)
+++ 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 {
@@ -1103,4 +1110,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;
Index: Slim/Buttons/TrackInfo.pm
===================================================================
--- Slim/Buttons/TrackInfo.pm	(revision 17616)
+++ Slim/Buttons/TrackInfo.pm	(working copy)
@@ -30,6 +30,9 @@
 
 our %functions = ();
 
+my %infoProvider;
+my @infoOrdering;
+
 # button functions for track info screens
 sub init {
 
@@ -55,6 +58,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 {
@@ -233,165 +241,29 @@
 
 		return;
 	}
-
-	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 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 (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 {
@@ -506,34 +378,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;
@@ -551,21 +395,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;
@@ -586,6 +420,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/Utils/Prefs.pm
===================================================================
--- Slim/Utils/Prefs.pm	(revision 17616)
+++ Slim/Utils/Prefs.pm	(working copy)
@@ -215,6 +215,7 @@
 		'timeFormat'            => q(|%I:%M %p),
 		'showArtist'            => 0,
 		'showYear'              => 0,
+		'sortAlbumsByYear'      => 0,
 		'guessFileFormats'	    => [
 									'(ARTIST - ALBUM) TRACKNUM - TITLE',
 									'/ARTIST/ALBUM/TRACKNUM - TITLE',
Index: Slim/Schema/ResultSet/Contributor.pm
===================================================================
--- Slim/Schema/ResultSet/Contributor.pm	(revision 17616)
+++ Slim/Schema/ResultSet/Contributor.pm	(working copy)
@@ -7,6 +7,31 @@
 
 use Slim::Utils::Prefs;
 
+our $prefs = preferences('server');
+
+our @albumSortRules = (
+	# 0: No special sorting
+	"concat('0', album.titlesort), album.disc",
+	
+	# 1: Sort by year ascending, unset years last
+	"CASE album.year " .
+	" WHEN 0 THEN 9999" .
+	" ELSE album.year " .
+	"END, concat('0', album.titlesort), album.disc",
+	
+	# 2: Sort by year ascending, unset years first
+	"album.year, concat('0', album.titlesort), album.disc",
+	
+	# 3: Sort by year descending, unset years last
+	"album.year DESC, concat('0', album.titlesort), album.disc",
+	
+	# 4: Sort by year ascending, unset years first
+	"CASE album.year " .
+	" WHEN 0 THEN 9999" .
+	" ELSE album.year " .
+	"END DESC, concat('0', album.titlesort), album.disc"
+);
+
 sub pageBarResults {
 	my $self = shift;
 
@@ -111,8 +136,9 @@
 		$sort = $rs->fixupSortKeys($sort);
 
 	} else {
-
-		$sort = "concat('0', album.titlesort), album.disc";
+	  
+		my $sortByYear = $prefs->get('sortAlbumsByYear') || 0;
+		$sort = $albumSortRules[$sortByYear] || $albumSortRules[0];
 	}
 
 	my $attr = {
Index: Slim/Web/Settings/Server/UserInterface.pm
===================================================================
--- Slim/Web/Settings/Server/UserInterface.pm	(revision 17616)
+++ Slim/Web/Settings/Server/UserInterface.pm	(working copy)
@@ -25,7 +25,7 @@
 }
 
 sub prefs {
-	return ($prefs, qw(skin itemsPerPage refreshRate thumbSize longdateFormat shortdateFormat timeFormat showArtist showYear titleFormatWeb));
+	return ($prefs, qw(skin itemsPerPage refreshRate thumbSize longdateFormat shortdateFormat timeFormat showArtist showYear sortAlbumsByYear titleFormatWeb));
 }
 
 sub handler {
Index: HTML/EN/settings/server/interface.html
===================================================================
--- HTML/EN/settings/server/interface.html	(revision 17616)
+++ HTML/EN/settings/server/interface.html	(working copy)
@@ -30,6 +30,18 @@
 			
 			</select>
 		[% END %]
+
+		[% WRAPPER settingGroup title="SETUP_SORTALBUMSBYYEAR" desc="SETUP_SORTALBUMSBYYEAR_DESC" %]
+			<select class="stdedit" name="sortAlbumsByYear" id="sortAlbumsByYear">
+ 
+				<option [% IF prefs.sortAlbumsByYear == 0 %]selected [% END %]value="0">[% 'DISABLED' | getstring %]</option>
+				<option [% IF prefs.sortAlbumsByYear == 1 %]selected [% END %]value="1">[% 'SETUP_SORTALBUMSBYYEAR_1' | getstring %]</option>
+				<option [% IF prefs.sortAlbumsByYear == 2 %]selected [% END %]value="2">[% 'SETUP_SORTALBUMSBYYEAR_2' | getstring %]</option>
+				<option [% IF prefs.sortAlbumsByYear == 3 %]selected [% END %]value="3">[% 'SETUP_SORTALBUMSBYYEAR_3' | getstring %]</option>
+				<option [% IF prefs.sortAlbumsByYear == 4 %]selected [% END %]value="4">[% 'SETUP_SORTALBUMSBYYEAR_4' | getstring %]</option>
+			
+			</select>
+		[% END %]
 	[% END %]
 
 	[% WRAPPER setting title="SETUP_TITLEFORMAT" desc="SETUP_GROUP_TITLEFORMATS_DESC" %]
Index: strings.txt
===================================================================
--- strings.txt	(revision 17616)
+++ strings.txt	(working copy)
@@ -4435,6 +4435,21 @@
 	NO	Du kan velge å vise årstallet et album ble gitt ut når du viser albumlisten eller albumomslag. Når valget er påskrudd vil årstallet vises sammen med tittelen til albumet.
 	ZH_CN	您可以选择通过浏览专辑或浏览专辑图象时是否一同显示专辑的年份。当您选择一同显示时，年份信息将显示在专辑标题旁边。
 
+SETUP_SORTALBUMSBYYEAR
+	EN	Sort Albums in Artist lists by year
+
+SETUP_SORTALBUMSBYYEAR_DESC
+	EN	You may choose to sort the albums by year when displayed within the artists list. Artist menu will be sorted by the year associated with that album. The order of sorting can be configured to be ascending or descended, with albums without years first or last. When disabled, the Albums listed from an Artist menu will be sorted alphabetic order.
+
+SETUP_SORTALBUMSBYYEAR_1
+	EN	Sort by year ascending, unset years last
+SETUP_SORTALBUMSBYYEAR_2
+	EN	Sort by year ascending, unset years first
+SETUP_SORTALBUMSBYYEAR_3
+	EN	Sort by year descending, unset years last
+SETUP_SORTALBUMSBYYEAR_4
+	EN	Sort by year descending, unset years first
+
 SETUP_SHOWARTIST
 	DA	Vis kunstner ved album
 	DE	Interpreten mit Alben anzeigen
