summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorKarl 'vollmerk' Vollmer <vollmer@ampache.org>2006-12-17 08:41:21 +0000
committerKarl 'vollmerk' Vollmer <vollmer@ampache.org>2006-12-17 08:41:21 +0000
commit7c661ba685287efd21512f9f0203641200bffed2 (patch)
treeb99371285778815b3faabbaae6b9e8d7bf3e19e1 /lib
parentce1a8672d4b2d78b8301527311a410af893c4943 (diff)
downloadampache-7c661ba685287efd21512f9f0203641200bffed2.tar.gz
ampache-7c661ba685287efd21512f9f0203641200bffed2.tar.bz2
ampache-7c661ba685287efd21512f9f0203641200bffed2.zip
* Added Recommendations based on matching ratings between users
* Integrated LastFM plugin (defaults to disabled still) * Fixed a view issue with the Admin Localplay Level preference * removed some old MPD pages that were not being usedwq
Diffstat (limited to 'lib')
-rw-r--r--lib/class/audioscrobbler.class.php228
-rw-r--r--lib/class/user.class.php140
-rw-r--r--lib/init.php3
-rw-r--r--lib/preferences.php24
4 files changed, 377 insertions, 18 deletions
diff --git a/lib/class/audioscrobbler.class.php b/lib/class/audioscrobbler.class.php
new file mode 100644
index 00000000..68162c46
--- /dev/null
+++ b/lib/class/audioscrobbler.class.php
@@ -0,0 +1,228 @@
+<?php
+/*
+
+ Copyright (c) 2001 - 2006 Ampache.org
+ All rights reserved.
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License v2
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+*/
+
+class scrobbler {
+
+ var $error_msg;
+ var $username;
+ var $password;
+ var $challenge;
+ var $submit_host;
+ var $submit_port;
+ var $submit_url;
+ var $queued_tracks;
+
+ /**
+ * Constructor
+ * This is the constructer it takes a username and password
+ */
+ function scrobbler($username, $password) {
+
+ $this->error_msg = '';
+ $this->username = trim($username);
+ $this->password = trim($password);
+ $this->challenge = '';
+ $this->queued_tracks = array();
+
+ } // scrobbler
+
+ /**
+ * get_error_msg
+ */
+ function get_error_msg() {
+
+ return $this->error_msg;
+
+ } // get_error_msg
+
+ /**
+ * get_queue_count
+ function get_queue_count() {
+
+ return count($this->queued_tracks);
+
+ } // get_queue_count
+
+ /**
+ * handshake
+ * This does a handshake with the audioscrobber server it doesn't pass the password, but
+ * it does pass the username and has a 10 second timeout
+ */
+ function handshake() {
+
+ $as_socket = @fsockopen('post.audioscrobbler.com', 80, $errno, $errstr, 10);
+ if(!$as_socket) {
+ $this->error_msg = $errstr;
+ return false;
+ }
+
+ $username = rawurlencode($this->username);
+
+ $get_string = "GET /?hs=true&p=1.1&c=m3a&v=0.1&u=$username HTTP/1.1\r\n";
+
+ fwrite($as_socket, $get_string);
+ fwrite($as_socket, "Host: post.audioscrobbler.com\r\n");
+ fwrite($as_socket, "Accept: */*\r\n\r\n");
+
+ $buffer = '';
+ while(!feof($as_socket)) {
+ $buffer .= fread($as_socket, 8192);
+ }
+ fclose($as_socket);
+ $split_response = preg_split("/\r\n\r\n/", $buffer);
+ if(!isset($split_response[1])) {
+ $this->error_msg = 'Did not receive a valid response';
+ return false;
+ }
+ $response = explode("\n", $split_response[1]);
+ if(substr($response[0], 0, 6) == 'FAILED') {
+ $this->error_msg = substr($response[0], 7);
+ return false;
+ }
+ if(substr($response[0], 0, 7) == 'BADUSER') {
+ $this->error_msg = 'Invalid Username';
+ return false;
+ }
+ if(substr($response[0], 0, 6) == 'UPDATE') {
+ $this->error_msg = 'You need to update your client: '.substr($response[0], 7);
+ return false;
+ }
+
+ if(preg_match('/http:\/\/(.*):(\d+)(.*)/', $response[3], $matches)) {
+ $this->submit_host = $matches[1];
+ $this->submit_port = $matches[2];
+ $this->submit_url = $matches[3];
+ } else {
+ $this->error_msg = 'Invalid POST URL returned, unable to continue';
+ return false;
+ }
+
+ $this->challenge = $response[2];
+ return true;
+
+ } // handshake
+
+ /**
+ * queue_track
+ * This queues the LastFM track by storing it in this object, it doesn't actually
+ * submit the track or talk to LastFM in anyway, kind of useless for our uses but its
+ * here, and that's how it is.
+ */
+ function queue_track($artist, $album, $track, $timestamp, $length) {
+ $date = gmdate('Y-m-d H:i:s', $timestamp);
+ $mydate = date('Y-m-d H:i:s T', $timestamp);
+
+ if($length < 30) {
+ debug_event('lastfm',"Not queuing track, too short",'5');
+ return false;
+ }
+
+ $newtrack = array();
+ $newtrack['artist'] = $artist;
+ $newtrack['album'] = $album;
+ $newtrack['track'] = $track;
+ $newtrack['length'] = $length;
+ $newtrack['time'] = $date;
+
+ $this->queued_tracks[$timestamp] = $newtrack;
+ return true;
+
+ } // queue_track
+ /**
+ * submit_tracks
+ * This actually talks to LastFM submiting the tracks that are queued up. It
+ * passed the md5'd password combinted with the challenge, which is then md5'd
+ */
+ function submit_tracks() {
+
+ // Check and make sure that we've got some queued tracks
+ if(!count($this->queued_tracks)) {
+ $this->error_msg = "No tracks to submit";
+ return false;
+ }
+
+ //sort array by timestamp
+ ksort($this->queued_tracks);
+
+ // build the query string
+ $query_str = 'u='.rawurlencode($this->username).'&s='.rawurlencode(md5($this->password.$this->challenge)).'&';
+
+ $i = 0;
+
+ foreach($this->queued_tracks as $track) {
+ $query_str .= "a[$i]=".rawurlencode($track['artist'])."&t[$i]=".rawurlencode($track['track'])."&b[$i]=".rawurlencode($track['album'])."&";
+ $query_str .= "m[$i]=&l[$i]=".rawurlencode($track['length'])."&i[$i]=".rawurlencode($track['time'])."&";
+ $i++;
+ }
+
+ $as_socket = @fsockopen($this->submit_host, $this->submit_port, $errno, $errstr, 10);
+
+ if(!$as_socket) {
+ $this->error_msg = $errstr;
+ return false;
+ }
+
+ $action = "POST ".$this->submit_url." HTTP/1.0\r\n";
+ fwrite($as_socket, $action);
+ fwrite($as_socket, "Host: ".$this->submit_host."\r\n");
+ fwrite($as_socket, "Accept: */*\r\n");
+ fwrite($as_socket, "Content-type: application/x-www-form-urlencoded\r\n");
+ fwrite($as_socket, "Content-length: ".strlen($query_str)."\r\n\r\n");
+
+ fwrite($as_socket, $query_str."\r\n\r\n");
+
+ $buffer = '';
+ while(!feof($as_socket)) {
+ $buffer .= fread($as_socket, 8192);
+ }
+ fclose($as_socket);
+
+ $split_response = preg_split("/\r\n\r\n/", $buffer);
+ if(!isset($split_response[1])) {
+ $this->error_msg = 'Did not receive a valid response';
+ return false;
+ }
+ $response = explode("\n", $split_response[1]);
+ if(!isset($response[0])) {
+ $this->error_msg = 'Unknown error submitting tracks'.
+ "\nDebug output:\n".$buffer;
+ return false;
+ }
+ if(substr($response[0], 0, 6) == 'FAILED') {
+ $this->error_msg = $response[0];
+ return false;
+ }
+ if(substr($response[0], 0, 7) == 'BADAUTH') {
+ $this->error_msg = 'Invalid username/password';
+ return false;
+ }
+ if(substr($response[0], 0, 2) != 'OK') {
+ $this->error_msg = 'Unknown error submitting tracks'.
+ "\nDebug output:\n".$buffer;
+ return false;
+ }
+
+ return true;
+
+ } // submit_tracks
+
+} // end audioscrobbler class
+?>
diff --git a/lib/class/user.class.php b/lib/class/user.class.php
index fefca81f..f662e7df 100644
--- a/lib/class/user.class.php
+++ b/lib/class/user.class.php
@@ -156,11 +156,10 @@ class User {
}
} // get_preferences
- /*!
- @function get_favorites
- @discussion returns an array of your $type
- favorites
- */
+ /**
+ * get_favorites
+ * returns an array of your $type favorites
+ */
function get_favorites($type) {
$web_path = conf('web_path');
@@ -209,6 +208,68 @@ class User {
} // get_favorites
+ /**
+ * get_recommendations
+ * This returns recommended objects of $type. The recommendations
+ * are based on voodoo economics,the phase of the moon and my current BAL.
+ */
+ function get_recommendations($type) {
+
+ /* First pull all of your ratings of this type */
+ $sql = "SELECT object_id,user_rating FROM ratings " .
+ "WHERE object_type='" . sql_escape($type) . "' AND user='" . sql_escape($this->id) . "'";
+ $db_results = mysql_query($sql,dbh());
+
+ while ($r = mysql_fetch_assoc($db_results)) {
+ /* Store the fact that you rated this */
+ $key = $r['object_id'];
+ $ratings[$key] = true;
+
+ /* Build a key'd array of users with this same rating */
+ $sql = "SELECT user FROM ratings WHERE object_type='" . sql_escape($type) . "' " .
+ "AND user !='" . sql_escape($this->id) . "' AND object_id='" . sql_escape($r['object_id']) . "' " .
+ "AND user_rating ='" . sql_escape($r['user_rating']) . "'";
+ $user_results = mysql_query($sql,dbh());
+
+ while ($user_info = mysql_fetch_assoc($user_results)) {
+ $key = $user_info['user'];
+ $users[$key]++;
+ }
+
+ } // end while
+
+ /* now we've got your ratings, and all users and the # of ratings that match your ratings
+ * sort the users[$key] array by value and then find things they've rated high (4+) that you
+ * haven't rated
+ */
+ asort($users);
+ $recommendations = array();
+
+ foreach ($users as $user_id=>$score) {
+
+ /* Find everything they've rated at 4+ */
+ $sql = "SELECT object_id,user_rating FROM ratings " .
+ "WHERE user='" . sql_escape($user_id) . "' AND user_rating >='4' AND object_type = '" . sql_escape($type) . "' ORDER BY user_rating DESC";
+ $db_results = mysql_query($sql,dbh());
+
+ while ($r = mysql_fetch_assoc($db_results)) {
+ $key = $r['object_id'];
+ if (isset($ratings[$key])) { continue; }
+
+ /* Let's only get 5 total for now */
+ if (count($recommendations) > 5) { return $recommendations; }
+
+ $recommendations[$key] = $r['user_rating'];
+
+ } // end while
+
+
+ } // end foreach users
+
+ return $recommendations;
+
+ } // get_recommendations
+
/*!
@function is_logged_in
@discussion checks to see if $this user is logged in
@@ -433,7 +494,7 @@ class User {
$song_info = new Song($song_id);
//FIXME:: User uid reference
$user = $this->uid;
-
+
if (!$song_info->file) { return false; }
$stats = new Stats();
@@ -442,6 +503,38 @@ class User {
$stats->insert('artist',$song_info->artist,$user);
$stats->insert('genre',$song_info->genre,$user);
+ /**
+ * Record this play to LastFM
+ * because it lags like the dickens try twice on everything
+ */
+ if (!empty($this->prefs['lastfm_user']) AND !empty($this->prefs['lastfm_pass'])) {
+ $song_info->format_song();
+ $lastfm = new scrobbler($this->prefs['lastfm_user'],$this->prefs['lastfm_pass']);
+ /* Attempt handshake */
+ $handshake = $lastfm->handshake();
+
+ /* We failed, try again */
+ if (!$handshake) { sleep(1); $handshake = $lastfm->handshake(); }
+
+ if ($handshake) {
+ if (!$lastfm->queue_track($song_info->f_artist_full,$song_info->f_album_full,$song_info->title,time(),$song_info->time)) {
+ debug_event('LastFM','Error: Queue Failed: ' . $lastfm->error_msg,'3');
+ }
+
+ $submit = $lastfm->submit_tracks();
+
+ /* Try again if it fails */
+ if (!$submit) { sleep(1); $submit = $lastfm->submit_tracks(); }
+
+ if (!$submit) {
+ debug_event('LastFM','Error Submit Failed: ' . $lastfm->error_msg,'3');
+ }
+ } // if handshake
+ else {
+ debug_event('LastFM','Error: Handshake failed with LastFM: ' . $lastfm->error_msg,'3');
+ }
+ } // record to LastFM
+
} // update_stats
/**
@@ -583,6 +676,41 @@ class User {
} // format_favorites
/**
+ * format_recommendations
+ * This takes an array of [object_id] = ratings
+ * and displays them in a semi-pretty format
+ */
+ function format_recommendations($items,$type) {
+
+ foreach ($items as $object_id=>$rating) {
+
+ switch ($type) {
+ case 'artist':
+ $object = new Artist($object_id);
+ $object->format_artist();
+ $name = $object->f_name;
+ break;
+ case 'album':
+ $object = new Album($object_id);
+ $object->format_album();
+ $name = $object->f_name;
+ break;
+ case 'song':
+ $object = new Song($object_id);
+ $object->format_song();
+ $name = $object->f_title;
+ break;
+ } // end switch on type
+ $results[] = "<li>$name -- $rating<br />\n</li>";
+
+ } // end foreach items
+
+
+ return $results;
+
+ } // format_recommendations
+
+ /**
* fix_preferences
* this makes sure that the specified user
* has all the correct preferences. This function
diff --git a/lib/init.php b/lib/init.php
index d645f0ae..abf43b39 100644
--- a/lib/init.php
+++ b/lib/init.php
@@ -172,6 +172,7 @@ if (conf('ratings')) {
require_once(conf('prefix') . '/lib/rating.lib.php');
}
+
// Classes
require_once(conf('prefix') . '/lib/class/localplay.class.php');
require_once(conf('prefix') . '/lib/class/plugin.class.php');
@@ -190,6 +191,8 @@ require_once(conf('prefix') . '/lib/class/access.class.php');
require_once(conf('prefix') . '/lib/class/error.class.php');
require_once(conf('prefix') . '/lib/class/genre.class.php');
require_once(conf('prefix') . '/lib/class/flag.class.php');
+require_once(conf('prefix') . '/lib/class/audioscrobbler.class.php');
+
/* Set a new Error Handler */
$old_error_handler = set_error_handler("ampache_error_handler");
diff --git a/lib/preferences.php b/lib/preferences.php
index 6d1d5a5a..62781048 100644
--- a/lib/preferences.php
+++ b/lib/preferences.php
@@ -123,10 +123,16 @@ function update_preferences($pref_id=0) {
case 'sample_rate':
$value = validate_bitrate($value);
break;
+ /* MD5 the LastFM so it's not plainTXT */
+ case 'lastfm_pass':
+ /* If it's our default blanking thing then don't use it */
+ if ($value == '******') { unset($_REQUEST[$name]); break; }
+ $value = md5($value);
+ break;
default:
break;
}
-
+
/* Run the update for this preference only if it's set */
if (isset($_REQUEST[$name])) {
update_preference($pref_id,$name,$id,$value);
@@ -221,10 +227,6 @@ function create_preference_input($name,$value) {
elseif ($value == '0') {
echo "Disabled";
}
- elseif ($name == 'upload_dir' || $name == 'quarantine_dir') {
- /* Show Nothing */
- echo "&nbsp;";
- }
else {
echo $value;
}
@@ -318,8 +320,8 @@ function create_preference_input($name,$value) {
echo "</select>\n";
break;
case 'localplay_level':
- if ($GLOBALS['user']->prefs['localplay_level'] == '2') { $is_full = 'selected="selected"'; }
- elseif ($GLOBALS['user']->prefs['localplay_level'] == '1') { $is_global = 'selected="selected"'; }
+ if ($value == '2') { $is_full = 'selected="selected"'; }
+ elseif ($value == '1') { $is_global = 'selected="selected"'; }
echo "<select name=\"$name\">\n";
echo "<option value=\"0\">" . _('Disabled') . "</option>\n";
echo "<option value=\"1\" $is_global>" . _('Global') . "</option>\n";
@@ -336,11 +338,9 @@ function create_preference_input($name,$value) {
} // foreach themes
echo "</select>\n";
break;
- case 'quarantine_dir':
- case 'upload_dir':
- if (!$GLOBALS['user']->has_access(100)) {
- break;
- }
+ case 'lastfm_pass':
+ echo "<input type=\"password\" size=\"16\" name=\"$name\" value=\"******\" />";
+ break;
default:
echo "<input type=\"text\" size=\"$len\" name=\"$name\" value=\"$value\" />";
break;