We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

Commit 05fd3dda authored by P. J. McDermott's avatar P. J. McDermott

Merge branch 'master' into code-cleanup

Conflicts:
	nixtape/2.0/index.php
	nixtape/radio/radio-utils.php

Resolving conflicts and cleaning up new code.
parents da2ddee4 ad521e85
......@@ -195,14 +195,13 @@ function validateMBID ($input) {
function forwardScrobble($userid, $artist, $album, $track, $time, $mbid, $source, $rating, $length) {
global $adodb, $lastfm_key, $lastfm_secret;
// Strip database quoting from details and urlencode
$artist = urlencode(substr($artist, 1, strlen($artist) - 2));
$track = urlencode(substr($track, 1, strlen($track) - 2));
$album == 'NULL' ? $album = false : $album = urlencode(substr($album, 1, strlen($album) - 2));
$mbid == 'NULL' ? $mbid = false : $mbid = urlencode(substr($mbid, 1, strlen($mbid) - 2));
$source == 'NULL' ? $source = false : $source = urlencode(substr($source, 1, strlen($source) - 2));
$rating == '\'0\'' ? $rating = false : 0;
$length == 'NULL' ? $length = false : 0;
$artist = urlencode($artist);
$track = urlencode($track);
$album = urlencode($album);
$mbid = urlencode($mbid);
$source = urlencode($source);
$rating = urlencode($rating);
$length = urlencode($length);
$res = $adodb->CacheGetAll(600, 'SELECT * FROM Service_Connections WHERE userid = ' . $userid . ' AND forward = 1');
foreach($res as &$row) {
......@@ -231,7 +230,7 @@ function forwardScrobble($userid, $artist, $album, $track, $time, $mbid, $source
}
$post_vars .= '&timestamp[0]=' . $time . '&track[0]=' . $track;
$sig = str_replace('&', '', urldecode($post_vars));
$sig = urldecode(str_replace('&', '', $post_vars));
$sig = str_replace('=', '', $sig);
$sig = md5($sig . $lastfm_secret);
......
......@@ -164,7 +164,7 @@ for($i = 0; $i < count($_POST['a']); $i++) {
try {
$res =& $adodb->Execute($sql);
if(isset($lastfm_key)) {
forwardScrobble($userid, $artist, $album, $track, $time, $mbid, $source, $rating, $length);
forwardScrobble($userid, $_POST['a'][$i], $_POST['b'][$i], $_POST['t'][$i], $time, $_POST['m'][$i], $_POST['o'][$i], $_POST['r'][$i], $_POST['l'][$i]);
}
}
catch (exception $e) {
......
......@@ -88,6 +88,7 @@ $method_map = array(
'user.gettoptags' => method_user_getTopTags,
'user.getlovedtracks' => method_user_getLovedTracks,
'user.getbannedtracks' => method_user_getBannedTracks,
'user.getneighbours' => method_user_getNeighbours,
'radio.tune' => method_radio_tune,
'radio.getplaylist' => method_radio_getPlaylist,
'track.addtags' => method_track_addTags,
......@@ -179,7 +180,21 @@ function method_user_getBannedTracks() {
respond($xml);
}
function method_user_getNeighbours() {
if (!isset($_GET['user'])) {
report_failure(LFM_INVALID_PARAMS);
}
$user = $_GET['user'];
if (isset($_GET['limit'])) {
$limit = $_GET['limit'];
} else {
$limit = 50;
}
$xml = UserXML::getNeighbours($user, $limit);
respond($xml);
}
/**
* Artist methods
......@@ -269,8 +284,7 @@ function method_auth_getToken() {
. $adodb->qstr($key) . ', '
. (int)(time() + 3600)
. ')');
}
catch (Exception $e) {
} catch (Exception $e) {
report_failure(LFM_SERVICE_OFFLINE);
}
......@@ -287,18 +301,17 @@ function method_auth_getMobileSession() {
// Check for a token that is bound to a user
try {
$result = $adodb->GetRow('SELECT username, password FROM Users WHERE '
. 'lower(username) = ' . strtolower($adodb->qstr($_GET['username'])));
}
catch (Exception $e) {
$result = $adodb->GetRow('SELECT username, lower(username) AS lc_username, password FROM Users WHERE '
. 'lower(username) = lower(' . $adodb->qstr($_GET['username']) . ')');
} catch (Exception $e) {
report_failure(LFM_SERVICE_OFFLINE);
}
if (is_null($result)) {
report_failure(LFM_INVALID_TOKEN);
}
list($username, $password) = $result;
if (md5($username . $password) != $_GET['authToken']) {
list($username, $lc_username, $password) = $result;
if (md5($lc_username . $password) != $_GET['authToken']) {
report_failure(LFM_INVALID_TOKEN);
}
......@@ -314,8 +327,7 @@ function method_auth_getMobileSession() {
. (int)(time() + 3600) . ', '
. $adodb->qstr($username)
. ')');
}
catch (Exception $e) {
} catch (Exception $e) {
report_failure(LFM_SERVICE_OFFLINE);
}
......@@ -345,8 +357,7 @@ function method_auth_getSession() {
$username = $adodb->GetOne('SELECT username FROM Auth WHERE '
. 'token = ' . $adodb->qstr($_GET['token']) . ' AND '
. 'username IS NOT NULL AND sk IS NULL');
}
catch (Exception $e) {
} catch (Exception $e) {
report_failure(LFM_SERVICE_OFFLINE);
}
if (!$username) {
......@@ -360,8 +371,7 @@ function method_auth_getSession() {
$result = $adodb->Execute('UPDATE Auth SET '
. 'sk = ' . $adodb->qstr($session) . ' WHERE '
. 'token = ' . $adodb->qstr($_GET['token']));
}
catch (Exception $e) {
} catch (Exception $e) {
report_failure(LFM_SERVICE_OFFLINE);
}
......@@ -394,8 +404,7 @@ function method_radio_tune() {
$username = $adodb->GetOne('SELECT username FROM Auth WHERE '
. 'sk = ' . $adodb->qstr($_POST['sk']) . ' AND '
. 'username IS NOT NULL');
}
catch (Exception $e) {
} catch (Exception $e) {
report_failure(LFM_SERVICE_OFFLINE);
}
if (!$username) {
......
......@@ -248,5 +248,35 @@ class UserXML {
} catch (Exception $e) {}
}
public static function getNeighbours($u, $limit=50) {
try {
$user = new User($u);
$res = $user->getNeighbours($limit);
} catch (Exception $e) {
return XML::error('error', '7', 'Invalid resource specified');
}
$xml = new SimpleXMLElement('<lfm status="ok"></lfm>');
$root = $xml->addChild('neighbours');
$root->addAttribute('user', $user->name);
if (empty($res)) {
return $xml;
}
$highest_match = $res[0]['shared_artists'];
foreach($res as $row) {
$neighbour = $row['user'];
$user_node = $root->addChild('user', null);
$user_node->addChild('name', repamp($neighbour->name));
$user_node->addChild('fullname', repamp($neighbour->fullname));
$user_node->addChild('url', repamp($neighbour->getURL()));
// Give a normalised value
$user_node->addChild('match', $row['shared_artists'] / $highest_match);
}
return $xml;
}
}
......@@ -499,4 +499,25 @@ class User {
return $res != 0;
}
/**
* Find the neighbours of this user based on the number of loved artists shared between them and other users.
*
* @param int The number of neighbours to return (defaults to 10).
* @return array An array of userids, User objects and the number of loved artists shared with this user.
*/
function getNeighbours($limit=10) {
global $adodb;
if(!$this->hasLoved()) {
return array();
}
$res = $adodb->CacheGetAll(7200, 'SELECT Loved_Tracks.userid AS userid, count(Loved_Tracks.userid) AS shared_artists FROM Loved_Tracks INNER JOIN (SELECT DISTINCT(artist) AS artist FROM Loved_Tracks WHERE userid=' . $this->uniqueid . ') AS Loved_Artists ON Loved_Tracks.artist = Loved_Artists.artist WHERE userid != ' . $this->uniqueid . ' GROUP BY Loved_Tracks.userid ORDER BY shared_artists DESC LIMIT ' . $limit);
foreach($res as &$neighbour) {
$neighbour['user'] = User::new_from_uniqueid_number($neighbour['userid']);
}
return $res;
}
}
/*
GNU FM -- a free network service for sharing your music listening hab""s
GNU FM -- a free network service for sharing your music listening habits
Copyright (C) 2009 Free Software Foundation, Inc
......@@ -23,6 +23,7 @@
for the JavaScript code in this page.
*/
var audio;
var scrobbled, now_playing;
var artist, album, track, trackpage, session_key, radio_key, ws_key;
var playlist = [], current_song = 0;
......@@ -39,7 +40,7 @@ var example_tags = "e.g. guitar, violin, female vocals, piano";
* @param string rk Radio session key or false if streaming isn't required
*/
function playerInit(list, sk, ws, rk) {
var audio = document.getElementById("audio");
audio = document.getElementById("audio");
if (!list) {
// We're playing a stream instead of a playlist
streaming = true;
......@@ -71,8 +72,6 @@ function playerInit(list, sk, ws, rk) {
* Finishes the player initialisation when the playlist has been loaded
*/
function playerReady() {
var audio = document.getElementById("audio");
populatePlaylist();
if(!playable_songs) {
return;
......@@ -82,12 +81,17 @@ function playerReady() {
audio.addEventListener("ended", songEnded, false);
updateProgress();
$("#play").fadeTo("normal", 1);
$("#pause").fadeTo("normal", 1);
$("#pause").hide();
$("#ban").fadeTo("normal", 1);
$("#love").fadeTo("normal", 1);
$("#open_tag").fadeTo("normal", 1);
$("#volume").fadeTo("normal", 1);
$("#progressbar").progressbar({ value: 0 });
$("#player > #interface").show();
$("#tags").placeholdr({placeholderText: example_tags});
$("#volume-slider").slider({range: "min", min: 0, max: 100, value: 60, slide: setVolume});
loadVolume();
player_ready = true;
}
......@@ -95,13 +99,12 @@ function playerReady() {
* Begins playback
*/
function play() {
var audio = document.getElementById("audio");
audio.play();
if(!now_playing) {
nowPlaying();
}
$("#play").fadeTo("normal", 0.5);
$("#pause").fadeTo("normal", 1);
$("#play").hide();
$("#pause").show();
$("#seekforward").fadeTo("normal", 1);
$("#seekback").fadeTo("normal", 1);
}
......@@ -110,10 +113,9 @@ function play() {
* Pauses playback
*/
function pause() {
var audio = document.getElementById("audio");
audio.pause();
$("#play").fadeTo("normal", 1);
$("#pause").fadeTo("normal", 0.5);
$("#play").show();
$("#pause").hide();
$("#seekforward").fadeTo("normal", 0.5);
$("#seekback").fadeTo("normal", 0.5);
}
......@@ -123,7 +125,6 @@ function pause() {
*/
function seekBack() {
try {
var audio = document.getElementById("audio");
audio.currentTime = audio.currentTime - 10;
} catch (e) {}
}
......@@ -133,7 +134,6 @@ function seekBack() {
*/
function seekForward() {
try {
var audio = document.getElementById("audio");
audio.currentTime = audio.currentTime + 10;
} catch (e) {}
}
......@@ -142,7 +142,6 @@ function seekForward() {
* Updates the progress bar every 900 milliseconds
*/
function updateProgress() {
var audio = document.getElementById("audio");
if (audio.duration > 0) {
$("#progressbar").progressbar('option', 'value', (audio.currentTime / audio.duration) * 100);
$("#duration").text(friendlyTime(audio.duration));
......@@ -160,7 +159,6 @@ function updateProgress() {
* Called automatically when a song finished. Loads the next song if there is one
*/
function songEnded() {
var audio = document.getElementById("audio");
if(current_song == playlist.length - 1) {
pause();
} else {
......@@ -213,7 +211,7 @@ function scrobble() {
}
timestamp = Math.round(new Date().getTime() / 1000);
$.post("/scrobble-proxy.php?method=scrobble", { "a[0]" : artist, "b[0]" : album, "t[0]" : track, "i[0]" : timestamp, "s" : session_key },
function(data){
function(data){
if(data.substring(0, 2) == "OK") {
$("#scrobbled").text("Scrobbled");
$("#scrobbled").fadeIn(5000, function() { $("#scrobbled").fadeOut(5000) } );
......@@ -221,7 +219,7 @@ function scrobble() {
$("#scrobbled").text(data);
$("#scrobbled").fadeIn(1000);
}
}, "text");
}, "text");
}
/**
......@@ -230,7 +228,6 @@ function scrobble() {
*/
function nowPlaying() {
var timestamp;
var audio = document.getElementById("audio");
now_playing = true;
if(!session_key) {
//Not authenticated
......@@ -246,7 +243,6 @@ function nowPlaying() {
* @param int song The song number in the playlist that should be played
*/
function playSong(song) {
var audio = document.getElementById("audio");
loadSong(song);
play();
}
......@@ -258,7 +254,6 @@ function playSong(song) {
*/
function loadSong(song) {
var url = playlist[song]["url"];
var audio = document.getElementById("audio");
artist = playlist[song]["artist"];
album = playlist[song]["album"];
track = playlist[song]["track"];
......@@ -322,7 +317,7 @@ function getRadioPlaylist() {
var tracks, artist, album, title, url, extension, trackpage_url, i;
$.get("/2.0/", {'method' : 'radio.getPlaylist', 'sk' : radio_key}, function(data) {
parser=new DOMParser();
xmlDoc=parser.parseFromString(data,"text/xml");
xmlDoc=parser.parseFromString(data,"text/xml");
tracks = xmlDoc.getElementsByTagName("track")
for(i = 0; i < tracks.length; i++) {
try {
......@@ -399,3 +394,51 @@ function tag() {
$("#tags").val("");
}
}
/**
* Toggle visibility of the volume slider
*/
function toggleVolume() {
$("#volume-box").toggle(500);
}
/**
* Set the player volume and store it in a cookie for future sessions
*/
function setVolume(event, vol) {
audio.volume = parseFloat(vol.value / 100);
var date = new Date();
date.setTime(date.getTime()+(315360000000)); // Remember for 10 years
document.cookie='volume=' + audio.volume + '; expires='+date.toGMTString()+ '; path=/';
}
/**
* Load the player volume from a cookie
*/
function loadVolume() {
volume = getCookie('volume');
if(volume == undefined) {
return;
}
volume = parseFloat(volume);
$("#volume-slider").slider('value', volume * 100);
audio.volume = volume;
}
/**
* Retrieve the contents of a cookie
*/
function getCookie(c_name)
{
var i,x,y,ARRcookies=document.cookie.split(";");
for (i=0;i<ARRcookies.length;i++)
{
x=ARRcookies[i].substr(0,ARRcookies[i].indexOf("="));
y=ARRcookies[i].substr(ARRcookies[i].indexOf("=")+1);
x=x.replace(/^\s+|\s+$/g,"");
if (x==c_name)
{
return unescape(y);
}
}
}
......@@ -51,6 +51,10 @@ function radio_title_from_url($url) {
$user = $regs[2];
return 'Libre.fm ' . ucwords($user) . '\'s Mix Radio';
}
if (preg_match('@l(ast|ibre)fm://user/(.*)/neighbours@', $url, $regs)) {
$user = $regs[2];
return 'Libre.fm ' . ucwords($user) . '\'s Neighbourhood radio';
}
if (preg_match('@l(ast|ibre)fm://community/loved@', $url, $regs)) {
return 'Libre.fm Community\'s Loved Radio';
}
......@@ -85,7 +89,7 @@ function make_playlist($session, $old_format = false) {
if (preg_match('@l(ast|ibre)fm://globaltags/(.*)@', $url, $regs)) {
$tag = $regs[2];
$res = $adodb->Execute('SELECT Track.name, Track.artist_name, Track.album_name, Track.duration, Track.streamurl FROM Track INNER JOIN Tags ON Track.name=Tags.track AND Track.artist_name=Tags.artist WHERE streamable=1 AND lower(tag) = lower(' . $adodb->qstr($tag) . ')');
$res = $adodb->CacheGetAll(7200, 'SELECT Track.name, Track.artist_name, Track.album_name, Track.duration, Track.streamurl FROM Track INNER JOIN Tags ON Track.name=Tags.track AND Track.artist_name=Tags.artist WHERE streamable=1 AND lower(tag) = lower(' . $adodb->qstr($tag) . ')');
} else if (preg_match('@l(ast|ibre)fm://artist/(.*)/similarartists@', $url, $regs)) {
try {
$artist = new Artist($regs[2]);
......@@ -96,65 +100,86 @@ function make_playlist($session, $old_format = false) {
$res = get_artist_selection($similarArtists, $artist);
} else if (preg_match('@l(ast|ibre)fm://artist/(.*)@', $url, $regs)) {
$artist = $regs[2];
$res = $adodb->Execute('SELECT name, artist_name, album_name, duration, streamurl FROM Track WHERE streamable=1 AND lower(artist_name) = lower(' . $adodb->qstr($artist) . ')');
} else if (preg_match('@l(ast|ibre)fm://user/(.*)/(loved|library|mix)@', $url, $regs)) {
$res = $adodb->CacheGetAll(7200, 'SELECT name, artist_name, album_name, duration, streamurl FROM Track WHERE streamable=1 AND lower(artist_name) = lower(' . $adodb->qstr($artist) . ')');
} else if (preg_match('@l(ast|ibre)fm://user/(.*)/(loved|library|personal)@', $url, $regs)) {
try {
$requser = new User($regs[2]);
} catch (Exception $e) {
die("FAILED\n"); // this should return a blank dummy playlist instead
}
$res = $adodb->Execute('SELECT Track.name, Track.artist_name, Track.album_name, Track.duration, Track.streamurl FROM Track INNER JOIN Loved_Tracks ON Track.artist_name=Loved_Tracks.artist AND Track.name=Loved_Tracks.track WHERE Loved_Tracks.userid=' . $requser->uniqueid . ' AND Track.streamable=1');
} else if (preg_match('@l(ast|ibre)fm://user/(.*)/recommended@', $url, $regs) || preg_match('@l(ast|ibre)fm://user/(.*)/mix@', $url, $regs)) {
$res = get_loved_tracks(array($requser->uniqueid));
} else if (preg_match('@l(ast|ibre)fm://user/(.*)/recommended@', $url, $regs)) {
try {
$requser = new User($regs[2]);
} catch (Exception $e) {
die("FAILED\n"); // this should return a blank dummy playlist instead
}
$recommendedArtists = $requser->getRecommended(8, true);
if ($res) {
// If we already have some results then we're adding these to the loved tracks for mix radio
$res += get_artist_selection($recommendedArtists);
} else {
$res = get_artist_selection($recommendedArtists);
$res = get_artist_selection($recommendedArtists);
} else if (preg_match('@l(ast|ibre)fm://user/(.*)/mix@', $url, $regs)) {
try {
$requser = new User($regs[2]);
} catch (Exception $e) {
die("FAILED\n"); // this should return a blank dummy playlist instead
}
$recommendedArtists = $requser->getRecommended(8, true);
$res = get_loved_tracks(array($requser->uniqueid)) + get_artist_selection($recommendedArtists);
} else if (preg_match('@l(ast|ibre)fm://user/(.*)/neighbours@', $url, $regs)) {
try {
$requser = new User($regs[2]);
} catch (Exception $e) {
die("FAILED\n"); // this should return a blank dummy playlist instead
}
$neighbours = $requser->getNeighbours();
$userids = array();
foreach ($neighbours as $neighbour) {
$userids[] = $neighbour['userid'];
}
$res = get_loved_tracks($userids);
} else if (preg_match('@l(ast|ibre)fm://community/loved@', $url, $regs)) {
$res = $adodb->Execute('SELECT Track.name, Track.artist_name, Track.album_name, Track.duration, Track.streamurl FROM Track INNER JOIN Loved_Tracks ON Track.artist_name=Loved_Tracks.artist AND Track.name=Loved_Tracks.track WHERE Track.streamable=1');
$res = $adodb->CacheGetAll(7200, 'SELECT Track.name, Track.artist_name, Track.album_name, Track.duration, Track.streamurl FROM Track INNER JOIN Loved_Tracks ON Track.artist_name=Loved_Tracks.artist AND Track.name=Loved_Tracks.track WHERE Track.streamable=1');
} else {
die("FAILED\n"); // this should return a blank dummy playlist instead
}
$avail = $res->RecordCount();
$tr[0] = rand(0, $avail - 1);
$tr[1] = rand(0, $avail - 1);
$tr[2] = rand(0, $avail - 1);
$tr[3] = rand(0, $avail - 1);
$tr[4] = rand(0, $avail - 1);
$tr = array_unique($tr);
// we should probably shuffle these here
$num_tracks = count($res) > 5 ? 5 : count($res);
$used_tracks = array();
$radiotracks = array();
$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
for ($i = 0; $i < count($tr); $i++) {
$res->Move($tr[$i]);
$row = $res->FetchRow();
if ($user) {
$banned = $adodb->GetOne('SELECT COUNT(*) FROM Banned_Tracks WHERE '
. 'artist = ' . $adodb->qstr($row['artist_name'])
. 'AND track = ' . $adodb->qstr($row['name'])
. 'AND userid = ' . $user->uniqueid);
if ($banned) {
// This track has been banned by the user, so select another one
$tr[$i] = rand(0, $avail - 1);
$i--;
continue;
for ($i = 0; $i < $num_tracks; $i++) {
$tracks_left = true;
do {
$random_track = rand(0, count($res) - 1);
$banned = false;
$row = $res[$random_track];
if (count($res) == count($used_tracks)) {
// Ran out of unique, unbanned tracks
$tracks_left = false;
}
if ($user) {
// See if a track has been banned by the user, if so select another one
$banned = $adodb->GetOne('SELECT COUNT(*) FROM Banned_Tracks WHERE '
. 'artist = ' . $adodb->qstr($row['artist_name'])
. 'AND track = ' . $adodb->qstr($row['name'])
. 'AND userid = ' . $user->uniqueid);
if ($banned && !in_array($random_track, $used_tracks)) {
$used_tracks[] = $random_track;
}
}
} while ((in_array($random_track, $used_tracks) || $banned) && $tracks_left);
if (!$tracks_left) {
break;
}
$album = new Album($row['album_name'], $row['artist_name']);
$used_tracks[] = $random_track;
$album = false;
if (isset($row['album_name'])) {
$album = new Album($row['album_name'], $row['artist_name']);
}
if ($row['duration'] == 0) {
$duration = 180000;
......@@ -165,15 +190,28 @@ function make_playlist($session, $old_format = false) {
$radiotracks[$i]['location'] = resolve_external_url($row['streamurl']);
$radiotracks[$i]['title'] = $row['name'];
$radiotracks[$i]['id'] = '0000';
$radiotracks[$i]['album'] = $album->name;
if ($album) {
$radiotracks[$i]['album'] = $album->name;
} else {
$radiotracks[$i]['album'] = '';
}
$radiotracks[$i]['creator'] = $row['artist_name'];
$radiotracks[$i]['duration'] = $duration;
$radiotracks[$i]['image'] = $album->image;
if ($album) {
$radiotracks[$i]['image'] = $album->image;
} else {
$radiotracks[$i]['image'] = '';
}
$radiotracks[$i]['artisturl'] = Server::getArtistURL($row['artist_name']);
$radiotracks[$i]['albumurl'] = $album->getURL();
$radiotracks[$i]['trackurl'] = Server::getTrackURL($row['artist_name'], $album->name, $row['name']);
$radiotracks[$i]['downloadurl'] = Server::getTrackURL($row['artist_name'], $album->name, $row['name']);
if ($album) {
$radiotracks[$i]['albumurl'] = $album->getURL();
$radiotracks[$i]['trackurl'] = Server::getTrackURL($row['artist_name'], $album->name, $row['name']);
$radiotracks[$i]['downloadurl'] = Server::getTrackURL($row['artist_name'], $album->name, $row['name']);
} else {
$radiotracks[$i]['albumurl'] = '';
$radiotracks[$i]['trackurl'] = Server::getTrackURL($row['artist_name'], false, $row['name']);
$radiotracks[$i]['downloadurl'] = Server::getTrackURL($row['artist_name'], false, $row['name']);
}
}
$smarty->assign('radiotracks', $radiotracks);
......@@ -199,6 +237,25 @@ function get_artist_selection($artists, $artist = false) {
}
$artistsClause .= 'lower(artist_name) = lower(' . $adodb->qstr($artists[$r]['artist']) . ')';
}
return $adodb->Execute('SELECT name, artist_name, album_name, duration, streamurl FROM Track WHERE streamable=1 AND ' . $artistsClause);
return $adodb->CacheGetAll(7200, 'SELECT name, artist_name, album_name, duration, streamurl FROM Track WHERE streamable=1 AND ' . $artistsClause);
}
/**
* Get the loved tracks for a list of users
*
* @param array An array of userids (integers).
* @return array An array of track details.
*/
function get_loved_tracks($users) {