git.gnu.io has moved to IP address 209.51.188.249 -- please double check where you are logging in.

Commit bc149f45 authored by Jonas Haraldsson's avatar Jonas Haraldsson

Merge branch 'player-2.0'

parents 33a2fef0 5f46521d
How to install phpdoc and generate api docs gnu-fm (on Debian Squeeze)
----------
---
1. Install dependencies and phpDocumentor-alpha:
---
# aptitude install php-pear php5-xsl
# pear channel-discover pear.phpdoc.org
# pear install phpdoc/phpDocumentor-alpha
for other ways to install, see http://www.phpdoc.org/docs/latest/for-users/installation.html
---
2. Create dir which will be holding the docs, and generate docs:
---
cd /path/to/gnu-fm/nixtape
mkdir docs
phpdoc -d 2.0/ -t docs/
The docs can now be found at http://mynixtapedomain.tld/docs/
Add something similar to this to your crontab to keep the docs up-to-date:
0 0 * * * phpdoc -q -d /path/to/gnu-fm/nixtape/2.0/ -t /path/to/gnu-fm/nixtape/docs
......@@ -198,6 +198,7 @@ if (isset($_POST['install'])) {
userid INTEGER REFERENCES Users(uniqueid),
sessionid VARCHAR(32) PRIMARY KEY,
client CHAR(3),
api_key VARCHAR(32),
expires INTEGER)',
'CREATE TABLE Now_Playing(
......@@ -378,17 +379,22 @@ if (isset($_POST['install'])) {
}
$adodb->Execute("CREATE INDEX scrobbles_time_idx ON Scrobbles(time)");
$adodb->Execute("CREATE INDEX scrobbles_userid_idx ON Scrobbles(userid)");
$adodb->Execute("CREATE INDEX scrobbles_userid_time_idx ON Scrobbles(userid, time)");
$adodb->Execute("CREATE INDEX scrobbles_track_idx on Scrobbles(track)");
$adodb->Execute("CREATE INDEX scrobble_track_name_idx ON Scrobble_Track(name)");
$adodb->Execute("CREATE INDEX track_streamable_idx on Track(streamable);");
$adodb->Execute("CREATE INDEX track_name_idx ON Track(name)");
$adodb->Execute("CREATE INDEX album_name_idx ON Album(name)");
$adodb->Execute("CREATE INDEX artist_name_idx ON Artist(name)");
if(strtolower(substr($dbms, 0, 5)) == 'pgsql') {
// MySQL doesn't support the use of lower() to create case-insensitive indexes
$adodb->Execute("CREATE INDEX album_artistname_idx ON Album(lower(artist_name))");
$adodb->Execute("CREATE INDEX track_artist_idx ON Track(lower(artist_name))");
$adodb->Execute("CREATE INDEX track_name_idx ON Track(lower(name))");
$adodb->Execute("CREATE INDEX scrobbles_artist_idx on Scrobbles(lower(artist))");
$adodb->Execute("CREATE INDEX scrobbles_track_idx on Scrobbles(lower(track))");
$adodb->Execute("CREATE INDEX groups_groupname_idx ON Groups(lower(groupname))");
$adodb->Execute("CREATE INDEX album_lower_artistname_idx ON Album(lower(artist_name))");
$adodb->Execute("CREATE INDEX track_lower_artist_idx ON Track(lower(artist_name))");
$adodb->Execute("CREATE INDEX track_lower_name_idx ON Track(lower(name))");
$adodb->Execute("CREATE INDEX scrobbles_lower_artist_idx on Scrobbles(lower(artist))");
$adodb->Execute("CREATE INDEX scrobbles_lower_track_idx on Scrobbles(lower(track))");
$adodb->Execute("CREATE INDEX groups_lower_groupname_idx ON Groups(lower(groupname))");
// PostgreSQL stored functions
$adodb->Execute("CREATE OR REPLACE LANGUAGE plpgsql;");
......
......@@ -2,11 +2,19 @@ Options +FollowSymLinks -MultiViews
RewriteEngine on
RewriteRule ^user/([^/]+)/?$ user-profile.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/journal/?$ user-journal.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/groups/?$ user-groups.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/recent-tracks/?$ user-recent-tracks.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/stats/?$ user-stats.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/station/?$ user-station.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/library/?$ user-library.php?user=$1 [NC,QSA]
RewriteRule ^user/([^/]+)/library/music/?$ user-library.php?user=$1&section=music [NC,QSA]
RewriteRule ^user/([^/]+)/library/scrobbles/?$ user-library.php?user=$1&section=scrobbles [NC,QSA]
RewriteRule ^user/([^/]+)/library/loved/?$ user-library.php?user=$1&section=loved [NC,QSA]
RewriteRule ^user/([^/]+)/library/banned/?$ user-library.php?user=$1&section=banned [NC,QSA]
RewriteRule ^user/([^/]+)/library/tags/?$ user-library.php?user=$1&section=tags [NC,QSA]
RewriteRule ^user/([^/]+)/library/tags/([^/]+)/?$ user-library.php?user=$1&section=tags&tag=$2 [NC,QSA]
RewriteRule ^user/([^/]+)/library/music/([^/]+)/?$ user-library.php?user=$1&section=music&artist=$2 [NC,QSA]
RewriteRule ^user/([^/]+)/library/music/([^/]+)/([^/]+)/?$ user-library.php?user=$1&section=music&artist=$2&album=$3 [NC,QSA]
RewriteRule ^user/([^/]+)/library/music/([^/]+)/_/([^/]+)/?$ user-library.php?user=$1&section=music&artist=$2&track=$3 [NC,QSA]
RewriteRule ^artist/([^/]+)/track/([^/]+)/edit/?$ track-add.php?artist=$1&track=$2 [NC,QSA]
RewriteRule ^artist/([^/]+)/track/([^/]+)/tag/?$ track-tag.php?artist=$1&track=$2 [NC,QSA]
RewriteRule ^artist/([^/]+)/track/([^/]+)/?$ track.php?artist=$1&track=$2 [NC,QSA]
......@@ -20,12 +28,8 @@ RewriteRule ^artist/([^/]+)/album/([^/]+)/?$ album.php?artist=$1&album=$2 [
RewriteRule ^artist/([^/]+)/?$ artist.php?artist=$1 [NC,QSA]
RewriteRule ^artist/([^/]+)/manage/?$ artist-manage.php?artist=$1 [NC,QSA]
RewriteRule ^artist/([^/]+)/tag/?$ artist-tag.php?artist=$1 [NC,QSA]
RewriteRule ^group/new$ edit_group.php?group=new [NC,QSA]
RewriteRule ^group/([^/]+)/?$ group.php?group=$1 [NC,QSA]
RewriteRule ^group/?$ group.php [NC,QSA]
RewriteRule ^country/([^/]+)/?$ location.php?country=$1 [NC,QSA]
RewriteRule ^logout login.php?action=logout [NC,QSA]
RewriteRule ^listen listen.php [NC,QSA]
RewriteRule ^music popular.php [NC,QSA]
RewriteRule ^users users.php [NC,QSA]
RewriteRule ^tag/([^/]+)/?$ tag.php?tag=$1 [NC,QSA]
......@@ -120,6 +120,8 @@ $method_map = array(
'track.love' => method_track_love,
'track.unlove' => method_track_unlove,
'track.unban' => method_track_unban,
'track.updatenowplaying' => method_track_updateNowPlaying,
'track.scrobble' => method_track_scrobble,
);
/**
......@@ -895,10 +897,10 @@ function method_auth_getSession() {
* Remove a scrobble from user's library
*
* #Parameters
* **timestamp** (required) : Timestamp in Unix time.
* **artist** (required) : Artist name.
* **track** (required) : Track name.
* **sk** (required) : Session key.
* * **timestamp** (required) : Timestamp in Unix time.
* * **artist** (required) : Artist name.
* * **track** (required) : Track name.
* * **sk** (required) : Session key.
* * **format** (optional) : Format of response, **xml** or **json**. Default is xml.
*
* #Additional info
......@@ -1356,6 +1358,124 @@ function method_track_unban() {
respond($xml);
}
/**
* track.updatenowplaying : Submits the user's currently playing track.
*
* ###Description
* Submits the user's currently playing track.
*
* ###Parameters
* * **artist** (required) : Artist name.
* * **track** (required) : Track name.
* * **sk** (required) : Session key.
* * **album** (optional) : Album name.
* * **tracknumber (optional) : Track's number on the album.
* * **context** (optional) : TODO
* * **mbid** (optional) : Track's musicbrainz ID.
* * **duration** (optional) : Length of the track in seconds.
* * **albumartist (optional) : Album's artist.
* * **api_key (optional) : Client API key.
* * **format** (optional) : Format of response, **xml** or **json**. Default is xml.
*
* ###Additional info
* **This method requires authentication**.
*
* **HTTP request method** : POST.
* - - -
*
* @todo context parameter not used
* @todo tracknumber parameter not stored in db
* @todo albumartist parameter not stored in db
* @package Webservice
* @subpackage Track
* @api
*/
function method_track_updateNowPlaying() {
if (!isset($_POST['artist']) || !isset($_POST['track'])) {
report_failure(LFM_INVALID_PARAMS);
}
$_POST_lower = array_change_key_case($_POST, CASE_LOWER);
$userid = get_userid();
$xml = TrackXML::updateNowPlaying($userid,
$_POST['artist'],
$_POST['track'],
$_POST['album'],
$_POST_lower['tracknumber'],
$_POST['context'],
$_POST['mbid'],
$_POST['duration'],
$_POST_lower['albumartist'],
$_POST['api_key']
);
respond($xml);
}
/**
* track.scrobble : Submits a track for scrobbling.
*
* ###Description
*
* Submits a track or a batch of tracks for scrobbling.
*
* ###Parameters
* * **artist[i]** (required) : Artist name.
* * **track[i]** (required) : Track name.
* * **sk** (required) : Session key.
* * **timestamp[i]** (required) : The time the track started playing (in UNIX time).
* * **album[i]** (optional) : Album name.
* * **context[i]** (optional) : TODO
* * **streamid[i]** (optional) : TODO
* * **chosenbyuser[i]** (optional) : TODO
* * **tracknumber[i]** (optional) : Track's number on album.
* * **mbid[i]** (optional) : Track's Musicbrainz ID.
* * **albumartist[i]** (optional) : Album's artist.
* * **duration[i]** (optional) : Length of the track in seconds.
* * **api_key (optional) : Client API key.
* * **format** (optional) : Format of response, **xml** or **json**. Default is xml.
*
* ###Additional info
* **This method requires authentication**.
*
* **HTTP request method** : POST.
* - - -
*
* @todo context parameter not used
* @todo streamid parameter not used
* @todo chosenbyuser parameter not used
* @todo tracknumber parameter not stored in db
* @todo albumartist parameter not stored in db
* @package Webservice
* @subpackage Track
* @api
*/
function method_track_scrobble() {
if (!isset($_POST['artist']) || !isset($_POST['track']) || !isset($_POST['timestamp'])) {
report_failure(LFM_INVALID_PARAMS);
}
$_POST_lower = array_change_key_case($_POST, CASE_LOWER);
$userid = get_userid();
$xml = TrackXML::scrobble($userid,
$_POST['artist'],
$_POST['track'],
$_POST['timestamp'],
$_POST['album'],
$_POST['context'],
$_POST_lower['streamid'],
$_POST_lower['chosenbyuser'],
$_POST_lower['tracknumber'],
$_POST['mbid'],
$_POST_lower['albumartist'],
$_POST['duration'],
$_POST['api_key']
);
respond($xml);
}
/**
* tag.gettoptags : Get the top tags.
*
......
......@@ -58,4 +58,3 @@ if (isset($this_user) && $this_user->manages($artist->name)) {
}
$smarty->assign('pagetitle', $artist->name . ' : ' . $album->name);
$smarty->assign('headerfile', 'album-header.tpl');
This diff is collapsed.
......@@ -97,8 +97,14 @@ if (!isset($_REQUEST['api_key']) || !(isset($_REQUEST['cb']) || isset($_REQUEST[
// Web app auth step 2.2
if(isset($_POST['cb'])) {
$redirect_url = $_POST['cb'];
header('Location:' . $redirect_url . '&token=' . $_POST['token']);
$callback_url = $_POST['cb'];
if (preg_match("/\?/", $callback_url)) {
$redirect_url = $callback_url . '&token=' . $_POST['token'];
} else {
$redirect_url = $callback_url . '?token=' . $_POST['token'];
}
header('Location:' . $redirect_url);
// Desktop app auth step 2.2
} else {
......
......@@ -49,4 +49,3 @@ if (isset($this_user) && $this_user->manages($artist->name)) {
}
$smarty->assign('pagetitle', $artist->name);
$smarty->assign('headerfile', 'artist-header.tpl');
......@@ -368,7 +368,7 @@ class Artist {
// Narrow down similar artists to ones that at least share the most common tag and get hold of their other tags
$otherArtists = $adodb->CacheGetAll(86400, 'SELECT artist, lower(tag) as ltag, count(tag) as num FROM Tags INNER JOIN Artist ON Artist.name = Tags.artist WHERE Artist.streamable = 1 AND artist in '
. '(SELECT distinct(artist) FROM Tags WHERE lower(tag) = ' . $adodb->qstr($tmpTags[0]['ltag']) . ') '
. 'GROUP BY artist, ltag ORDER BY num DESC');
. 'GROUP BY artist, ltag ORDER BY num DESC LIMIT 1000');
$totalTags = array();
......
This diff is collapsed.
......@@ -534,6 +534,7 @@ class Server {
n.track,
n.album,
client,
api_key,
n.mbid,
t.license
FROM Now_Playing n
......@@ -573,12 +574,14 @@ class Server {
foreach ($data as &$i) {
$row = sanitize($i);
$client = getClientData($row['client']);
if(is_array($client)) {
$row['clientname'] = $client['name'];
$row['clienturl'] = $client['url'];
$row['clientfree'] = $client['free'];
}
$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']);
......
......@@ -46,6 +46,8 @@ class Track {
*
* @param string $name The name of the track to load
* @param string $artist The name of the artist who recorded this track
*
* @todo Should we call Track::create() instead of throwing "No such track" exception?
*/
function __construct($name, $artist) {
global $adodb;
......@@ -324,59 +326,74 @@ class Track {
/**
* Add a list of tags to a track
*
* @param string $tags A comma-separated list of tags
* @param int $userid The user adding these tags
* @param string $tags A comma-separated list of tags.
* @param int $userid The user adding these tags.
* @return bool True if any tag was added, False if no tags were added.
*/
function addTags($tags, $userid) {
global $adodb;
$tags = explode(',', strtolower($tags));
$query = 'INSERT INTO Tags (tag, artist, album, track, userid) VALUES(?,?,?,?,?)';
foreach($tags as $tag) {
$tag = trim($tag);
if(strlen($tag) == 0) {
continue;
}
$params = array($tag, $this->artist_name, $this->album_name, $this->name, (int) $userid);
try {
$adodb->Execute('INSERT INTO Tags VALUES ('
. $adodb->qstr($tag) . ','
. $adodb->qstr($this->artist_name) . ', '
. $adodb->qstr($this->album_name) . ', '
. $adodb->qstr($this->name) . ', '
. $userid . ')');
} catch (Exception $e) {}
$adodb->Execute($query, $params);
if ($adodb->Affected_Rows()) {
$res = $res + 1;
}
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
}
}
return (bool) $res;
}
/**
* Love a track
*
* @param int $userid The user loving this track
* @param int $userid The user loving this track.
* @return bool True on success, False on fail.
*/
function love($userid) {
global $adodb;
$query = 'INSERT INTO Loved_Tracks (userid, track, artist, time) VALUES(?,?,?,?)';
$params = array((int) $userid, $this->name, $this->artist_name, time());
try {
$adodb->Execute('INSERT INTO Loved_Tracks VALUES ('
. $userid . ', '
. $adodb->qstr($this->name) . ', '
. $adodb->qstr($this->artist_name) . ', '
. time() . ')');
} catch (Exception $e) {}
$adodb->Execute($query, $params);
$res = $adodb->Affected_Rows();
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
return False;
}
return (bool) $res;
}
/**
* Unlove a track
*
* @param int $userid The user unloving this track
* @param int $userid The user unloving this track.
* @return bool True on success, False on fail.
*/
function unlove($userid) {
global $adodb;
$query = 'DELETE FROM Loved_Tracks WHERE userid=? AND track=? AND artist=?';
$params = array((int) $userid, $this->name, $this->artist_name);
try {
$adodb->Execute('DELETE FROM Loved_Tracks WHERE userid=' . $userid
. ' AND track=' . $adodb->qstr($this->name)
. ' AND artist=' . $adodb->qstr($this->artist_name));
} catch (Exception $e) {}
$adodb->Execute($query, $params);
$res = $adodb->Affected_Rows();
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
return False;
}
return (bool) $res;
}
/**
......@@ -388,12 +405,84 @@ class Track {
function isLoved($userid) {
global $adodb;
$query = 'SELECT * FROM Loved_Tracks WHERE userid=? AND track=? AND artist=?';
$params = array((int) $userid, $this->name, $this->artist_name);
try {
$res = $adodb->GetRow($query, $params);
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
return False;
}
if($res) {
return True;
}
return False;
}
/**
* Ban a track
*
* @param int $userid The user banning this track.
* @return bool True on success, False on fail.
*
*/
function ban($userid) {
global $adodb;
$query = 'INSERT INTO Banned_Tracks (userid, track, artist, time) VALUES(?,?,?,?)';
$params = array((int) $userid, $this->name, $this->artist_name, time());
try {
$res = $adodb->GetRow('SELECT * FROM Loved_Tracks WHERE userid='
. $userid . ' AND track='
. $adodb->qstr($this->name) . ' AND artist='
. $adodb->qstr($this->artist_name));
} catch (Exception $e) {}
$adodb->Execute($query, $params);
$res = $adodb->Affected_Rows();
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
return False;
}
return (bool) $res;
}
/**
* Unban a track
*
* @param int $userid The user unbanning this track.
* @return bool True on success, False on fail.
*/
function unban($userid) {
global $adodb;
$query = 'DELETE FROM Banned_Tracks WHERE userid=? AND track=? AND artist=?';
$params = array((int) $userid, $this->name, $this->artist_name);
try {
$adodb->Execute($query, $params);
$res = $adodb->Affected_Rows();
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
return False;
}
return (bool) $res;
}
/**
* Check if track has been banned by user
*
* @param int $userid The user we are looking for
* @return bool True if track has been banned by user
*/
function isBanned($userid) {
global $adodb;
$query = 'SELECT * FROM Banned_Tracks WHERE userid=? AND track=? AND artist=?';
$params = array((int) $userid, $this->name, $this->artist_name);
try {
$res = $adodb->GetRow($query, $params);
} catch (Exception $e) {
reportError($e->GetMessage(), $e->GetTraceAsString());
return False;
}
if($res) {
return True;
......@@ -401,11 +490,13 @@ class Track {
return False;
}
/*
* Remove a tag from a track
*
* @param string $tag The tag to be removed
* @param int $userid The user removing the tag
* @return bool True on success, False on fail.
*/
function removeTag($tag, $userid) {
global $adodb;
......@@ -414,13 +505,16 @@ class Track {
if(strlen($tag) == 0) {
return;
}
$query = 'DELETE FROM Tags WHERE tag = ? AND lower(artist) = lower(?) AND lower(track) = lower(?) AND userid = ?';
$params = array($tag, $this->artist_name, $this->name, $userid);
$query = 'DELETE FROM Tags WHERE tag=? AND artist=? AND track=? AND userid = ?';
$params = array($tag, $this->artist_name, $this->name, (int) $userid);
try {
$adodb->Execute($query, $params);
$res = $adodb->Affected_Rows();
} catch (Exception $e) {
reportError($e->getMessage(), $e->getTraceAsString());
return False;
}
return (bool) $res;
}
}
This diff is collapsed.
......@@ -34,7 +34,18 @@ try {
die("Unable to connect to database.");
}
// To keep compatibility with existing code
function reportError($title, $msg) {
/**
* Write error to Error database table
*
* @param string msg Message
* @param string data Data
* @return null
*/
function reportError($msg, $data) {
global $adodb;
$adodb->Execute('INSERT INTO Error(msg, data, time) VALUES('
. $adodb->qstr($msg) . ', '
. $adodb->qstr($data) . ', '
. time() . ')');
}
......@@ -42,6 +42,11 @@ function radio_title_from_url($url) {
$artist = $regs[2];
return $host_name . ' ' . ucwords($artist) . ' Similar Artist Radio';
}
if (preg_match('@l(ast|ibre)fm://artist/(.*)/album/(.*)@', $url, $regs)) {
$artist = $regs[2];
$album = $regs[3];
return $host_name . ' ' . ucwords($artist) . ' - ' . ucwords($album) . ' Album Radio';
}
if (preg_match('@l(ast|ibre)fm://artist/(.*)@', $url, $regs)) {
$artist = $regs[2];
return $host_name . ' ' . ucwords($artist) . ' Artist Radio';
......@@ -111,6 +116,10 @@ function make_playlist($session, $old_format = false, $format='xml') {
}
$similarArtists = $artist->getSimilar(20);
$res = get_artist_selection($similarArtists, $artist);
} else if (preg_match('@l(ast|ibre)fm://artist/(.*)/album/(.*)@', $url, $regs)) {
$query = 'SELECT name, artist_name, album_name, duration, streamurl FROM Track WHERE streamable=1 AND lower(artist_name)=lower(?) AND lower(album_name)=lower(?)';
$params = array($regs[2], $regs[3]);
$res = $adodb->CacheGetAll(7200, $query, $params);
} else if (preg_match('@l(ast|ibre)fm://artist/(.*)@', $url, $regs)) {
$artist = $regs[2];
$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) . ')');
......
This diff is collapsed.
......@@ -59,6 +59,7 @@ $smarty = new Smarty();
$smarty->template_dir = array($install_path . '/themes/'. $theme . '/templates/', $install_path . '/themes/gnufm/templates/');
$smarty->compile_dir = $install_path. '/themes/' . $theme . '/templates_c/';
$smarty->cache_dir = $install_path. '/cache/';
$smarty->config_dir = array($install_path . '/themes/' . $theme . '/config/', $install_path . '/themes/gnufm/config/');
$current_lang = preg_replace('/.UTF-8/', '', $current_lang);
$smarty->assign('lang_selector_array', array(($current_lang) => 1));
......
# CSS classes
librarytable = 'librarytable'
......@@ -100,6 +100,110 @@ body{ font:12px/18px sans-serif; }
::-moz-selection { background: #f16529; color: #fff; text-shadow: none; }
::selection { background: #f16529; color: #fff; text-shadow: none; }
#submenu li {
display:inline-block;
}
#submenu li.active {
font-weight:bold;
}
.pagination li {
display:inline-block;
}
.pagination li.disabled {
visibility:hidden;
}
.inline {
display:inline;
}
.librarytable {
width:100%;
/*border:1px solid black;*/
}
.librarytable td {
/*border-left:1px dotted black;*/
padding:2px;
}
.librarytable .icon {
width: 16px;
}
.librarytable .image {
width:24px;
}
.librarytable .title {
text-align:left;
}
.librarytable .count {
width:50px;
text-align:center;
}
.librarytable .time {
white-space:nowrap;
text-align:center;
width:100px;
}
.librarytable .icon-heart {
display:inline-block;
height:14px;
width:14px;
background-image:url("/themes/gnufm/img/love-small.png");
background-repeat:no-repeat;
background-position:center;
}
.librarytable .icon-music {
display:inline-block;
height:14px;
width:14px;
/*background-image:url("/themes/gnufm/img/love-small.png");*/
background-color:green;
background-repeat:no-repeat;
background-position:center;
}
.librarytable .icon-tag {
display:inline-block;
height:14px;
width:14px;
/*background-image:url("/themes/gnufm/img/love-small.png");*/
background-color:blue;
background-repeat:no-repeat;
background-position:center;
}
.librarytable .image img {
width:24px;
height:24px;
}
.librarytable tr .buttons form {
visibility:hidden;
width:50px;
}
.librarytable tbody tr:hover td {
background-color:#ddd;
}
.librarytable tbody tr:hover .buttons form {
visibility:visible;
}
.librarytable .buttons form {
margin:0px;
}
<