=== modified file 'gnukebox/install.php'
--- gnukebox/install.php	2009-06-06 14:51:17 +0000
+++ gnukebox/install.php	2009-06-12 18:56:58 +0000
@@ -255,8 +255,12 @@
 			mbid VARCHAR(36),
 			track INTEGER NOT NULL)");
 
+	$adodb->Execute("CREATE TABLE Love(
+			userid INTEGER REFERENCES Users(uniqueid),
+			trackid INTEGER REFERENCES Track(id))");
+
 	$adodb->Execute("CREATE VIEW Free_Scrobbles AS
-			SELECT s.username, s.track, s.artist, s.time, s.mbid, s.album, s.source, s.rating, s.length
+			SELECT s.userid, s.track, s.artist, s.time, s.mbid, s.album, s.source, s.rating, s.length
 				FROM Scrobbles s
 				JOIN Scrobble_Track st ON s.stid = st.id
 				JOIN Track t ON st.track = t.id

=== modified file 'nixtape/data/Server.php'
--- nixtape/data/Server.php	2009-06-11 18:24:08 +0000
+++ nixtape/data/Server.php	2009-06-13 17:07:01 +0000
@@ -62,7 +62,8 @@
 					l.mbid AS album_mbid,
 					l.image AS album_image,
 					l.artwork_license,
-					t.license
+					t.license,
+					t.id as trackid
 				FROM Scrobbles s
 				LEFT JOIN Artist a
 					ON s.artist=a.name
@@ -90,7 +91,8 @@
 					l.mbid AS album_mbid,
 					l.image AS album_image,
 					l.artwork_license,
-					t.license
+					t.license,
+					t.id AS trackid
 				FROM Scrobbles s
 				LEFT JOIN Artist a
 					ON s.artist=a.name
@@ -196,7 +198,8 @@
 						ClientCodes.url,
 						ClientCodes.free,
 						n.mbid,
-						t.license
+						t.license,
+						t.id AS trackid
 					FROM Now_Playing n
 					LEFT OUTER JOIN Scrobble_Sessions ss
 						ON n.sessionid=ss.sessionid
@@ -220,7 +223,8 @@
 						ClientCodes.url,
 						ClientCodes.free,
 						n.mbid,
-						t.license
+						t.license,
+						t.id AS trackid
 					FROM Now_Playing n
 					LEFT OUTER JOIN Scrobble_Sessions ss
 						ON n.sessionid=ss.sessionid
@@ -265,6 +269,97 @@
 
 		return $result;
 	}
+	
+	/**
+	* Adds a loved track
+	*
+	* @param int $trackid ID of the track to love
+	* @param int $userid ID of the user who loves the track
+	* @return True or false depending on the success
+	*/
+
+	static function addLove ($trackid, $userid) {
+		global $adodb;
+
+		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
+		try {
+			$data = $adodb->GetAll('SELECT
+							userid FROM Love WHERE
+							trackid = ' . (int)($trackid));
+		} catch (exception $e) {
+			return false;
+		}
+
+		if(!$data) {
+			try {
+				$adodb->Execute('INSERT INTO Love (userid, trackid) VALUES ('
+					. (int)($userid) . ', '
+					. (int)($trackid) . ');');
+			} catch (exception $e) {
+				return false;
+			}
+		}
+		
+		return true;
+	}
+
+	/**
+	* Retrieves a list of loved tracks
+	*
+	* @param int $number Number of tracks to list
+	* @param int $index Starting from this index
+	* @param int $userid ID of the user whose tracks to retrieve
+	* @return An array of loved track data or null on failure
+	*/
+	static function getLove($number, $index, $userid) {
+		global $adodb;
+
+		 $adodb->SetFetchMode(ADODB_FETCH_ASSOC);
+         try {
+			 $data = $adodb->CacheGetAll(60,'SELECT 
+			 				t.name AS track,
+							t.id,
+							t.license,
+							t.artist_name AS artist,
+							t.album_name AS album,
+							a.image AS album_image
+							FROM Track t
+							LEFT JOIN Album a
+							ON t.album_name = a.name 
+							WHERE t.id = ANY (SELECT 
+							trackid FROM Love
+							WHERE userid = '
+							. (int)($userid) . ') 
+							LIMIT ' . (int)($index) . ', '
+							. (int)($number));
+		} catch (exception $e) {
+			return null;
+		}
+
+		foreach($data as &$i) {
+			$row = sanitize($i);
+
+			$row['username'] = uniqueid_to_username($row['userid']);
+			$row['userurl'] = Server::getUserURL($row['username']);
+			$row['artisturl'] = Server::getArtistURL($row['artist']);
+			$row['trackurl'] = Server::getTrackURL($row['artist'], $row['album'], $row['track']);
+
+			if (!$row['album_image']) {
+				$row['album_image'] = false;
+			} else {
+				$row['album_image'] = resolve_external_url($row['album_image']);
+			}
+
+			$row['licenseurl'] = $row['license'];
+			$row['license'] = simplify_license($row['licenseurl']);
+
+
+			$result[] = $row;
+		}
+
+		return $result;
+
+	}
 
 	/**
 	 * The get*URL functions are implemented here rather than in their respective

=== modified file 'nixtape/data/User.php'
--- nixtape/data/User.php	2009-06-03 13:50:26 +0000
+++ nixtape/data/User.php	2009-06-13 14:44:23 +0000
@@ -198,6 +198,24 @@
 	function getNowPlaying($number) {
 		return Server::getNowPlaying($number, $this->name);
 	}
+	
+	/**
+	 * Add a track to the user's loved tracks 
+	 *
+	 * @param string $track Name of the track to be loved
+	 */
+	function addLove($trackid) {
+		return Server::addLove($trackid, $this->uniqueid);
+	}
+
+	/**
+	 * Get user's loved tracks
+	 *
+	 * @return An array of loved tracks data
+	 */
+	function getLove($number) {
+		return Server::getLove($number, $index, $this->uniqueid);
+	}
 
 	/**
 	 * Log in to the gnukebox server

=== added file 'nixtape/themes/librefm/templates/user-love.tpl'
--- nixtape/themes/librefm/templates/user-love.tpl	1970-01-01 00:00:00 +0000
+++ nixtape/themes/librefm/templates/user-love.tpl	2009-06-13 16:24:43 +0000
@@ -0,0 +1,41 @@
+{include file='header.tpl'}
+{include file='maxiprofile.tpl'}
+
+<div about="[_:seq1]" typeof="rdf:Seq" rev="rss:items">
+	<h3 typeof="rss:channel" property="rss:title" rel="rss:link" resource="#loved_songs" id="loved_songs" content="{$me->name|escape:'html':'UTF-8'}'s Loved Songs">Loved Songs</h3>
+</div>
+
+<ul class="gobbles" about="{$me->id|escape:'html':'UTF-8'}" rev="gob:user">
+{section name=i loop=$love}
+
+	<li class="play {if $love[i].license > 0}libre{/if}" about="{$love[i].id|escape:'html':'UTF-8'}" typeof="rss:item gob:ScrobbleEvent" rel="gob:track_played">
+		<div about="{$love[i].id_track|escape:'html':'UTF-8'}" typeof="mo:Track" class="haudio">
+			<div rev="mo:track">
+				<div about="{$love[i].id_album|escape:'html':'UTF-8'}" typeof="mo:Record"{if $love[i].album} property="dc:title" content="{$love[i].album|escape:'html':'UTF-8'}"{/if}>
+					{if $love[i].albumurl}<a rel="foaf:page" href="{$love[i].albumurl|escape:'html':'UTF-8'}">{/if}
+						<span{if $love[i].album_image} rel="foaf:depiction"{/if}{if $love[i].albumurl} about="{$love[i].id_album|escape:'html':'UTF-8'}"{/if}>
+							<img height="50" width="50" 
+								src="{if !$love[i].album_image}/themes/librefm/images/qm50.png{else}{$love[i].album_image|escape:'html':'UTF-8'}{/if}" 
+								class="albumart{if !$love[i].album_image} photo{/if}" 
+								{if $love[i].album}title="{$love[i].album|escape:'html':'UTF-8'}" alt="Album: {$love[i].album|escape:'html':'UTF-8'}"{else}alt="Unknown album"{/if}  />
+						</span>
+					{if $love[i].albumurl}</a>{/if}
+				</div>
+			</div>
+			<div rel="foaf:maker" class="contributor vcard">
+				<a about="{$love[i].id_artist|escape:'html':'UTF-8'}" typeof="mo:MusicArtist" property="foaf:name" rel="foaf:page"
+					class="fn url" href="{$love[i].artisturl|escape:'html':'UTF-8'}"
+					>{$love[i].artist|escape:'html':'UTF-8'}</a>
+			</div>
+			<div><a class="fn" property="dc:title" rel="foaf:page" href="{$love[i].trackurl|escape:'html':'UTF-8'}">{$love[i].track|escape:'html':'UTF-8'}</a></div>
+			<small>&nbsp;</small>
+			<span about="{$love[i].id|escape:'html':'UTF-8'}" property="rss:link" content="{$love[i].trackurl|escape:'html':'UTF-8'}">
+				<span property="rss:description" content="{if $love[i].album}{$love[i].album}{else}Unknown album{/if}">
+					<span property="rss:title" content="{$love[i].artist|escape:'html':'UTF-8'}: {$love[i].track|escape:'html':'UTF-8'}" rev="rdf:_{$smarty.section.i.index_next}" resource="[_:seq1]"></span>
+				</span>
+			</span>
+		</div>
+	</li>
+{/section}
+</ul>
+{include file='footer.tpl'}

=== modified file 'nixtape/themes/librefm/templates/user-profile.tpl'
--- nixtape/themes/librefm/templates/user-profile.tpl	2009-05-08 14:41:07 +0000
+++ nixtape/themes/librefm/templates/user-profile.tpl	2009-06-13 17:02:49 +0000
@@ -1,13 +1,22 @@
 {include file='header.tpl'}
-
 {include file='maxiprofile.tpl'}
 
+<script type="text/javascript"> {literal}
+$(document).ready(function() {
+	$(".love").click(function() {
+		var id = $(this).attr('id');
+		$.get("user-love.php", {user: "{/literal}{$me->name}{literal}", love : id});
+	});
+}); {/literal}
+</script>
+
 {if $nowplaying|@count > 0}
 <h3>{t}Now Playing:{/t}</h3>
 <!-- We should try to make this list work like the gobbles list. -->
 <dl class='now-playing'>
     {section name=i loop=$nowplaying}
-    <dt class='track-name {if $scrobbles[i].license > 0}libre{/if}'><a href="{$nowplaying[i].trackurl|escape:'html':'UTF-8'}">{$nowplaying[i].track|escape:'html':'UTF-8'}</a></dt>
+    <dt class='track-name {if $scrobbles[i].license > 0}libre{/if}'><a href="{$nowplaying[i].trackurl|escape:'html':'UTF-8'}">{$nowplaying[i].track|escape:'html':'UTF-8'}</a>
+	<a id="{$nowplaying[i].trackid}" style="color:red;" class="love" href="">&#x2665;</a></dt>
     <dd>by <span class='artist-name'><a href='{$nowplaying[i].artisturl|escape:'html':'UTF-8'}'>{$nowplaying[i].artist|escape:'html':'UTF-8'}</a></span></dd>
     <dd>with <span class='gobbler'>{$nowplaying[i].clientstr}</span></dd>
     {/section}
@@ -40,7 +49,8 @@
 					class="fn url" href="{$scrobbles[i].artisturl|escape:'html':'UTF-8'}"
 					>{$scrobbles[i].artist|escape:'html':'UTF-8'}</a>
 			</div>
-			<div><a class="fn" property="dc:title" rel="foaf:page" href="{$scrobbles[i].trackurl|escape:'html':'UTF-8'}">{$scrobbles[i].track|escape:'html':'UTF-8'}</a></div>
+			<div><a class="fn" property="dc:title" rel="foaf:page" href="{$scrobbles[i].trackurl|escape:'html':'UTF-8'}">{$scrobbles[i].track|escape:'html':'UTF-8'}</a>
+			<a id="{$scrobbles[i].trackid}" style="color:red;" class="love" href="">&#x2665;</a></div>
 			<small about="{$scrobbles[i].id|escape:'html':'UTF-8'}" property="dc:date" content="{$scrobbles[i].timeiso}" datatype="xsd:dateTime">{$scrobbles[i].timehuman}</small>
 			<span about="{$scrobbles[i].id|escape:'html':'UTF-8'}" property="rss:link" content="{$scrobbles[i].trackurl|escape:'html':'UTF-8'}">
 				<span property="rss:description" content="{if $scrobbles[i].album}{$scrobbles[i].album}{else}Unknown album{/if}">

=== added file 'nixtape/user-love.php'
--- nixtape/user-love.php	1970-01-01 00:00:00 +0000
+++ nixtape/user-love.php	2009-06-13 15:44:17 +0000
@@ -0,0 +1,98 @@
+<?php
+
+/* Libre.fm -- a free network service for sharing your music listening habits
+
+   Copyright (C) 2009 Libre.fm Project
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Affero General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Affero General Public License for more details.
+
+   You should have received a copy of the GNU Affero General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+require_once('database.php');
+require_once('templating.php');
+require_once('data/User.php');
+require_once('data/TagCloud.php');
+require_once('data/Server.php');
+
+if(!isset($_GET['user']) && $logged_in == false) {
+	$smarty->assign('error', 'Error!');
+	$smarty->assign('details', 'User not set! You shouldn\'t be here!');
+	$smarty->display('error.tpl');
+	die();
+}
+
+try {
+	$user = new User($_GET['user']);
+} catch (exception $e) {
+	$error = 'User not found';
+}
+
+if(isset($_GET['love']) && $logged_in == true) {
+	$trackid = $_GET['love'];
+ 	$user->addLove($trackid);
+	return;
+}
+
+if(isset($_GET['i'])) {
+	$i = $_GET['i'];
+}
+else {
+	$i = 0 ;
+}
+
+if(isset($user->name)) {
+
+	$smarty->assign('geo', Server::getLocationDetails($user->location_uri));
+	try {
+	$aUserTagCloud =  TagCloud::GenerateTagCloud(TagCloud::scrobblesTable('user'), 'artist', 40, $user->name);
+		$smarty->assign('user_tagcloud',$aUserTagCloud);
+	} catch (exception $e) {}
+	try {
+	$aUserLove = $user->getLove(10,$i);
+		$smarty->assign('love', $aUserLove);
+	} catch (exception $e) {}
+	$smarty->assign('isme', ($this_user->name == $user->name));
+	$smarty->assign('me', $user);
+	$smarty->assign('sidebar', true);
+	$smarty->assign('sidebartemplate', 'profile-sidebar.tpl');
+
+	$smarty->assign('extra_head_links', array(
+				array(
+					'rel'=>'alternate',
+					'type' => 'application/rss+xml' ,
+					'title' => 'RSS 1.0 Feed (Recent plays)',
+					'href' => $base_url.'/rdf.php?fmt=rss&page='.urlencode(str_replace($base_url, '', $user->getURL('recent-tracks')))
+				     ),
+				array(
+					'rel'=>'alternate',
+					'type' => 'application/rss+xml' ,
+					'title' => 'RSS 1.0 Feed (Journal)',
+					'href' => $user->journal_rss
+				     ),
+				array(
+					'rel' => 'meta',
+					'type' => 'application/rdf+xml' ,
+					'title' => 'FOAF',
+					'href' => $base_url.'/rdf.php?fmt=xml&page='.urlencode(str_replace($base_url, '', $user->getURL()))
+				     )
+				));
+
+	$smarty->display('user-love.tpl');
+} else {
+	$smarty->assign('error', $error);
+	$smarty->assign('details', 'Shall I call in a missing persons report?');
+	$smarty->display('error.tpl');
+}
+
+?>

