From a9da6a6fa22325ba0dfecd4d46ae23305473796f Mon Sep 17 00:00:00 2001 From: Paul Arthur Date: Sat, 2 Apr 2011 00:22:55 -0400 Subject: Reworked search Still has tentacles and should have been integrated into the existing API/Browse implementation better, but it's functional. --- batch.php | 29 +- browse.php | 6 + lib/class/api.class.php | 8 +- lib/class/browse.class.php | 5 + lib/class/playlist.class.php | 54 +- lib/class/playlist_object.abstract.php | 72 ++ lib/class/query.class.php | 40 + lib/class/random.class.php | 197 ++--- lib/class/search.class.php | 1033 +++++++++++++++++++++++ lib/class/update.class.php | 22 + lib/init.php | 2 +- lib/javascript/search-data.php | 49 ++ lib/javascript/search.js | 168 ++++ lib/search.php | 294 ------- random.php | 7 +- search.php | 20 +- server/browse.ajax.php | 6 + smartplaylist.php | 103 +++ templates/show_edit_smartplaylist_row.inc.php | 45 + templates/show_edit_smartplaylist_title.inc.php | 43 + templates/show_random.inc.php | 118 ++- templates/show_random_rules.inc.php | 59 -- templates/show_rules.inc.php | 67 ++ templates/show_search.inc.php | 145 +--- templates/show_search_bar.inc.php | 17 +- templates/show_search_options.inc.php | 2 +- templates/show_smartplaylist.inc.php | 67 ++ templates/show_smartplaylist_row.inc.php | 39 + templates/show_smartplaylist_title.inc.php | 24 + templates/show_smartplaylists.inc.php | 63 ++ templates/sidebar_home.inc.php | 3 +- 31 files changed, 2095 insertions(+), 712 deletions(-) create mode 100644 lib/class/playlist_object.abstract.php create mode 100644 lib/class/search.class.php create mode 100644 lib/javascript/search-data.php create mode 100644 lib/javascript/search.js delete mode 100644 lib/search.php create mode 100644 smartplaylist.php create mode 100644 templates/show_edit_smartplaylist_row.inc.php create mode 100644 templates/show_edit_smartplaylist_title.inc.php delete mode 100644 templates/show_random_rules.inc.php create mode 100644 templates/show_rules.inc.php create mode 100644 templates/show_smartplaylist.inc.php create mode 100644 templates/show_smartplaylist_row.inc.php create mode 100644 templates/show_smartplaylist_title.inc.php create mode 100644 templates/show_smartplaylists.inc.php diff --git a/batch.php b/batch.php index 427a726e..c57961eb 100644 --- a/batch.php +++ b/batch.php @@ -53,6 +53,18 @@ switch ($_REQUEST['action']) { $media_ids = $playlist->get_songs(); $name = $playlist->name; break; + case 'smartplaylist': + $search = new Search('song', $_REQUEST['id']); + $sql = $search->to_sql(); + $sql = $sql['base'] . ' ' . $sql['table_sql'] . ' WHERE ' . + $sql['where_sql']; + $db_results = Dba::read($sql); + $media_ids = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $media_ids[] = $row['id']; + } + $name = $search->name; + break; case 'album': $album = new Album($_REQUEST['id']); $media_ids = $album->get_songs(); @@ -66,7 +78,22 @@ switch ($_REQUEST['action']) { case 'browse': $id = scrub_in($_REQUEST['browse_id']); $browse = new Browse($id); - $media_ids = $browse->get_saved(); + $browse_media_ids = $browse->get_saved(); + $media_ids = array(); + foreach ($browse_media_ids as $media_id) { + switch ($_REQUEST['type']) { + case 'album': + $album = new Album($media_id); + $media_ids = array_merge($media_ids, $album->get_songs()); + break; + case 'song': + $media_ids[] = $media_id; + break; + case 'video': + $media_ids[] = array('Video', $media_id); + break; + } // switch on type + } // foreach media_id $name = 'Batch-' . date("dmY",time()); default: // Rien a faire diff --git a/browse.php b/browse.php index 91f6e30a..62f4cdfd 100644 --- a/browse.php +++ b/browse.php @@ -55,6 +55,7 @@ switch ($_REQUEST['action']) { case 'album': case 'artist': case 'playlist': + case 'smartplaylist': case 'live_stream': case 'video': case 'song': @@ -108,6 +109,11 @@ switch($_REQUEST['action']) { $browse->set_filter('playlist_type','1'); $browse->show_objects(); break; + case 'smartplaylist': + $browse->set_sort('type', 'ASC'); + $browse->set_filter('playlist_type','1'); + $browse->show_objects(); + break; case 'video': $browse->set_sort('title','ASC'); $browse->show_objects(); diff --git a/lib/class/api.class.php b/lib/class/api.class.php index 508d38c2..9bd659a4 100644 --- a/lib/class/api.class.php +++ b/lib/class/api.class.php @@ -594,11 +594,13 @@ class Api { /** * search_songs - * This returns the songs and returns... songs + * This searches the songs and returns... songs */ public static function search_songs($input) { + $array['rule_1'] = 'anywhere'; + $array['rule_1_input'] = $input['filter']; + $array['rule_1_operator'] = 0; - $array['s_all'] = $input['filter']; ob_end_clean(); xmlData::set_offset($input['offset']); @@ -608,7 +610,7 @@ class Api { //Run search references these variables, ooh the huge manatee unset($input['limit'],$input['offset']); - $results = run_search($array); + $results = Search::run($array); echo xmlData::songs($results); diff --git a/lib/class/browse.class.php b/lib/class/browse.class.php index 56eda8b7..c997c478 100644 --- a/lib/class/browse.class.php +++ b/lib/class/browse.class.php @@ -188,6 +188,11 @@ class Browse extends Query { require_once Config::get('prefix') . '/templates/show_localplay_playlist.inc.php'; show_box_bottom(); break; + case 'smartplaylist': + show_box_top(_('Smart Playlists') . $match, $class); + require_once Config::get('prefix') . '/templates/show_smartplaylists.inc.php'; + show_box_bottom(); + break; case 'catalog': show_box_top(_('Catalogs'), $class); require_once Config::get('prefix') . '/templates/show_catalogs.inc.php'; diff --git a/lib/class/playlist.class.php b/lib/class/playlist.class.php index 6624540c..42638820 100644 --- a/lib/class/playlist.class.php +++ b/lib/class/playlist.class.php @@ -46,13 +46,9 @@ * @link http://www.ampache.org/ * @since Class available since Release 1.0 */ -class Playlist extends database_object { +class Playlist extends playlist_object { /* Variables from the database */ - public $id; - public $name; - public $user; - public $type; public $genre; public $date; @@ -94,43 +90,33 @@ class Playlist extends database_object { } // build_cache /** - * format - * This takes the current playlist object and gussies it up a little - * bit so it is presentable to the users + * get_playlists + * Returns a list of playlists accessible by the current user. */ - public function format() { - - $this->f_name = truncate_with_ellipsis($this->name,Config::get('ellipse_threshold_title')); - $this->f_link = '' . $this->f_name . ''; - - $this->f_type = ($this->type == 'private') ? get_user_icon('lock',_('Private')) : ''; + public static function get_playlists() { + $sql = "SELECT `id` from `playlist` WHERE `type`='public' OR " . + "`user`='" . $GLOBALS['user']->id . "' ORDER BY `name`"; + $db_results = Dba::read($sql); - $client = new User($this->user); + $results = array(); - $this->f_user = $client->fullname; + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } - } // format + return $results; + } // get_playlists /** - * has_access - * This function returns true or false if the current user - * has access to this playlist + * format + * This takes the current playlist object and gussies it up a little + * bit so it is presentable to the users */ - public function has_access() { - - if (!Access::check('interface','25')) { - return false; - } - if ($this->user == $GLOBALS['user']->id) { - return true; - } - else { - return Access::check('interface','100'); - } - - return false; + public function format() { + parent::format(); + $this->f_link = '' . $this->f_name . ''; - } // has_access + } // format /** * get_track diff --git a/lib/class/playlist_object.abstract.php b/lib/class/playlist_object.abstract.php new file mode 100644 index 00000000..def7b3f2 --- /dev/null +++ b/lib/class/playlist_object.abstract.php @@ -0,0 +1,72 @@ +f_name = truncate_with_ellipsis($this->name,Config::get('ellipse_threshold_title')); + $this->f_type = ($this->type == 'private') ? get_user_icon('lock',_('Private')) : ''; + + $client = new User($this->user); + + $this->f_user = $client->fullname; + + } // format + + /** + * has_access + * This function returns true or false if the current user + * has access to this playlist + */ + public function has_access() { + + if (!Access::check('interface','25')) { + return false; + } + if ($this->user == $GLOBALS['user']->id) { + return true; + } + else { + return Access::check('interface','100'); + } + + return false; + + } // has_access + + +} // end playlist_object diff --git a/lib/class/query.class.php b/lib/class/query.class.php index 898e8029..11095b80 100644 --- a/lib/class/query.class.php +++ b/lib/class/query.class.php @@ -144,6 +144,10 @@ class Query { 'alpha_match', 'starts_with' ), + 'smartplaylist' => array( + 'alpha_match', + 'starts_with' + ), 'tag' => array( 'tag', 'object_type', @@ -194,6 +198,10 @@ class Query { 'name', 'user' ), + 'smartplaylist' => array( + 'name', + 'user' + ), 'shoutbox' => array( 'date', 'user', @@ -465,6 +473,7 @@ class Query { case 'video': case 'playlist': case 'playlist_song': + case 'smartplaylist': case 'song': case 'flagged': case 'catalog': @@ -735,6 +744,10 @@ class Query { $this->set_select("`playlist`.`id`"); $sql = "SELECT %%SELECT%% FROM `playlist` "; break; + case 'smartplaylist': + self::set_select('`search`.`id`'); + $sql = "SELECT %%SELECT%% FROM `search` "; + break; case 'flagged': $this->set_select("`flagged`.`id`"); $sql = "SELECT %%SELECT%% FROM `flagged` "; @@ -1127,6 +1140,20 @@ class Query { break; } // end filter break; + case 'smartplaylist': + switch ($filter) { + case 'alpha_match': + $filter_sql = " `search`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'starts_with': + $filter_sql = " `search`.`name` LIKE '" . Dba::escape($value) . "%' AND "; + break; + case 'playlist_type': + $user_id = intval($GLOBALS['user']->id); + $filter_sql = " (`search`.`type` = 'public' OR `search`.`user`='$user_id') AND "; + break; + } // end switch on $filter + break; case 'tag': switch ($filter) { case 'alpha_match': @@ -1251,6 +1278,19 @@ class Query { break; } // end switch break; + case 'smartplaylist': + switch ($field) { + case 'type': + $sql = "`search`.`type`"; + break; + case 'name': + $sql = "`search`.`name`"; + break; + case 'user': + $sql = "`search`.`user`"; + break; + } // end switch on $field + break; case 'live_stream': switch ($field) { case 'name': diff --git a/lib/class/random.class.php b/lib/class/random.class.php index 575cf8fb..5472bb80 100644 --- a/lib/class/random.class.php +++ b/lib/class/random.class.php @@ -254,120 +254,60 @@ class Random implements media { * This processes the results of a post from a form and returns an * array of song items that were returned from said randomness */ - public static function advanced($data) { + public static function advanced($type, $data) { /* Figure out our object limit */ $limit = intval($data['random']); // Generate our matchlist - if ($data['catalog'] != '-1') { - $matchlist['catalog'] = $data['catalog']; - } - if ($data['genre'][0] != '-1') { - $matchlist['genre'] = $data['genre']; - } - /* If they've passed -1 as limit then don't get everything */ + /* If they've passed -1 as limit then get everything */ if ($data['random'] == "-1") { unset($data['random']); } else { $limit_sql = "LIMIT " . Dba::escape($limit); } - $where = "1=1 "; - if (is_array($matchlist)) { - foreach ($matchlist as $type => $value) { - if (is_array($value)) { - foreach ($value as $v) { - if (!strlen($v)) { continue; } - $v = Dba::escape($v); - if ($v != $value[0]) { $where .= " OR $type='$v' "; } - else { $where .= " AND ( $type='$v'"; } - } - $where .= ")"; - } - elseif (strlen($value)) { - $value = Dba::escape($value); - $where .= " AND $type='$value' "; - } - } // end foreach - } // end if matchlist - - switch ($data['random_type']) { - case 'full_album': - $query = "SELECT `album`.`id` FROM `song` INNER JOIN `album` ON `song`.`album`=`album`.`id` " . - "WHERE $where GROUP BY `song`.`album` ORDER BY RAND() $limit_sql"; - $db_results = Dba::read($query); - while ($row = Dba::fetch_assoc($db_results)) { - $albums_where .= " OR `song`.`album`=" . $row['id']; - } - $albums_where = ltrim($albums_where," OR"); - $sql = "SELECT `song`.`id`,`song`.`size`,`song`.`time` FROM `song` WHERE $albums_where ORDER BY `song`.`album`,`song`.`track` ASC"; - break; - case 'full_artist': - $query = "SELECT `artist`.`id` FROM `song` INNER JOIN `artist` ON `song`.`artist`=`artist`.`id` " . - "WHERE $where GROUP BY `song`.`artist` ORDER BY RAND() $limit_sql"; - $db_results = Dba::read($query); - while ($row = Dba::fetch_row($db_results)) { - $artists_where .= " OR song.artist=" . $row[0]; - } - $artists_where = ltrim($artists_where," OR"); - $sql = "SELECT song.id,song.size,song.time FROM song WHERE $artists_where ORDER BY RAND()"; - break; - case 'unplayed': - $uid = Dba::escape($GLOBALS['user']->id); - $sql = "SELECT object_id,COUNT(`id`) AS `total` FROM `object_count` WHERE `user`='$uid' GROUP BY `object_id`"; - $db_results = Dba::read($sql); - - $in_sql = "`id` IN ("; + $search_data = Search::clean_request($data); - while ($row = Dba::fetch_assoc($db_results)) { - $row['object_id'] = Dba::escape($row['object_id']); - $in_sql .= "'" . $row['object_id'] . "',"; - } + $search_info = false; - $in_sql = rtrim($in_sql,',') . ')'; + if (count($search_data) > 1) { + $search = new Search($type); + $search->parse_rules($search_data); + $search_info = $search->to_sql(); + } - $sql = "SELECT song.id,song.size,song.time FROM song " . - "WHERE ($where) AND $in_sql ORDER BY RAND() $limit_sql"; - break; - case 'high_rating': - $sql = "SELECT `rating`.`object_id`,`rating`.`rating` FROM `rating` " . - "WHERE `rating`.`object_type`='song' ORDER BY `rating` DESC"; - $db_results = Dba::read($sql); - - // Get all of the ratings for songs - while ($row = Dba::fetch_assoc($db_results)) { - $results[$row['object_id']][] = $row['rating']; + switch ($type) { + case 'song': + $sql = "SELECT `song`.`id`, `size`, `time` " . + "FROM `song` "; + if ($search_info) { + $sql .= $search_info['table_sql']; + $sql .= ' WHERE ' . $search_info['where_sql']; } - // Calculate the averages - foreach ($results as $key=>$rating_array) { - $average = intval(array_sum($rating_array) / count($rating_array)); - // We have to do this because array_slice doesn't maintain indexes - $new_key = $average . $key; - $ratings[$new_key] = $key; + break; + case 'album': + $sql = "SELECT `album`.`id`, SUM(`song`.`size`) AS `size`, SUM(`song`.`time`) AS `time` FROM `album` "; + if (! $search_info || ! $search_info['join']['song']) { + $sql .= "LEFT JOIN `song` ON `song`.`album`=`album`.`id` "; } - - // Sort it by the value and slice at $limit * 2 so we have a little bit of randomness - krsort($ratings); - $ratings = array_slice($ratings,0,$limit*2); - - $in_sql = "`song`.`id` IN ("; - - // Build the IN query, cause if you're OUT it ain't cool - foreach ($ratings as $song_id) { - $key = Dba::escape($song_id); - $in_sql .= "'$key',"; + if ($search_info) { + $sql .= $search_info['table_sql']; + $sql .= ' WHERE ' . $search_info['where_sql']; } - - $in_sql = rtrim($in_sql,',') . ')'; - - // Apply true limit and order by rand - $sql = "SELECT song.id,song.size,song.time FROM song " . - "WHERE ($where) AND $in_sql ORDER BY RAND() $limit_sql"; + $sql .= ' GROUP BY `album`.`id`'; break; - default: - $sql = "SELECT `id`,`size`,`time` FROM `song` WHERE $where ORDER BY RAND() $limit_sql"; - + case 'artist': + $sql = "SELECT `artist`.`id`, SUM(`song`.`size`) AS `size`, SUM(`song`.`time`) AS `time` FROM `artist` "; + if (! $search_info || ! $search_info['join']['song']) { + $sql .= "LEFT JOIN `song` ON `song`.`artist`=`artist`.`id` "; + } + if ($search_info) { + $sql .= $search_info['table_sql']; + $sql .= ' WHERE ' . $search_info['where_sql']; + } + $sql .= ' GROUP BY `artist`.`id`'; break; - } // end switch on type of random play + } + $sql .= " ORDER BY RAND() $limit_sql"; // Run the query generated above so we can while it $db_results = Dba::read($sql); @@ -380,17 +320,23 @@ class Random implements media { // Convert $new_size = ($row['size'] / 1024) / 1024; - // Only fuzzy 10 times - if ($fuzzy_size > 10) { return $results; } + // Only fuzzy 100 times + if ($fuzzy_size > 100) { + break; + } - // Add and check, skip if over don't return incase theres a smaller one commin round - if (($size_total + $new_size) > $data['size_limit']) { $fuzzy_size++; continue; } + // Add and check, skip if over size + if (($size_total + $new_size) > $data['size_limit']) { + $fuzzy_size++; + continue; + } $size_total = $size_total + $new_size; $results[] = $row['id']; // If we are within 4mb of target then jump ship - if (($data['size_limit'] - floor($size_total)) < 4) { return $results; } + if (($data['size_limit'] - floor($size_total)) < 4) { + break; } } // if size_limit // If length really does matter @@ -398,33 +344,60 @@ class Random implements media { // base on min, seconds are for chumps and chumpettes $new_time = floor($row['time'] / 60); - if ($fuzzy_time > 10) { return $results; } + if ($fuzzy_time > 100) { + break;; + } - // If the new one would go voer skip! - if (($time_total + $new_time) > $data['length']) { $fuzzy_time++; continue; } + // If the new one would go over skip! + if (($time_total + $new_time) > $data['length']) { + $fuzzy_time++; + continue; + } $time_total = $time_total + $new_time; $results[] = $row['id']; // If there are less then 2 min of free space return - if (($data['length'] - $time_total) < 2) { return $results; } - + if (($data['length'] - $time_total) < 2) { + return $results; + } } // if length does matter - if (!$data['size_limit'] AND !$data['length']) { + if (!$data['size_limit'] && !$data['length']) { $results[] = $row['id']; } } // end while results - - return $results; - + switch ($type) { + case 'song': + return $results; + break; + case 'album': + $songs = array(); + foreach ($results as $result) { + $album = new Album($result); + $songs = array_merge($songs, $album->get_songs()); + } + return $songs; + break; + case 'artist': + $songs = array(); + foreach ($results as $result) { + $artist = new Artist($result); + $songs = array_merge($songs, $artist->get_songs()); + } + return $songs; + break; + default: + return false; + break; + } } // advanced /** * get_type_name - * This returns a 'purrty' name for the differnt random types + * This returns a 'purrty' name for the different random types */ public static function get_type_name($type) { diff --git a/lib/class/search.class.php b/lib/class/search.class.php new file mode 100644 index 00000000..f2c3cee9 --- /dev/null +++ b/lib/class/search.class.php @@ -0,0 +1,1033 @@ +searchtype = $searchtype; + if ($id) { + $info = $this->get_info($id); + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + $this->rules = unserialize($this->rules); + } + + // Define our basetypes + + $this->basetypes['numeric'][] = array( + 'name' => 'gte', + 'description' => _('is greater than or equal to'), + 'sql' => '>=' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'lte', + 'description' => _('is less than or equal to'), + 'sql' => '<=' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'equal', + 'description' => _('is'), + 'sql' => '<=>' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'ne', + 'description' => _('is not'), + 'sql' => '<>' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'gt', + 'description' => _('is greater than'), + 'sql' => '>' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'lt', + 'description' => _('is less than'), + 'sql' => '<' + ); + + + $this->basetypes['boolean'][] = array( + 'name' => 'true', + 'description' => _('is true') + ); + + $this->basetypes['boolean'][] = array( + 'name' => 'false', + 'description' => _('is false') + ); + + + $this->basetypes['text'][] = array( + 'name' => 'contain', + 'description' => _('contains'), + 'sql' => 'LIKE', + 'preg_match' => array('/^/','/$/'), + 'preg_replace' => array('%', '%') + ); + + $this->basetypes['text'][] = array( + 'name' => 'notcontain', + 'description' => _('does not contain'), + 'sql' => 'NOT LIKE', + 'preg_match' => array('/^/','/$/'), + 'preg_replace' => array('%', '%') + ); + + $this->basetypes['text'][] = array( + 'name' => 'start', + 'description' => _('starts with'), + 'sql' => 'LIKE', + 'preg_match' => '/$/', + 'preg_replace' => '%' + ); + + $this->basetypes['text'][] = array( + 'name' => 'end', + 'description' => _('ends with'), + 'sql' => 'LIKE', + 'preg_match' => '/^/', + 'preg_replace' => '%' + ); + + $this->basetypes['text'][] = array( + 'name' => 'equal', + 'description' => _('is'), + 'sql' => '=' + ); + + $this->basetypes['text'][] = array( + 'name' => 'sounds', + 'description' => _('sounds like'), + 'sql' => 'SOUNDS LIKE' + ); + + $this->basetypes['text'][] = array( + 'name' => 'notsounds', + 'description' => _('does not sound like'), + 'sql' => 'NOT SOUNDS LIKE' + ); + + + $this->basetypes['boolean_numeric'][] = array( + 'name' => 'equal', + 'description' => _('is'), + 'sql' => '<=>' + ); + + $this->basetypes['boolean_numeric'][] = array( + 'name' => 'ne', + 'description' => _('is not'), + 'sql' => '<>' + ); + + + $this->basetypes['boolean_subsearch'][] = array( + 'name' => 'equal', + 'description' => _('is'), + 'sql' => '' + ); + + $this->basetypes['boolean_subsearch'][] = array( + 'name' => 'ne', + 'description' => _('is not'), + 'sql' => 'NOT' + ); + + + $this->basetypes['date'][] = array( + 'name' => 'lt', + 'description' => _('before'), + 'sql' => '>' + ); + + $this->basetypes['date'][] = array( + 'name' => 'gt', + 'description' => _('after'), + 'sql' => '>' + ); + + switch ($searchtype) { + case 'song': + $this->types[] = array( + 'name' => 'anywhere', + 'label' => _('Any searchable text'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'title', + 'label' => _('Title'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'album', + 'label' => _('Album'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'artist', + 'label' => _('Artist'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'comment', + 'label' => _('Comment'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + + $this->types[] = array( + 'name' => 'tag', + 'label' => _('Tag'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'file', + 'label' => _('Filename'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'year', + 'label' => _('Year'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'time', + 'label' => _('Length (in minutes)'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + + if (Config::get('ratings')) { + $this->types[] = array( + 'name' => 'rating', + 'label' => _('Rating'), + 'type' => 'numeric', + 'widget' => array( + 'select', + array( + '1 Star', + '2 Stars', + '3 Stars', + '4 Stars', + '5 Stars' + ) + ) + ); + } + + $this->types[] = array( + 'name' => 'bitrate', + 'label' => _('Bitrate'), + 'type' => 'numeric', + 'widget' => array( + 'select', + array( + '32', + '40', + '48', + '56', + '64', + '80', + '96', + '112', + '128', + '160', + '192', + '224', + '256', + '320' + ) + ) + ); + + $this->types[] = array( + 'name' => 'played', + 'label' => _('Played'), + 'type' => 'boolean', + 'widget' => array('input', 'hidden') + ); + + $this->types[] = array( + 'name' => 'added', + 'label' => _('Added'), + 'type' => 'date', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'updated', + 'label' => _('Updated'), + 'type' => 'date', + 'widget' => array('input', 'text') + ); + + $catalogs = array(); + foreach (Catalog::get_catalogs() as $catid) { + $catalog = new Catalog($catid); + $catalog->format(); + $catalogs[$catid] = $catalog->f_name; + } + $this->types[] = array( + 'name' => 'catalog', + 'label' => _('Catalog'), + 'type' => 'boolean_numeric', + 'widget' => array('select', $catalogs) + ); + + $playlists = array(); + foreach (Playlist::get_playlists() as $playlistid) { + $playlist = new Playlist($playlistid); + $playlist->format(); + $playlists[$playlistid] = $playlist->f_name; + } + $this->types[] = array( + 'name' => 'playlist', + 'label' => _('Playlist'), + 'type' => 'boolean_numeric', + 'widget' => array('select', $playlists) + ); + + $playlists = array(); + foreach (Search::get_searches() as $playlistid) { + // Slightly different from the above so we don't + // instigate a vicious loop. + $playlists[$playlistid] = Search::get_name_byid($playlistid); + } + $this->types[] = array( + 'name' => 'smartplaylist', + 'label' => _('Smart Playlist'), + 'type' => 'boolean_subsearch', + 'widget' => array('select', $playlists) + ); + break; + case 'album': + $this->types[] = array( + 'name' => 'title', + 'label' => _('Title'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'year', + 'label' => _('Year'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + + if (Config::get('ratings')) { + $this->types[] = array( + 'name' => 'rating', + 'label' => _('Rating'), + 'type' => 'numeric', + 'widget' => array( + 'select', + array( + '1 Star', + '2 Stars', + '3 Stars', + '4 Stars', + '5 Stars' + ) + ) + ); + } + + $catalogs = array(); + foreach (Catalog::get_catalogs() as $catid) { + $catalog = new Catalog($catid); + $catalog->format(); + $catalogs[$catid] = $catalog->f_name; + } + $this->types[] = array( + 'name' => 'catalog', + 'label' => _('Catalog'), + 'type' => 'boolean_numeric', + 'widget' => array('select', $catalogs) + ); + + + $this->types[] = array( + 'name' => 'tag', + 'label' => _('Tag'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + case 'video': + $this->types[] = array( + 'name' => 'filename', + 'label' => _('Filename'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + case 'artist': + $this->types[] = array( + 'name' => 'name', + 'label' => _('Name'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + $this->types[] = array( + 'name' => 'tag', + 'label' => _('Tag'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + } // end switch on searchtype + + } // end constructor + + /** + * clean_request + * Sanitizes raw search data + */ + public static function clean_request($data) { + foreach ($data as $key => $value) { + $prefix = substr($key, 0, 4); + $value = trim($value); + + if ($prefix == 'rule' && strlen($value)) { + $request[$key] = Dba::escape($value); + } + } // end foreach $data + + // Figure out if they want an AND based search or an OR based + // search + switch($data['operator']) { + case 'or': + $request['operator'] = 'OR'; + break; + default: + $request['operator'] = 'AND'; + break; + } // end switcn on operator + + return $request; + } // end clean_request + + /** + * get_name_byid + * Returns the name of the saved search corresponding to the given ID + */ + public static function get_name_byid($id) { + $sql = "SELECT `name` FROM `search` WHERE `id`='$id'"; + $db_results = Dba::read($sql); + $r = Dba::fetch_assoc($db_results); + return $r['name']; + } // end get_name_byid + + /** + * get_searches + * Return the IDs of all saved searches accessible by the current user. + */ + public static function get_searches() { + $sql = "SELECT `id` from `search` WHERE `type`='public' OR " . + "`user`='" . $GLOBALS['user']->id . "' ORDER BY `name`"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } // end get_searches + + /** + * run + * This function actually runs the search, and returns an array of the + * results. + */ + public static function run($data) { + $limit = intval($data['limit']); + /* Create an array of the object we need to search on */ + $data = Search::clean_request($data); + + $search = new Search($_REQUEST['type']); + $search->parse_rules($data); + + /* Generate BASE SQL */ + + if ($limit > 0) { + $limit_sql = " LIMIT " . $limit; + } + + $search_info = $search->to_sql(); + $sql = $search_info['base'] . ' ' . $search_info['table_sql'] . + ' WHERE ' . $search_info['where_sql'] . " $limit_sql"; + + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } // run + + /** + * delete + * Does what it says on the tin. + */ + public function delete() { + $id = Dba::escape($this->id); + $sql = "DELETE FROM `search` WHERE `id`='$id'"; + $db_results = Dba::write($sql); + + return true; + } // end delete + + /** + * format + * Gussy up the data + */ + public function format() { + parent::format(); + $this->f_link = '' . $this->f_name . ''; + } // end format + + /** + * get_items + * return an array of the items output by our search (part of the + * playlist interface). + */ + public function get_items() { + $results = array(); + + $sql = $this->to_sql(); + $sql = $sql['base'] . ' ' . $sql['table_sql'] . ' WHERE ' . + $sql['where_sql']; + + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = array( + 'object_id' => $row['id'], + 'type' => $this->searchtype + ); + } + + return $results; + } // end get_items + + /** + * name_to_basetype + * Iterates over our array of types to find out the basetype for + * the passed string. + */ + public function name_to_basetype($name) { + foreach ($this->types as $type) { + if ($type['name'] == $name) { + return $type['type']; + } + } + return false; + } // end name_to_basetype + + /** + * parse_rules + * Takes an array of sanitized search data from the form and generates + * our real array from it. + */ + public function parse_rules($data) { + $this->rules = array(); + foreach ($data as $rule => $value) { + if (preg_match('/^rule_(\d)$/', $rule, $ruleID)) { + $ruleID = $ruleID[1]; + foreach (explode('|', $data['rule_' . $ruleID . '_input']) as $input) { + $this->rules[] = array( + $value, + $this->basetypes[$this->name_to_basetype($value)][$data['rule_' . $ruleID . '_operator']]['name'], + $input + ); + } + } + } + $this->logic_operator = $data['operator']; + } // end parse_rules + + /** + * save + * Save this search to the database for use as a smart playlist + */ + public function save() { + // Make sure we have a unique name + if (! $this->name) { + $this->name = $GLOBALS['user']->username . ' - ' . date("Y-m-d H:i:s",time()); + } + $sql = "SELECT `id` FROM `search` WHERE `name`='$this->name'"; + $db_results = Dba::read($sql); + if (Dba::num_rows($db_results)) { + $this->name .= uniqid('', true); + } + + // clean up variables for insert + $name = Dba::escape($this->name); + $user = Dba::escape($GLOBALS['user']->id); + $type = Dba::escape($this->type); + $rules = serialize($this->rules); + $logic_operator = $this->logic_operator; + + $sql = "INSERT INTO `search` (`name`, `type`, `user`, `rules`, `logic_operator`) VALUES ('$name', '$type', '$user', '$rules', '$logic_operator')"; + $db_results = Dba::write($sql); + $insert_id = Dba::insert_id(); + $this->id = $insert_id; + return $insert_id; + } // end save + + + /** + * to_js + * Outputs the javascript necessary to re-show the current set of + * rules. + */ + public function to_js() { + foreach ($this->rules as $rule) { + $js .= ''; + } + return $js; + } // end to_js + + /** + * to_sql + * Call the appropriate real function + */ + public function to_sql() { + return call_user_func( + array($this, $this->searchtype . "_to_sql")); + } // end to_sql + + /** + * update + * This function updates the saved version with the current settings + */ + public function update() { + if (!$this->id) { + return false; + } + + $name = Dba::escape($this->name); + $user = Dba::escape($GLOBALS['user']->id); + $type = Dba::escape($this->type); + $rules = serialize($this->rules); + $logic_operator = $this->logic_operator; + + $sql = "UPDATE `search` SET `name`='$name', `type`='$type', `rules`='$rules', `logic_operator`='$logic_operator' WHERE `id`='" . Dba::escape($this->id) . "'"; + $db_results = Dba::write($sql); + return $db_results; + } // end update + + /** + * mangle_data + * Private convenience function. Mangles the input according to a set + * of predefined rules so that we don't have to include this logic in + * foo_to_sql. + */ + private function mangle_data($data, $type, $operator) { + if ($operator['preg_match']) { + $data = preg_replace( + $operator['preg_match'], + $operator['preg_replace'], + $data + ); + } + + if ($type == 'numeric') { + return intval($data); + } + + if ($type == 'boolean') { + return make_bool($input); + } + + return $data; + } // end mangle_data + + /** + * album_to_sql + * Handles the generation of the SQL for album searches. + */ + private function album_to_sql() { + $sql_logic_operator = $this->logic_operator; + + $where = array(); + $table = array(); + $join = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + foreach ($this->basetypes[$type] as $operator) { + if ($operator['name'] == $rule[1]) { + break; + } + } + $input = $this->mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'title': + $where[] = "`album`.`name` $sql_match_operator '$input'"; + break; + case 'year': + $where[] = "`album`.`year` $sql_match_operator '$input'"; + break; + case 'rating': + $where[] = " `realrating`.`rating` $sql_match_operator '$input'"; + $join['rating'] = true; + break; + case 'catalog': + $where[] = "`song`.`catalog` $sql_match_operator '$input'"; + $join['song'] = true; + break; + case 'tag': + $where[] = "`realtag`.`name` $sql_match_operator '$input'"; + $join['tag'] = true; + break; + default: + // Nae laird! + break; + } // switch on ruletype + } // foreach rule + + $where_sql = implode(" $sql_logic_operator ", $where); + + if ($join['tag']) { + $table['tag'] = "LEFT JOIN (SELECT `object_id`, `name` FROM `tag` " . + "LEFT JOIN `tag_map` ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`object_type`='album') AS realtag " . + "ON `album`.`id`=`realtag`.`object_id`"; + } + if ($join['song']) { + $table['song'] = "LEFT JOIN `song` ON `song`.`album`=`album`.`id`"; + } + if ($join['rating']) { + $userid = $GLOBALS['user']->id; + $table['rating'] = "LEFT JOIN " . + "(SELECT `object_id`, `rating` FROM `rating` " . + "WHERE `object_type`='album' AND `user`='$userid' " . + "UNION " . + "SELECT `object_id`, FLOOR(AVG(`rating`)) AS 'rating' FROM `rating` " . + "WHERE `object_type`='album' AND " . + "`object_id` NOT IN (SELECT `object_id` FROM `rating` " . + "WHERE `object_type`='album' AND `user`='$userid') " . + "GROUP BY `object_id` " . + ") AS realrating ON `album`.`id` = `realrating`.`object_id`"; + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`album`.`id`) FROM `album`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } // album_to_sql + + /** + * artist_to_sql + * Handles the generation of the SQL for artist searches. + */ + private function artist_to_sql() { + $sql_logic_operator = $this->logic_operator; + $where = array(); + $table = array(); + $join = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + foreach ($this->basetypes[$type] as $operator) { + if ($operator['name'] == $rule[1]) { + break; + } + } + $input = $this->mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'name': + $where[] = "`artist`.`name` $sql_match_operator '$input'"; + break; + case 'tag': + $where[] = " realtag`.`name` $sql_match_operator '$input'"; + $join['tag'] = true; + break; + default: + // Nihil + break; + } // switch on ruletype + } // foreach rule + + $where_sql = implode(" $sql_logic_operator ", $where); + + if ($join['tag']) { + $table['tag'] = "LEFT JOIN (SELECT `object_id`, `name` FROM `tag` " . + "LEFT JOIN `tag_map` ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`object_type`='artist') AS realtag " . + "ON `artist`.`id`=`realtag`.`object_id`"; + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`artist`.`id`) FROM `artist`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } // artist_to_sql + + /** + * song_to_sql + * Handles the generation of the SQL for song searches. + */ + private function song_to_sql() { + $sql_logic_operator = $this->logic_operator; + + $where = array(); + $table = array(); + $join = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + foreach ($this->basetypes[$type] as $operator) { + if ($operator['name'] == $rule[1]) { + break; + } + } + $input = $this->mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'anywhere': + $where[] = "(`artist`.`name` $sql_match_operator '$input' OR `album`.`name` $sql_match_operator '$input' OR `song_data`.`comment` $sql_match_operator '$input' OR `song`.`file` $sql_match_operator '$input' OR `song`.`title` $sql_match_operator '$input')"; + $join['album'] = true; + $join['artist'] = true; + $join['song_data'] = true; + break; + case 'tag': + $where[] = "`realtag`.`name` $sql_match_operator '$input'"; + $join['tag'] = true; + break; + case 'title': + $where[] = "`song`.`title` $sql_match_operator '$input'"; + break; + case 'album': + $where[] = "`album`.`name` $sql_match_operator '$input'"; + $join['album'] = true; + break; + case 'artist': + $where[] = "`artist`.`name` $sql_match_operator '$input'"; + $join['artist'] = true; + break; + case 'time': + $input = $input * 60; + $where[] = "`song`.`time` $sql_match_operator '$input'"; + break; + case 'file': + $where[] = "`song`.`file` $sql_match_operator '$input'"; + break; + case 'year': + $where[] = "`song`.`year` $sql_match_operator '$input'"; + break; + case 'comment': + $where[] = "`song_data`.`comment` $sql_match_operator '$input'"; + $join['song_data'] = true; + break; + case 'played': + $where[] = " `song`.`played` = '$input'"; + break; + case 'bitrate': + $input = $input * 1000; + $where[] = "`song`.`bitrate` $sql_match_operator '$input'"; + break; + case 'rating': + $where[] = "`realrating`.`rating` $sql_match_operator '$input'"; + $join['rating'] = true; + break; + case 'catalog': + $where[] = "`song`.`catalog` $sql_match_operator '$input'"; + break; + case 'playlist': + $join['playlist_data'] = true; + $where[] = "`playlist_data`.`playlist` $sql_match_operator '$input'"; + break; + case 'smartplaylist': + $subsearch = new Search('song', $input); + $subsql = $subsearch->to_sql(); + $where[] = "$sql_match_operator (" . $subsql['where_sql'] . ")"; + $join = array_merge($subsql['join'], $join); + break; + case 'added': + $input = strtotime($input); + $where[] = "`song`.`addition_time` $sql_match_operator $input"; + break; + case 'updated': + $input = strtotime($input); + $where[] = "`song`.`update_time` $sql_match_operator $input"; + default: + // NOSSINK! + break; + } // end switch on type + } // end foreach over rules + + $where_sql = implode(" $sql_logic_operator ", $where); + + // now that we know which things we want to JOIN... + if ($join['artist']) { + $table['artist'] = "LEFT JOIN `artist` ON `song`.`artist`=`artist`.`id`"; + } + if ($join['album']) { + $table['album'] = "LEFT JOIN `album` ON `song`.`album`=`album`.`id`"; + } + if ($join['song_data']) { + $table['song_data'] = "LEFT JOIN `song_data` ON `song`.`id`=`song_data`.`song_id`"; + } + if ($join['tag']) { + $table['tag'] = "LEFT JOIN (SELECT `object_id`, `name` FROM `tag` " . + "LEFT JOIN `tag_map` ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`object_type`='song') AS realtag " . + "ON `song`.`id`=`realtag`.`object_id`"; + } + if ($join['rating']) { + // We do a join on ratings from the table with a + // preference for our own and fall back to the FLOORed + // average of everyone's rating if it's a song we + // haven't rated. + $userid = $GLOBALS['user']->id; + $table['rating'] = "LEFT JOIN " . + "(SELECT `object_id`, `rating` FROM `rating` " . + "WHERE `object_type`='song' AND `user`='$userid' " . + "UNION " . + "SELECT `object_id`, FLOOR(AVG(`rating`)) AS 'rating' FROM `rating` " . + "WHERE `object_type`='song' AND " . + "`object_id` NOT IN (SELECT `object_id` FROM `rating` " . + "WHERE `object_type`='song' AND `user`='$userid') " . + "GROUP BY `object_id` " . + ") AS realrating ON `song`.`id`=`realrating`.`object_id`"; + } + if ($join['playlist_data']) { + $table['playlist_data'] = "LEFT JOIN `playlist_data` ON `song`.`id`=`playlist_data`.`object_id` AND `playlist_data`.`object_type`='song'"; + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`song`.`id`) FROM `song`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } // end song_to_sql + + /** + * video_to_sql + * Handles the generation of the SQL for video searches. + */ + private function video_to_sql() { + $sql_logic_operator = $this->logic_operator; + + $where = array(); + + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + foreach ($this->basetypes[$type] as $operator) { + if ($operator['name'] == $rule[1]) { + break; + } + } + $input = $this->mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'filename': + $where[] = "`video`.`file` $sql_match_operator '$input'"; + break; + default: + // WE WILLNA BE FOOLED AGAIN! + } // switch on ruletype + } // foreach rule + + $where_sql = explode(" $sql_logic_operator ", $where); + + return array( + 'base' => 'SELECT DISTINCT(`video`.`id`) FROM `video`', + 'where' => $where, + 'where_sql' => $where_sql + ); + } // end video_to_sql + +} // end of Search class +?> diff --git a/lib/class/update.class.php b/lib/class/update.class.php index 0dc1f726..14478de0 100644 --- a/lib/class/update.class.php +++ b/lib/class/update.class.php @@ -371,6 +371,9 @@ class Update { $update_string = '- Modify tmp_browse to allow caching of multiple browses per session.
'; $version[] = array('version' => '360005','description' => $update_string); + $update_string = '- Add table for dynamic playlists.
'; + $version[] = array('version' => '360006','description' => $update_string); + return $version; } // populate_version @@ -1979,5 +1982,24 @@ class Update { self::set_version('db_version','360005'); } // update_360005 + /** + * update_360006 + * This adds the table for newsearch/dynamic playlists + */ + public static function update_360006() { + $sql = "CREATE TABLE `search` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `type` enum('private','public') CHARACTER SET utf8 DEFAULT NULL, + `rules` mediumtext COLLATE utf8_unicode_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8 DEFAULT NULL, + `logic_operator` varchar(3) CHARACTER SET utf8 DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $db_results = Dba::write($sql); + + self::set_version('db_version','360006'); + } + } // end update class ?> diff --git a/lib/init.php b/lib/init.php index d492dc8b..d43c3fc2 100644 --- a/lib/init.php +++ b/lib/init.php @@ -134,7 +134,6 @@ $results['mysql_db'] = $results['database_name']; define('INIT_LOADED','1'); // Library and module includes we can't do with the autoloader -require_once $prefix . '/lib/search.php'; require_once $prefix . '/lib/preferences.php'; require_once $prefix . '/lib/log.lib.php'; require_once $prefix . '/lib/ui.lib.php'; @@ -143,6 +142,7 @@ require_once $prefix . '/lib/batch.lib.php'; require_once $prefix . '/lib/themes.php'; require_once $prefix . '/lib/class/localplay.abstract.php'; require_once $prefix . '/lib/class/database_object.abstract.php'; +require_once $prefix . '/lib/class/playlist_object.abstract.php'; require_once $prefix . '/lib/class/media.interface.php'; require_once $prefix . '/modules/getid3/getid3.php'; require_once $prefix . '/modules/nusoap/nusoap.php'; diff --git a/lib/javascript/search-data.php b/lib/javascript/search-data.php new file mode 100644 index 00000000..11ef68d9 --- /dev/null +++ b/lib/javascript/search-data.php @@ -0,0 +1,49 @@ + $value) { + $json .= '"' . $key . '" : '; + if (is_array($value)) { + $json .= arrayToJSON($value); + } + else { + $json .= '"' . $value . '"'; + } + $json .= ' , '; + } + $json = rtrim($json, ', '); + return $json . ' }'; +} + +Header('content-type: application/x-javascript'); + +$search = new Search($_REQUEST['type']); + +echo 'var types = $H(\''; +echo arrayToJSON($search->types) . "'.evalJSON());\n"; +echo 'var basetypes = $H(\''; +echo arrayToJSON($search->basetypes) . "'.evalJSON());\n"; +echo 'removeIcon = \'' . get_user_icon('disable', _('Remove')) . '\';'; +?> diff --git a/lib/javascript/search.js b/lib/javascript/search.js new file mode 100644 index 00000000..35995001 --- /dev/null +++ b/lib/javascript/search.js @@ -0,0 +1,168 @@ +// vim:set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: +// +// Copyright (c) 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. + +var rowIter = 1; +var rowCount = 0; + +var SearchRow = { + add: function(ruleType, operator, input) { + if (typeof(ruleType) != 'string') { + ruleType = 0; + } + else { + types.each(function(i) { + if (i.value.name == ruleType) { + ruleType = i.key; + throw $break; + } + }); + } + + if (typeof(operator) != 'string') { + operator = 0; + } + else { + $H(basetypes.get(types.get(ruleType).type)).each(function(i) { + if (i.value.name == operator) { + operator = i.key; + throw $break; + } + }); + } + + var row = document.createElement('tr'); + var cells = new Array(); + for (var i = 0 ; i < 5 ; i++) { + cells[i] = document.createElement('td'); + } + + cells[0].appendChild(SearchRow.constructOptions(ruleType, rowIter)); + cells[1].appendChild(SearchRow.constructOperators(ruleType, rowIter, operator)); + cells[2].appendChild(SearchRow.constructInput(ruleType, rowIter, input)); + cells[3].innerHTML = removeIcon; + + cells.each(function(i) { + row.appendChild(i); + }); + + $('searchtable').appendChild(row); + rowCount++; + + Event.observe(cells[3], 'click', function(e){if(rowCount > 1) { Element.remove(this.parentNode); rowCount--; }}); + + rowIter++; + }, + constructInput: function(ruleType, ruleNumber, input) { + if (input === null || input === undefined) { + input = ''; + } + + widget = $H(types.get(ruleType).widget); + + var inputNode = document.createElement(widget.get('0')); + inputNode.id = 'rule_' + ruleNumber + '_input'; + inputNode.name = 'rule_' + ruleNumber + '_input'; + + switch(widget.get('0')) { + case 'input': + inputNode.setAttribute('type', widget.get('1')); + inputNode.setAttribute('value', input); + break; + case 'select': + $H(widget.get('1')).each(function(i) { + var option = document.createElement('option'); + if ( isNaN(parseInt(i.value)) ) { + realvalue = i.key; + } + else { + realvalue = parseInt(i.value); + } + if ( input == realvalue ) { + option.selected = true; + } + option.value = realvalue; + option.innerHTML = i.value; + inputNode.appendChild(option); + }); + break; + } + + return inputNode; + }, + constructOptions: function(ruleType, ruleNumber) { + var optionsNode = document.createElement('select'); + optionsNode.id = 'rule_' + ruleNumber; + optionsNode.name = 'rule_' + ruleNumber; + + types.each(function(i) { + var option = document.createElement('option'); + option.innerHTML = i.value.label; + option.value = i.value.name; + if ( i.key == ruleType ) { + option.selected = true; + } + optionsNode.appendChild(option); + }); + + Event.observe(optionsNode, 'change', SearchRow.update); + + return optionsNode; + }, + constructOperators: function(ruleType, ruleNumber, operator) { + var operatorNode = document.createElement('select'); + operatorNode.id = 'rule_' + ruleNumber + '_operator'; + operatorNode.name = 'rule_' + ruleNumber + '_operator'; + + basetype = types.get(ruleType).type; + operatorNode.className = 'operator' + basetype; + + $H(basetypes.get(basetype)).each(function(i) { + var option = document.createElement('option'); + option.innerHTML = i.value.description; + option.value = i.key; + if (i.key == operator) { + option.selected = true; + } + operatorNode.appendChild(option); + }); + + return operatorNode; + }, + update: function() { + var r_findID = /rule_(\d+)/; + var targetID = r_findID.exec(this.id)[1]; + + var operator = $('rule_' + targetID + '_operator'); + if (operator.className != 'operator' + types.get(this.selectedIndex).type) { + var operator_cell = operator.parentNode; + Element.remove(operator); + operator_cell.appendChild(SearchRow.constructOperators(this.selectedIndex, targetID)); + } + + var input = $('rule_' + targetID + '_input'); + + if (input.type == 'text') { + var oldinput = input.value; + } + + var input_cell = input.parentNode; + Element.remove(input); + input_cell.appendChild(SearchRow.constructInput(this.selectedIndex, targetID, oldinput)); + } +}; diff --git a/lib/search.php b/lib/search.php deleted file mode 100644 index 992b01d2..00000000 --- a/lib/search.php +++ /dev/null @@ -1,294 +0,0 @@ - - * @copyright 2001 - 2011 Ampache.org - * @license http://opensource.org/licenses/gpl-2.0 GPLv2 - * @version PHP 5.2 - * @link http://www.ampache.org/ - * @since File available since Release 1.0 - */ - -/** - * run_search - * this function actually runs the search, and returns an array of the results. Unlike the previous - * function it does not do the display work its self. - */ -function run_search($data) { - - /* Create an array of the object we need to search on */ - foreach ($data as $key=>$value) { - /* Get the first two chars to check - * and see if it's s_ - */ - $prefix = substr($key,0,2); - $value = trim($value); - - if ($prefix == 's_' AND strlen($value)) { - $true_name = substr($key,2,strlen($key)); - $search[$true_name] = Dba::escape($value); - } - - } // end foreach - - /* Figure out if they want a AND based search or a OR based search */ - switch($_REQUEST['operator']) { - case 'or': - $operator = 'OR'; - break; - default: - $operator = 'AND'; - break; - } // end switch on operator - - /* Figure out what type of method they would like to use, exact or fuzzy */ - switch($_REQUEST['method']) { - case 'fuzzy': - $method = "LIKE '%__%'"; - break; - default: - $method = "= '__'"; - break; - } // end switch on method - - $limit = intval($_REQUEST['limit']); - - /* Switch, and run the correct function */ - switch($_REQUEST['object_type']) { - case 'artist': - case 'album': - case 'song': - $function_name = 'search_' . $_REQUEST['object_type']; - if (function_exists($function_name)) { - $results = call_user_func($function_name,$search,$operator,$method,$limit); - return $results; - } - break; - default: - $results = search_song($search,$operator,$method,$limit); - return $results; - break; - } // end switch - - return array(); - -} // run_search - -/** - * search_song - * This function deals specificly with returning song object for the run_search - * function, it assumes that our root table is songs - * @package Search - * @catagory Search - */ -function search_song($data,$operator,$method,$limit) { - - /* Generate BASE SQL */ - - $where_sql = ''; - $table_sql = ''; - $group_sql = ' GROUP BY'; - $select_sql = ','; - $field_sql = ''; - $order_sql = ''; - - if ($limit > 0) { - $limit_sql = " LIMIT $limit"; - } - - foreach ($data as $type=>$value) { - - /* Create correct Value statement based on method */ - - $value_string = str_replace("__",$value,$method); - - switch ($type) { - case 'all': - $additional_soundex = false; - - if (!(strpos($value, '-'))) // if we want a fuzzier search - $additional_soundex = true; - - $where_sql = "( MATCH (`artist2`.`name`, `album2`.`name`, `song`.`title`) AGAINST ('$value' IN BOOLEAN MODE)"; - - if ($additional_soundex) { - $where_sql.= " OR `artist2`.`name` SOUNDS LIKE '$value'"; - $where_sql.= " OR `album2`.`name` SOUNDS LIKE '$value'"; - $where_sql.= " OR `song`.`title` SOUNDS LIKE '$value' "; - } - $where_sql .= ") $operator"; - - $table_sql = " LEFT JOIN `album` as `album2` ON `song`.`album`=`album2`.`id`"; - $table_sql.= " LEFT JOIN `artist` AS `artist2` ON `song`.`artist`=`artist2`.`id`"; - - $order_sql = " ORDER BY"; - - $order_sql.= " MATCH (`artist2`.`name`) AGAINST ('$value' IN BOOLEAN MODE)"; - if ($additional_soundex) $order_sql.= " + (SOUNDEX(`artist2`.`name`)=SOUNDEX('$value')) DESC,"; else $order_sql.= " DESC,"; - - $order_sql.= " MATCH (`album2`.`name`) AGAINST ('$value' IN BOOLEAN MODE)"; - if ($additional_soundex) $order_sql.= " + (SOUNDEX(`album2`.`name`)=SOUNDEX('$value')) DESC,"; else $order_sql.= " DESC,"; - - $order_sql.= " MATCH (`song`.`title`) AGAINST ('$value' IN BOOLEAN MODE)"; - if ($additional_soundex) $order_sql.= " + (SOUNDEX(`song`.`title`)=SOUNDEX('$value')) DESC,"; else $order_sql.= " DESC,"; - - $order_sql.= " `artist2`.`name`,"; - $order_sql.= " `album2`.`name`,"; - $order_sql.= " `song`.`track`,"; - $order_sql.= " `song`.`title`"; - break; - case 'title': - $where_sql .= " `song`.`title` $value_string $operator"; - break; - case 'album': - $where_sql .= " `album`.`name` $value_string $operator"; - $table_sql .= " LEFT JOIN `album` ON `song`.`album`=`album`.`id`"; - break; - case 'artist': - $where_sql .= " `artist`.`name` $value_string $operator"; - $table_sql .= " LEFT JOIN `artist` ON `song`.`artist`=`artist`.`id` "; - break; - case 'year': - if (empty($data["year2"]) && is_numeric($data["year"])) { - $where_sql .= " `song`.`year` $value_string $operator"; - } - elseif (!empty($data["year"]) && is_numeric($data["year"]) && !empty($data["year2"]) && is_numeric($data["year2"])) { - $where_sql .= " (`song`.`year` BETWEEN ".$data["year"]." AND ".$data["year2"].") $operator"; - } - break; - case 'time': - if (!empty($data['time2'])) { - $where_sql .= " `song`.`time` <= " . Dba::escape(intval($data['time2'])*60) . " $operator"; - } - if (!empty($data['time'])) { - $where_sql .= " `song`.`time` >= " . Dba::escape(intval($data['time'])*60) . " $operator"; - } - break; - case 'filename': - $where_sql .= " `song`.`file` $value_string $operator"; - break; - case 'comment': - $table_sql .= ' INNER JOIN `song_data` ON `song`.`id`=`song_data`.`song_id`'; - $where_sql .= " `song_data`.`comment` $value_string $operator"; - break; - case 'played': - /* This is a 0/1 value so bool it */ - $value = make_bool($value); - $where_sql .= " `song`.`played` = '$value' $operator"; - break; - case 'minbitrate': - $value = intval($value); - $where_sql .= " `song`.`bitrate` >= ('$value'*1000) $operator"; - break; - case 'rating': - $value = intval($value); - $userid = $GLOBALS['user']->id; - $rcomparison = '>='; - if ($_REQUEST['s_rating_operator'] == '1') { - $rcomparison = '<='; - } - elseif ($_REQUEST['s_rating_operator'] == '2') { - $rcomparison = '<=>'; - } - // Complex SQL follows - // We do a join on ratings from the table with a - // preference for our own and fall back to the - // FLOORed average of everyone's rating if it's - // a song we haven't rated. - if ($operator == 'AND') { - $table_sql .= ' INNER JOIN'; - } - else { - $table_sql .= ' LEFT JOIN'; - } - $table_sql .= " (SELECT `object_id`, `rating` FROM `rating` WHERE `object_type`='song' AND `user`='$userid' - UNION - SELECT `object_id`, FLOOR(AVG(`rating`)) AS 'rating' FROM `rating` - WHERE `object_type`='song' AND - `object_id` NOT IN (SELECT `object_id` FROM `rating` WHERE `object_type`='song' AND `user`='$userid') - GROUP BY `object_id` - ) AS realrating ON `song`.`id` = `realrating`.`object_id`"; - - $where_sql .= " `realrating`.`rating` $rcomparison '$value' $operator"; - break; - case 'tag': - - // Fill it with one value to prevent sql error on no results - $ids = array('0'); - - $tag_sql = "SELECT `object_id` FROM `tag` LEFT JOIN `tag_map` ON `tag`.`id`=`tag_map`.`tag_id` " . - "WHERE `tag_map`.`object_type`='song' AND `tag`.`name` $value_string "; - $db_results = Dba::read($tag_sql); - - while ($row = Dba::fetch_assoc($db_results)) { - $ids[] = $row['object_id']; - } - - $where_sql = " `song`.`id` IN (" . implode(',',$ids) . ") $operator"; - - break; - default: - // Notzing! - break; - } // end switch on type - - - } // foreach data - - /* Trim off the extra $method's and ,'s then combine the sucka! */ - $where_sql = rtrim($where_sql,$operator); - $group_sql = rtrim($group_sql,','); - $select_sql = rtrim($select_sql,','); - - if ($group_sql == ' GROUP BY') { $group_sql = ''; } - - $base_sql = "SELECT DISTINCT(`song`.`id`) $field_sql $select_sql FROM `song`"; - - $sql = $base_sql . $table_sql . " WHERE " . $where_sql . $group_sql . $order_sql . $limit_sql; - - /** - * Because we might need this for Dynamic Playlist Action - * but we don't trust users to provide this store it in the - * session where they can't get to it! - */ - - $_SESSION['userdata']['stored_search'] = $sql; - - $db_results = Dba::read($sql); - - $results = array(); - - while ($row = Dba::fetch_assoc($db_results)) { - $results[] = $row['id']; - } - - return $results; - -} // search_songs - - -?> diff --git a/random.php b/random.php index 841b333b..e8dd8fb4 100644 --- a/random.php +++ b/random.php @@ -37,18 +37,15 @@ show_header(); switch ($_REQUEST['action']) { case 'get_advanced': - $object_ids = Random::advanced($_POST); + $object_ids = Random::advanced($_REQUEST['type'], $_POST); // We need to add them to the active playlist foreach ($object_ids as $object_id) { - $GLOBALS['user']->playlist->add_object($object_id,'song'); + $GLOBALS['user']->playlist->add_object($object_id, 'song'); } - case 'advanced': default: require_once Config::get('prefix') . '/templates/show_random.inc.php'; -/* require_once Config::get('prefix') . '/templates/show_random_rules.inc.php';*/ - break; } // end switch diff --git a/search.php b/search.php index 4ef60940..c8eff20d 100644 --- a/search.php +++ b/search.php @@ -39,24 +39,12 @@ show_header(); * action switch */ switch ($_REQUEST['action']) { - case 'quick_search': - /* This needs to be done because we don't know what thing - * they used the quick search to search on until after they've - * submited it - */ - $_REQUEST['s_all'] = $_REQUEST['search_string']; - - if (strlen($_REQUEST['search_string']) < 1) { - Error::add('keyword',_('Error: No Keyword Entered')); - require_once Config::get('prefix') . '/templates/show_search.inc.php'; - break; - } case 'search': $browse = new Browse(); require_once Config::get('prefix') . '/templates/show_search.inc.php'; require_once Config::get('prefix') . '/templates/show_search_options.inc.php'; - $results = run_search($_REQUEST); - $browse->set_type('song'); + $results = Search::run($_REQUEST); + $browse->set_type($_REQUEST['type']); $browse->show_objects($results); $browse->store(); break; @@ -65,6 +53,10 @@ switch ($_REQUEST['action']) { $playlist = new Playlist($playlist_id); show_confirmation(_('Search Saved'),sprintf(_('Your Search has been saved as a track in %s'), $playlist->name),conf('web_path') . "/search.php"); break; + case 'save_as_smartplaylist': + $playlist = new Search(); + $playlist->parse_rules(Search::clean_request($_REQUEST)); + $playlist->save(); default: require_once Config::get('prefix') . '/templates/show_search.inc.php'; break; diff --git a/server/browse.ajax.php b/server/browse.ajax.php index 60d3cb8d..f24511b0 100644 --- a/server/browse.ajax.php +++ b/server/browse.ajax.php @@ -99,6 +99,12 @@ switch ($_REQUEST['action']) { $playlist->delete(); $key = 'playlist_row_' . $playlist->id; break; + case 'smartplaylist': + $playlist = new Search('song', $_REQUEST['id']); + if (!$playlist->has_access()) { exit; } + $playlist->delete(); + $key = 'playlist_row_' . $playlist->id; + break; case 'live_stream': if (!$GLOBALS['user']->has_access('75')) { exit; } $radio = new Radio($_REQUEST['id']); diff --git a/smartplaylist.php b/smartplaylist.php new file mode 100644 index 00000000..fa243ea3 --- /dev/null +++ b/smartplaylist.php @@ -0,0 +1,103 @@ +has_access()) { + $playlist->delete(); + // Go elsewhere + header('Location: ' . Config::get('web_path') . '/browse.php?action=smartplaylist'); + } +} + +show_header(); + +/* Switch on the action passed in */ +switch ($_REQUEST['action']) { + case 'create_playlist': + /* Check rights */ + if (!Access::check('interface','25')) { + access_denied(); + break; + } + + foreach ($_REQUEST as $key => $value) { + $prefix = substr($key, 0, 4); + $value = trim($value); + + if ($prefix == 'rule' && strlen($value)) { + $rules[$key] = Dba::escape($value); + } + } + + switch($_REQUEST['operator']) { + case 'or': + $operator = 'OR'; + break; + default: + $operator = 'AND'; + break; + } // end switch on operator + + $playlist_name = scrub_in($_REQUEST['playlist_name']); + + $playlist = new Search('song'); + $playlist->parse_rules($data); + $playlist->logic_operator = $operator; + $playlist->name = $playlist_name; + $playlist->save(); + + break; + case 'delete_playlist': + // If we made it here, we didn't have sufficient rights. + access_denied(); + break; + case 'show_playlist': + $playlist = new Search('song', $_REQUEST['playlist_id']); + $playlist->format(); + require_once Config::get('prefix') . '/templates/show_smartplaylist.inc.php'; + break; + case 'update_playlist': + $playlist = new Search('song', $_REQUEST['playlist_id']); + if ($playlist->has_access()) { + $playlist->parse_rules(Search::clean_request($_REQUEST)); + $playlist->update(); + $playlist->format(); + } + else { + access_denied(); + break; + } + require_once Config::get('prefix') . '/templates/show_smartplaylist.inc.php'; + break; + default: + require_once Config::get('prefix') . '/templates/show_smartplaylist.inc.php'; + break; +} // switch on the action + +show_footer(); +?> diff --git a/templates/show_edit_smartplaylist_row.inc.php b/templates/show_edit_smartplaylist_row.inc.php new file mode 100644 index 00000000..fadd1bc7 --- /dev/null +++ b/templates/show_edit_smartplaylist_row.inc.php @@ -0,0 +1,45 @@ + + +
+ + + + + +
+ + + type; ${$name} = ' selected="selected"'; ?> + + + + + id . '&type=smartplaylist_row','download',_('Save Changes'),'save_playlist_' . $playlist->id,'edit_playlist_' . $playlist->id); ?> +
+
+ + diff --git a/templates/show_edit_smartplaylist_title.inc.php b/templates/show_edit_smartplaylist_title.inc.php new file mode 100644 index 00000000..e6451e66 --- /dev/null +++ b/templates/show_edit_smartplaylist_title.inc.php @@ -0,0 +1,43 @@ + +
+ + + + + +
+ + + type; ${$name} = ' selected="selected"'; ?> + + + + + id . '&type=smartplaylist_title','download',_('Save Changes'),'save_playlist_' . $playlist->id,'edit_playlist_' . $playlist->id); ?> +
+
+ diff --git a/templates/show_random.inc.php b/templates/show_random.inc.php index 65815e5e..b1287ee8 100644 --- a/templates/show_random.inc.php +++ b/templates/show_random.inc.php @@ -33,80 +33,70 @@ ?> -
- + +
- - - - - - + + + + +
- - -
- - -
 
+ - - + + - - + + - - + +
- - - + + +
- - + + +
- - - + + +
+ + +
- +
diff --git a/templates/show_random_rules.inc.php b/templates/show_random_rules.inc.php deleted file mode 100644 index 45be357e..00000000 --- a/templates/show_random_rules.inc.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @copyright 2001 - 2011 Ampache.org - * @license http://opensource.org/licenses/gpl-2.0 GPLv2 - * @version PHP 5.2 - * @link http://www.ampache.org/ - * @since File available since Release 1.0 - */ - -?> - - -- - - - - - - - - - - - - - - - - - - - -
- diff --git a/templates/show_rules.inc.php b/templates/show_rules.inc.php new file mode 100644 index 00000000..1f98e0ae --- /dev/null +++ b/templates/show_rules.inc.php @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + +
+ +
+ + + + + +
+ + +to_js(); +} +else { + $mysearch = new Search($_REQUEST['type']); + $mysearch->parse_rules(Search::clean_request($_REQUEST)); + $out = $mysearch->to_js(); +} +if ($out) { + echo $out; +} +else { + echo ''; +} +?> diff --git a/templates/show_search.inc.php b/templates/show_search.inc.php index 47c3085f..18992b16 100644 --- a/templates/show_search.inc.php +++ b/templates/show_search.inc.php @@ -37,130 +37,39 @@ */ ?> -
+ - - + + + + + + + +
 
+ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
- + - -
- - - -
- - - -
- - - - - - -
- - - - - - - -
- - - -
- - - - -
- - - -
+ + +
   - + +    + +
diff --git a/templates/show_search_bar.inc.php b/templates/show_search_bar.inc.php index 486ad9fe..b5bb4f4f 100644 --- a/templates/show_search_bar.inc.php +++ b/templates/show_search_bar.inc.php @@ -33,13 +33,20 @@ ?>
-
- - - + + + + + - +
diff --git a/templates/show_search_options.inc.php b/templates/show_search_options.inc.php index dad9d733..1d890767 100644 --- a/templates/show_search_options.inc.php +++ b/templates/show_search_options.inc.php @@ -40,7 +40,7 @@
  • - +
  • diff --git a/templates/show_smartplaylist.inc.php b/templates/show_smartplaylist.inc.php new file mode 100644 index 00000000..4c276ffd --- /dev/null +++ b/templates/show_smartplaylist.inc.php @@ -0,0 +1,67 @@ + +id . '">' . $title . + ''); +?> +
    + +
    + +
    + + + +
    + +
    + +
    + + diff --git a/templates/show_smartplaylist_row.inc.php b/templates/show_smartplaylist_row.inc.php new file mode 100644 index 00000000..bf1f43f0 --- /dev/null +++ b/templates/show_smartplaylist_row.inc.php @@ -0,0 +1,39 @@ + + + id,'add',_('Add'),'add_playlist_' . $playlist->id); ?> + +f_link; ?> +f_type; ?> +f_user); ?> + + + + + + + has_access()) { ?> + id,'edit',_('Edit'),'edit_playlist_' . $playlist->id); ?> + id,'delete',_('Delete'),'delete_playlist_' . $playlist->id); ?> + + diff --git a/templates/show_smartplaylist_title.inc.php b/templates/show_smartplaylist_title.inc.php new file mode 100644 index 00000000..8691ac98 --- /dev/null +++ b/templates/show_smartplaylist_title.inc.php @@ -0,0 +1,24 @@ + + +f_type, $playlist->name); ?> diff --git a/templates/show_smartplaylists.inc.php b/templates/show_smartplaylists.inc.php new file mode 100644 index 00000000..2c891884 --- /dev/null +++ b/templates/show_smartplaylists.inc.php @@ -0,0 +1,63 @@ + + + ++ + + + + + + + + + + + + + +format(); +?> + + + + + + + + + + + + + + + + +
     
     
    + diff --git a/templates/sidebar_home.inc.php b/templates/sidebar_home.inc.php index b0fb642a..4b97abd5 100644 --- a/templates/sidebar_home.inc.php +++ b/templates/sidebar_home.inc.php @@ -48,6 +48,7 @@ $ajax_info = Config::get('ajax_url'); $web_path = Config::get('web_path');
  • +
  • @@ -77,7 +78,7 @@ $ajax_info = Config::get('ajax_url'); $web_path = Config::get('web_path');
  • -
  • +
  • -- cgit