<?php

/* GNU FM -- a free network service for sharing your music listening habits

   Copyright (C) 2009 Free Software Foundation, Inc

   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($install_path . '/database.php');
require_once($install_path . '/data/Artist.php');
// require_once($install_path . '/data/Group.php');
require_once($install_path . '/data/Track.php');
require_once($install_path . '/data/User.php');
require_once($install_path . '/data/sanitize.php');
require_once($install_path . '/utils/linkeddata.php');
require_once($install_path . '/utils/arc/ARC2.php');
require_once($install_path . '/utils/resolve-external.php');
require_once($install_path . '/utils/licenses.php');
require_once($install_path . '/utils/rewrite-encode.php');
require_once($install_path . '/temp-utils.php'); // this is extremely dodgy and shameful
require_once($install_path . '/data/clientcodes.php');

/**
 * Provides access to server-wide data
 *
 * All methods are statically accessible
 */
class Server {

	/**
	 * Retrieves a list of recent scrobbles
	 *
	 * @param int $number The number of scrobbles to return
	 * @param int $userid The user id to return scrobbles for
	 * @param int $offset Amount of entries to skip before returning scrobbles
	 * @param int $from Only return scrobbles with time higher than this timestamp
	 * @param int $to Only return scrobbles with time lower than this timestamp
	 * @return array Scrobbles or null in case of failure
	 */
	static function getRecentScrobbles($number = 10, $userid = false, $offset = 0, $from = false, $to = false) {
		global $adodb;

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);

		if ($userid) {
			$params = array();
			$query = 'SELECT s.*, lt.userid as loved FROM Scrobbles s LEFT JOIN Loved_Tracks lt ON (s.track=lt.track AND s.artist=lt.artist AND s.userid=lt.userid) WHERE s.userid=?';
			$params[] = $userid;

			if ($from) {
				$query .= ' AND s.time>?';
				$params[] = (int) $from;
			}

			if ($to) {
				$query .= ' AND s.time<?';
				$params[] = (int) $to;
			}

		} else {
			$params = array();
			$query = 'SELECT s.* FROM Scrobbles s';

			if ($from) {
				$query .= ' WHERE s.time>?';
				$params[] = (int) $from;
				if ($to) {
					$query .= ' AND s.time<?';
					$params[] = (int) $to;
				}
			} else {
				if ($to) {
					$query .= ' WHERE s.time<?';
					$params[] = (int) $to;
				}
			}
		}

		$query .= ' ORDER BY s.time DESC LIMIT ? OFFSET ?';
		$params[] = (int) $number;
		$params[] = (int) $offset;

		try {
			$res = $adodb->CacheGetAll(60, $query, $params);
		} catch (Exception $e) {
			reportError($e->getMessage(), $e->getTraceAsString());
			return null;
		}

		if($userid) {
			$username = uniqueid_to_username($userid);
			$userurl = Server::getUserURL($username);
		}

		$result = array();

		foreach ($res as &$i) {
			$row = sanitize($i);

			if(!$userid) {
				$row['username'] = uniqueid_to_username($row['userid']);
				$row['userurl'] = Server::getUserURL($row['username']);
			} else {
				$row['username'] = $username;
				$row['userurl'] = $userurl;
			}
			if ($row['album']) {
				$row['albumurl'] = Server::getAlbumURL($row['artist'], $row['album']);
			}
			$row['artisturl'] = Server::getArtistURL($row['artist']);
			$row['trackurl'] = Server::getTrackURL($row['artist'], $row['album'], $row['track']);

			$row['timehuman'] = human_timestamp($row['time']);
			$row['timeiso']   = date('c', (int)$row['time']);

			$row['id']        = identifierScrobbleEvent($row['username'], $row['artist'], $row['track'], $row['album'], $row['time'], $row['mbid'], $row['artist_mbid'], $row['album_mbid']);
			$row['id_artist'] = identifierArtist($row['username'], $row['artist'], $row['track'], $row['album'], $row['time'], $row['mbid'], $row['artist_mbid'], $row['album_mbid']);
			$row['id_track']  = identifierTrack($row['username'], $row['artist'], $row['track'], $row['album'], $row['time'], $row['mbid'], $row['artist_mbid'], $row['album_mbid']);
			$row['id_album']  = identifierAlbum($row['username'], $row['artist'], $row['track'], $row['album'], $row['time'], $row['mbid'], $row['artist_mbid'], $row['album_mbid']);

			if (!$row['album_image']) {
				$row['album_image'] = false;
			} else {
				$row['album_image'] = resolve_external_url($row['album_image']);
			}

			if ($row['artwork_license'] == 'amazon') {
				$row['album_image'] = str_replace('SL160', 'SL50', $row['album_image']);
			}

			$row['licenseurl'] = $row['license'];
			$row['license'] = simplify_license($row['licenseurl']);

			$result[] = $row;
		}

		return $result;
	}

	/**
	 * Retrieves a list of popular artists
	 *
	 * @param int $limit The number of artists to return
	 * @param int $offset Skip this number of rows before returning artists
	 * @param bool $streamable Only return streamable artists
	 * @param int $begin Only use scrobbles with time higher than this timestamp
	 * @param int $end Only use scrobbles with time lower than this timestamp
	 * @param int $userid Only return results from this userid
	 * @param int $cache Caching period in seconds
	 * @return array An array of artists ((artist, freq, artisturl) ..) or empty array in case of failure
	 */
	static function getTopArtists($limit = 21, $offset = 0, $streamable = False, $begin = null, $end = null, $userid = null, $cache = 600) {
		global $adodb;

		$query = ' SELECT artist, COUNT(artist) as freq FROM Scrobbles s';

		if ($streamable) {
			$query .= ' INNER JOIN Artist a ON s.artist=a.name WHERE a.streamable=1';
			$andquery = True;
		} else {
			if($begin || $end || $userid) {
				$query .= ' WHERE';
				$andquery = False;
			}
		}

		if($begin) {
			//change time resolution to full hours (for easier caching)
			$begin = $begin - ($begin % 3600);
			
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' time>' . (int)$begin;
		}

		if($end) {
			//change time resolution to full hours (for easier caching)
			$end = $end - ($end % 3600);
			
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' time<' . (int)$end;
		}

		if($userid) {
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' userid=' . (int)$userid;
		}

		$query .= ' GROUP BY artist ORDER BY freq DESC LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		try {
			$data = $adodb->CacheGetAll($cache, $query);
		} catch (Exception $e) {
			return array();
		}

		$result = array();

		foreach ($data as &$i) {
			$row = sanitize($i);
			$row['artisturl'] = Server::getArtistURL($row['artist']);
			$result[] = $row;
		}

		return $result;
	}

	/**
	 * Retrieves a list of loved artists
	 *
	 * @param int $limit The number of artists to return
	 * @param int $offset Skip this number of rows before returning artists
	 * @param bool $streamable Only return streamable artists
	 * @param int $userid Only return results from this userid
	 * @param int $cache Caching period in seconds
	 * @return array Artists ((artist, freq, artisturl) ..) or empty array in case of failure
	 */
	static function getLovedArtists($limit = 20, $offset = 0, $streamable = False, $userid = null, $cache = 600) {
		global $adodb;

		$query = ' SELECT artist, COUNT(artist) as freq FROM Loved_Tracks lt INNER JOIN Artist a ON a.name=lt.artist';

		if ($streamable) {
			$query .= ' WHERE a.streamable=1';
			$andquery = True;
		} else {
			if ($userid) {
				$query .= ' WHERE';
				$andquery = False;
			}
		}

		if ($userid) {
			$andquery ? $query .= ' AND' : null;
			$query .= ' userid=' . (int)$userid;
		}

		$query .= ' GROUP BY artist ORDER BY freq DESC LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		try {
			$data = $adodb->CacheGetAll($cache, $query);
		} catch (Exception $e) {
			return array();
		}

		$result = array();

		if (!empty($data)) {
			foreach ($data as &$i) {
				$row = sanitize($i);
				$row['artisturl'] = Server::getArtistURL($row['artist']);
				$result[] = $row;
			}
		}

		return $result;
	}

	/**
	 * Retrieves a list of popular tracks
	 *
	 * @param int $limit The number of tracks to return
	 * @param int $offset Skip this number of rows before returning tracks
	 * @param bool $streamable Only return streamable tracks
	 * @param int $begin Only use scrobbles with time higher than this timestamp
	 * @param int $end Only use scrobbles with time lower than this timestamp
	 * @param int $artist Only return results from this artist
	 * @param int $userid Only return results from this userid
	 * @param int $cache Caching period in seconds
	 * @return array Tracks ((artist, track, freq, listeners, artisturl, trackurl) ..) or empty array in case of failure
	 */
	static function getTopTracks($limit = 20, $offset = 0, $streamable = False, $begin = null, $end = null, $artist = null, $userid = null, $cache = 600) {
		global $adodb;

		$query = 'SELECT s.artist, s.track, count(s.track) AS freq, count(DISTINCT s.userid) AS listeners FROM Scrobbles s';
		
		if ($streamable) {
			$query .= ' WHERE ROW(s.artist, s.track) IN (SELECT artist_name, name FROM Track WHERE streamable=1)';
			$andquery = True;
		} else {
			if($begin || $end || $userid || $artist) {
				$query .= ' WHERE';
				$andquery = False;
			}
		}

		if($begin) {
			//change time resolution to full hours (for easier caching)
			$begin = $begin - ($begin % 3600);
			
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' s.time>' . (int)$begin;
		}

		if($end) {
			//change time resolution to full hours (for easier caching)
			$end = $end - ($end % 3600);
			
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' s.time<' . (int)$end;
		}

		if($userid) {
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' s.userid=' . (int)$userid;
		}

		if($artist) {
			$andquery ? $query .= ' AND' : $andquery = True;
			$query .= ' lower(s.artist)=lower(' . $adodb->qstr($artist) . ')';
		}
	
		$query .= ' GROUP BY s.track, s.artist ORDER BY freq DESC LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		try {
			$data = $adodb->CacheGetAll($cache, $query);
		} catch (Exception $e) {
			return array();
		}

		$result = array();

		foreach ($data as &$i) {
			$row = sanitize($i);
			$row['artisturl'] = Server::getArtistURL($row['artist']);
			$row['trackurl'] = Server::getTrackURL($row['artist'], null, $row['track']);
			$result[] = $row;
		}

		return $result;
	}


	/**
	 * Get a list of users with the most listens
	 *
	 * @param int $limit Amount of results to return
	 * @param int $offset Skip this many items before returning results
	 * @param int $streamable Only return results for streamable tracks
	 * @param int $begin Only use scrobbles with time higher than this timestamp
	 * @param int $end Only use scrobbles with time lower than this timestamp
	 * @param string $artist Filter results by this artist
	 * @param string $track Filter result by this track (need $artist to be set)
	 * @param int $cache Caching period in seconds
	 * @return array ((userid, freq, username, userurl) ..)
	 */
	static function getTopListeners($limit = 10, $offset = 0, $streamable = True, $begin = null, $end = null, $artist = null, $track = null, $cache = 600) {
		global $adodb;

		$params = array();
		$query = 'SELECT s.userid, COUNT(*) as freq FROM Scrobbles s';

		if ($streamable) {
			$query .= ' WHERE ROW(s.artist, s.track) IN (SELECT artist_name, name FROM Track WHERE streamable=1)';
			$andquery = True;
		} else {
			if($begin || $end || $artist) {
				$query .= ' WHERE';
				$andquery = False;
			}
		}

		if($begin) {
			//change time resolution to full hours (for easier caching)
			$begin = $begin - ($begin % 3600);
			
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' s.time > ?';
			$params[] = (int)$begin;
		}

		if($end) {
			//change time resolution to full hours (for easier caching)
			$end = $end - ($end % 3600);
			
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' s.time < ?';
			$params[] = (int)$end;
		}

		if($artist) {
			$andquery ? $query .= ' AND' : $andquery = True;
			$query .= ' lower(s.artist)=lower(?)';
			$params[] = $artist;

			if($track) {
				$andquery ? $query .= ' AND' : $andquery = True;
				$query .= ' lower(s.track)=lower(?)';
				$params[] = $track;
			}
		}
	
		$query .= ' GROUP BY s.userid ORDER BY freq DESC LIMIT ? OFFSET ?';
		$params[] = (int)$limit;
		$params[] = (int)$offset;

		try {
			$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
			$res = $adodb->CacheGetAll($cache, $query, $params);
		}catch (Exception $e) {
			return array();
		}

		foreach($res as &$row) {
			$row['username'] = uniqueid_to_username($row['userid']);
			$row['userurl'] = Server::getUserURL($row['username']);
			$result[] = $row;
		}

		return $result;
	}


	/**
	 * Retrieves a list of loved tracks
	 *
	 * @param int $limit The number of tracks to return
	 * @param int $offset Skip this number of rows before returning tracks
	 * @param bool $streamable Only return streamable tracks
	 * @param int $artist Only return results from this artist
	 * @param int $userid Only return results from this userid
	 * @param int $cache Caching period in seconds
	 * @return array Tracks ((artist, track, freq, listeners, artisturl, trackurl) ..) or empty array in case of failure
	 */
	static function getLovedTracks($limit = 20, $offset = 0, $streamable = False, $artist = null, $userid = null, $cache = 600) {
		global $adodb;

		$query = 'SELECT lt.artist, lt.track, max(lt.time) as time, count(lt.track) AS freq FROM Loved_Tracks lt';
		
		if ($streamable) {
			$query .= ' WHERE ROW(lt.artist, lt.track) IN (SELECT artist_name, name FROM Track WHERE streamable=1)';
			$andquery = True;
		} else {
			if($userid || $artist) {
				$query .= ' WHERE';
				$andquery = False;
			}
		}

		if($userid) {
			$andquery ? $query .= ' AND' : $andquery = True ;
			$query .= ' lt.userid=' . (int)$userid;
		}

		if($artist) {
			$andquery ? $query .= ' AND' : $andquery = True;
			$query .= ' lower(lt.artist)=lower(' . $adodb->qstr($artist) . ')';
		}
	
		$query .= ' GROUP BY lt.track, lt.artist ORDER BY freq DESC, time DESC LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		try {
			$data = $adodb->CacheGetAll($cache, $query);
		} catch (Exception $e) {
			return array();
		}

		$result = array();

		foreach ($data as &$i) {
			$row = sanitize($i);
			$row['artisturl'] = Server::getArtistURL($row['artist']);
			$row['trackurl'] = Server::getTrackURL($row['artist'], null, $row['track']);
			$result[] = $row;
		}

		return $result;
	}

	/**
	 * Get a list of users
	 *
	 * @param string $alpha Search for user names starting with this string
	 */
	static function getUserList($alpha) {
		global $adodb;

		$alpha .= '%';
		$query = 'SELECT username from Users where username LIKE ' . $adodb->qstr($alpha) . ' ORDER BY username ASC';

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		$data = $adodb->CacheGetAll(7200, $query);
		if (!$data) {
			throw new Exception('ERROR ' . $query);
		}

		return $data;
	}

	/**
	 * Retrieves a list of the currently playing tracks
	 *
	 * @param int $number The maximum number of tracks to return
	 * @param string $username The name of the user to retrieve playing tracks for
	 * @return array Now playing data or null in case of failure
	 */
	static function getNowPlaying($number = 1, $username = false) {
		global $adodb;

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		try {
			if ($username) {
				$data = $adodb->CacheGetAll(1, 'SELECT
							ss.userid,
							n.artist,
							n.track,
							n.album,
							client,
							api_key,
							n.mbid,
							t.license
						FROM Now_Playing n
						LEFT OUTER JOIN Scrobble_Sessions ss
							ON n.sessionid=ss.sessionid
						LEFT OUTER JOIN Track t
							ON lower(n.artist) = lower(t.artist_name)
							AND lower(n.album) = lower(t.album_name)
							AND lower(n.track) = lower(t.name)
							AND lower(n.mbid) = lower(t.mbid)
						WHERE ss.userid= ' . username_to_uniqueid($username) . '
						ORDER BY t.streamable DESC, n.expires DESC LIMIT ' . (int)($number));
			} else {
				$data = $adodb->CacheGetAll(60, 'SELECT
							ss.userid,
							n.artist,
							n.track,
							n.album,
							client,
							n.mbid,
							t.license
						FROM Now_Playing n
						LEFT OUTER JOIN Scrobble_Sessions ss
							ON n.sessionid=ss.sessionid
						LEFT OUTER JOIN Track t
							ON lower(n.artist) = lower(t.artist_name)
							AND lower(n.album) = lower(t.album_name)
							AND lower(n.track) = lower(t.name)
							AND lower(n.mbid) = lower(t.mbid)
						ORDER BY t.streamable DESC, n.expires DESC LIMIT ' . (int)($number));
			}
		} catch (Exception $e) {
			return null;
		}

		$result = array();

		foreach ($data as &$i) {
			$row = sanitize($i);
			
			$client = getClientData($row['client'], $row['api_key']);
			$row['clientcode'] = $client['code'];
			$row['clientapi_key'] = $client['code'];
			$row['clientname'] = $client['name'];
			$row['clienturl'] = $client['url'];
			$row['clientfree'] = $client['free'];
			
			$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 ($username) {
				$row['loved'] = $adodb->CacheGetOne(60, 'SELECT Count(*) FROM Loved_Tracks WHERE artist='
					. $adodb->qstr($row['artist'])
					. ' AND track=' . $adodb->qstr($row['track'])
					. ' AND userid=' . $row['userid']);
			}

			// We really want to get an image URI from the database and only fall back to qm50.png if we can't find an image.
			$row['albumart'] = $base_url . 'themes/' . $default_theme . '/images/qm50.png';

			$row['licenseurl'] = $row['license'];
			$row['license'] = simplify_license($row['licenseurl']);

			$result[] = $row;
		}

		return $result;
	}

	/**
	 * Gets the URL to a user's profile page
	 *
	 * The get*URL functions are implemented here rather than in their respective
	 * objects so that we can produce URLs without needing to build whole objects.
	 *
	 * @param string $username The user name we want a URL for
	 * @param string $component Type of URL to return
	 * @param string $params Trailing get parameters
	 * @return string URL to the user's profile
	 */
	static function getUserURL ($username, $component = 'profile', $params = false) {
		global $friendly_urls, $base_url;
		if ($component == 'edit') {
			return $base_url . '/user-edit.php';
		} else if ($component == 'delete') {
			return $base_url . '/delete-profile.php';
		} else if ($friendly_urls) {
			if ($component == 'profile') {
				$component = '';
			} else {
				$component = "/{$component}";
			}
			return $base_url . '/user/' . rewrite_encode($username) . $component . ($params ? '?' . $params : null);
		} else {
			return $base_url . "/user-{$component}.php?user=" . rawurlencode($username) . ($params ? '&' . $params : null);
		}
	}

	/**
	 * Gets the URL to a group's page
	 *
	 * @param string $groupname The group we want a URL for
	 * @return string URL to the group's page
	 */
	static function getGroupURL($groupname) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return $base_url . '/group/' . rewrite_encode($groupname);
		} else {
			return $base_url . '/group.php?group=' . rawurlencode($groupname);
		}
	}

	/**
	 * Gets the URL to an artist's page
	 *
	 * @param string $artist The artist we want a URL for
	 * @param string $component Type of URL to return
	 * @return string URL to the artist's page
	 */
	static function getArtistURL($artist, $component = '') {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return $base_url . '/artist/' . rewrite_encode($artist) . '/' . $component;
		} else {
			if ($component) {
				return $base_url . '/artist-' . $component . '.php?artist=' . rawurlencode($artist);
			} else {
				return $base_url . '/artist.php?artist=' . rawurlencode($artist);
			}
		}
	}
	/**
	 * Gives the URL to the management interface for an artist
	 *
	 * @param string $artist The artist we want a URL for
	 * @return string URL for an artist's management interface
	 */
	static function getArtistManagementURL($artist) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return Server::getArtistURL($artist) . '/manage';
		} else {
			return $base_url . '/artist-manage.php?artist=' . rawurlencode($artist);
		}
	}

	/**
	 * Gives the URL for managers to add a new album to an artist
	 *
	 * @param string $artist The artist we want a URL for
	 * @return string URL for adding albums to an artist
	 */
	static function getAddAlbumURL($artist) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return Server::getArtistURL($artist) . '/album/add';
		} else {
			return $base_url . '/album-add.php?artist=' . rawurlencode($artist);
		}
	}

	/**
	 * Gets the URL to an album's page
	 *
	 * @param string $artist The artist name of the album
	 * @param string $album The name of the album
	 * @return string URL to the album's page
	 */
	static function getAlbumURL($artist, $album) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return $base_url . '/artist/' . rewrite_encode($artist) . '/album/' . rewrite_encode($album);
		} else {
			return $base_url . '/album.php?artist=' . rawurlencode($artist) . '&album=' . rawurlencode($album);
		}
	}

	/**
	 * Gives the URL for managers to add a new track to an album
	 *
	 * @param string $artist The artist name of the album
	 * @param string $album The name of the album
	 * @return string URL for adding tracks to an album
	 */
	static function getAddTrackURL($artist, $album) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return Server::getAlbumURL($artist, $album) . '/track/add';
		} else {
			return $base_url . '/track-add.php?artist=' . rawurlencode($artist) . '&album=' . rawurlencode($album);
		}
	}

	/**
	 * Gets the URL to a track's page
	 *
	 * @param string $artist The artist name of the track
	 * @param string $album The album name of this track (optional)
	 * @param string $track The name of the track
	 * @param string $component Type of page
	 * @return string URL to the track's page
	 */
	static function getTrackURL($artist, $album, $track, $component = '') {
		global $friendly_urls, $base_url;

		if($friendly_urls) {
			$trackurl = $base_url . '/artist/' . rewrite_encode($artist);
			if($album) {
				$trackurl .= '/album/' . rewrite_encode($album);
			}
			$trackurl .= '/track/' . rewrite_encode($track);
			if($component) {
				$trackurl .= '/' . $component;
			}
		} else {
			if($component) {
				$trackurl = $base_url . '/track-' . $component . '.php?artist='	. rawurlencode($artist)
					. '&album=' . rawurlencode($album) . '&track=' . rawurlencode($track);
			} else {
				$trackurl = $base_url . '/track.php?artist=' . rawurlencode($artist)
					. '&album=' . rawurlencode($album) . '&track=' . rawurlencode($track);
			}
		}

		return $trackurl;
	}

	/**
	 * Gets the URL to a track's edit page
	 *
	 * @param string $artist The artist name of the track
	 * @param string $album The album name of this track (optional)
	 * @param string $track The name of the track
	 * @return string URL to the track's edit page
	 */
	static function getTrackEditURL($artist, $album, $track) {
		global $friendly_urls, $base_url;
		if ($friendly_urls && $album) {
			return $base_url . '/artist/' . rewrite_encode($artist) . '/album/' . rewrite_encode($album) . '/track/' . rewrite_encode($track) . '/edit';
		} else if ($friendly_urls) {
			return $base_url . '/artist/' . rewrite_encode($artist) . '/track/' . rewrite_encode($track) . '/edit';
		} else {
			return $base_url . '/track-add.php?artist=' . rawurlencode($artist) . '&album=' . rawurlencode($album) . '&track=' . rawurlencode($track);
		}
	}

	static function getAlbumEditURL($artist, $album) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return $base_url . '/artist/' . rewrite_encode($artist) . '/album/' . rewrite_encode($album) . '/edit';
		} else {
			return $base_url . '/album-add.php?artist=' . rawurlencode($artist) . '&album=' . rawurlencode($album);
		}
	}

	/**
	 * Gets the URL to a tag's page
	 *
	 * @param string $tag The name of the tag
	 * @return string URL to the tag's page
	 */
	static function getTagURL($tag) {
		global $friendly_urls, $base_url;
		if ($friendly_urls) {
			return $base_url . '/tag/' . rewrite_encode($tag);
		} else {
			return $base_url . '/tag.php?tag=' . rawurlencode($tag);
		}
	}

	static function getLocationDetails($name) {
		global $adodb;

		if (!$name) {
			return array();
		}

		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		$rv = $adodb->GetRow('SELECT p.latitude, p.longitude, p.country, c.country_name, c.wikipedia_en '
			. 'FROM Places p '
			. 'LEFT JOIN Countries c ON p.country=c.country '
			. 'WHERE p.location_uri=' . $adodb->qstr($name, 'text'));

		if ($rv) {

			if (!($rv['latitude'] && $rv['longitude'] && $rv['country'])) {

				$parser = ARC2::getRDFXMLParser();
				$parser->parse($name);
				$index = $parser->getSimpleIndex();

				$rv = array(
					'latitude'  => $index[$name]['http://www.w3.org/2003/01/geo/wgs84_pos#lat'][0],
					'longitude' => $index[$name]['http://www.w3.org/2003/01/geo/wgs84_pos#long'][0],
					'country'   => strtoupper(substr($index[$name]['http://www.geonames.org/ontology#inCountry'][0], -2))
					);

				$adodb->Execute(sprintf('UPDATE Places SET latitude=%s, longitude=%s, country=%s WHERE location_uri=%s',
					$adodb->qstr($rv['latitude']),
					$adodb->qstr($rv['longitude']),
					$adodb->qstr($rv['country']),
					$adodb->qstr($name)));
			}
		} else {
			$parser = ARC2::getRDFXMLParser();
			$parser->parse($name);
			$index = $parser->getSimpleIndex();

			$rv = array(
				'latitude'  => $index[$name]['http://www.w3.org/2003/01/geo/wgs84_pos#lat'][0],
				'longitude' => $index[$name]['http://www.w3.org/2003/01/geo/wgs84_pos#long'][0],
				'country'   => strtoupper(substr($index[$name]['http://www.geonames.org/ontology#inCountry'][0], -2))
				);

			$adodb->Execute(sprintf('INSERT INTO Places (location_uri, latitude, longitude, country) VALUES (%s, %s, %s, %s)',
				$adodb->qstr($name),
				$adodb->qstr($rv['latitude']),
				$adodb->qstr($rv['longitude']),
				$adodb->qstr($rv['country'])));
		}

		return $rv;
	}

	/**
	 * Log in to the radio server
	 *
	 * @param string $station The station to be played
	 * @param string $username The user to associate this session with (optional)
	 * @param string $session_id Allows for a custom session id to be set, allowing for compatibility with webservices
	 * @return string Session key to be used for streaming
	 */
	static function getRadioSession($station, $username = false, $session_id = false) {
		global $adodb;
		if (!$session_id) {
			$session_id = md5(mt_rand() . time());
		}
		// Remove any previous station for this session id
		$adodb->Execute('DELETE FROM Radio_Sessions WHERE session = ' . $adodb->qstr($session_id));
		if ($username) {
			$sql = 'INSERT INTO Radio_Sessions(username, session, url, expires) VALUES ('
				. $adodb->qstr($username) . ','
				. $adodb->qstr($session_id) . ','
				. $adodb->qstr($station) . ','
				. (int)(time() + 86400) . ')';
		} else {
			$sql = 'INSERT INTO Radio_Sessions(session, url, expires) VALUES ('
				. $adodb->qstr($session_id) . ','
				. $adodb->qstr($station) . ','
				. (int)(time() + 86400) . ')';
		}
		$res = $adodb->Execute($sql);
		return $session_id;
	}

	/**
	 * Log in to web services
	 *
	 * @param string $username The user to create a session for
	 * @return string The web service session key
	 */
	static function getWebServiceSession($username) {
		global $adodb;
		$sk = md5(mt_rand() . time());
		$token = md5(mt_rand() . time());
		$adodb->Execute('INSERT INTO Auth(token, sk, expires, username) VALUES ('
			. $adodb->qstr($token) . ', '
			. $adodb->qstr($sk) . ', '
			. (int)(time() + 86400) . ', '
			. $adodb->qstr($username) . ')');
		return $sk;
	}

	/**
	 * Get scrobble session ID for a user.
	 *
	 * Gets the most recent scrobble session ID for userid,
	 * or creates a new session ID if it can't find one.
	 *
	 * @param int userid (required)			User ID.
	 * @param string api_key (optional)		Client API key (32 characters)
	 * @param int expire_limit (optional)	Amount of time in seconds before session will expire (defaults to 86400 = 24 hours)
	 * @return string						Scrobble session ID
	 */
	static function getScrobbleSession($userid, $api_key = null, $expire_limit = 86400) {
		//TODO Add code to remove expired sessions (this is currently only done in gnukebox)
		global $adodb;
		$query = 'SELECT sessionid FROM Scrobble_Sessions WHERE userid = ? AND expires > ?';
		$params = array( (int) $userid, time());

		if (strlen($api_key) == 32) {
			$query .= ' AND api_key=?';
			$params[] = $api_key;
		} elseif (strlen($api_key) == 3) {
			// api_key is really a 3 char client code (2.0-scrobble-proxy.php sends client code in api_key)
			$query .= ' AND client=?';
			$client_id = $api_key;
			$params[] = $client_id;
			// we dont want to insert a 3 char code as api_key in db
			$api_key = null;
		}

		$sessionid = $adodb->GetOne($query, $params);
		if (!$sessionid) {
			$sessionid = md5(mt_rand() . time());
			$expires = time() + (int) $expire_limit;
			$query = 'INSERT INTO Scrobble_Sessions(userid, sessionid, client, expires, api_key) VALUES (?,?,?,?,?)';
			$params = array($userid, $sessionid, $client_id, $expires, $api_key);
			try {
				$adodb->Execute($query, $params);
			} catch (Exception $e) {
				return null;
			}
		}
		return $sessionid;
	}

	/**
	 * Get all artists
	 *
	 * @return array Artists ordered by name
	 */
	static function getAllArtists() {
		global $adodb;

		$sql = 'SELECT * from Artist ORDER by name';
		$adodb->SetFetchMode(ADODB_FETCH_ASSOC);
		try {
			$res = $adodb->CacheGetAll(86400, $sql);
		} catch (Exception $e) {
			return null;
		}

		$result = array();
		foreach ($res as &$i) {
			$row = sanitize($i);

			$row['artisturl'] = Server::getArtistURL($row['name']);
			$result[] = $row;
		}

		return $result;
	}

	/**
	 * Search for users, artists or tags
	 *
	 * Does a lower-case search of %search_term%
	 *
	 * @param string $search_term
	 * @param string $search_type Type of search, artist|user|tag
	 * @param int $limit How many items to return
	 * @param bool $streamable Only return streamable artists
	 * @return array Results
	 */
	static function search($search_term, $search_type, $limit = 40, $streamable = false) {
		global $adodb;

		if ($search_term) {

		    switch ($search_type) {
		    case 'artist':
		      $table = 'Artist';
		      $search_fields[] = 'name';
		      $data_fields[] = 'name';
		      $data_fields[] = 'bio_summary';
		      $data_fields[] = 'streamable';
		      $data_fields[] = 'image_small';
		      $data_fields[] = 'image_medium';
		      $data_fields[] = 'image_large';
		      break;
		    case 'user':
		      $table = 'Users';
		      $search_fields[] = 'username';
		      $search_fields[] = 'fullname';
		      $data_fields[] = 'username';
		      $data_fields[] = 'fullname';
		      $data_fields[] = 'bio';
		      break;
		    case 'tag':
		      $table = 'Tags';
		      $search_fields[] = 'tag';
		      $data_fields[] = 'tag';
		      break;
		    default:
		      return array();
		    }

		    $sql = 'SELECT DISTINCT ';

		    for ($i = 0; $i < count($data_fields); $i++) {
		      $sql .= $data_fields[$i];
		      if ($i < count($data_fields) - 1) {
			$sql .= ', ';
		      }
		    }

		    $sql .= ' FROM ' . $table . ' WHERE ';

		    for ($i = 0; $i < count($search_fields); $i++) {
		      if ($i > 0) {
			$sql .= ' OR ';
		      }
		      $sql .= 'LOWER(' . $search_fields[$i] . ') LIKE LOWER(' . $adodb->qstr('%' . $search_term . '%') . ')';
		    }

		    if ($streamable) {
		      $sql .= " AND streamable = 1 ";
		    }

		    $sql .= 'LIMIT ' . $limit;

		    $res = $adodb->CacheGetAll(600, $sql);

		    $result = array();
		    foreach ($res as &$i) {
		      $row = sanitize($i);
		      switch ($search_type) {
		      case 'artist':
			$row['url'] = Server::getArtistURL($row['name']);
			break;
		      case 'user':
			$row['url'] = Server::getUserURL($row['username']);
			break;
		      case 'tag':
			$row['url'] = Server::getTagURL($row['tag']);
			break;
		      }
		      $result[] = $row;
		    }

		  }

		return $result;
	}

	/**
	 * Create a random authentication token and return it
	 *
	 * @return string Token.
	 */
	static function getAuthToken() {
		global $adodb;

		$key = md5(time() . rand());
		$expires = (int) (time() + 3600);

		$query = 'INSERT INTO Auth(token, expires) VALUES(?,?)';
		$params = array($key, $expires);
		try {
			$adodb->Execute($query, $params);
			return $key;
		} catch (Exception $e) {
			reportError($e->getMessage(), $e->getTraceAsString());
		}
	}

}
