src/Repository/Model/Search.php
<?php
declare(strict_types=0);
/**
* vim:set softtabstop=4 shiftwidth=4 expandtab:
*
* LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
* Copyright Ampache.org, 2001-2023
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Ampache\Repository\Model;
use Ampache\Config\AmpConfig;
use Ampache\Module\Authorization\Access;
use Ampache\Module\Playlist\Search\AlbumDiskSearch;
use Ampache\Module\Playlist\Search\AlbumSearch;
use Ampache\Module\Playlist\Search\ArtistSearch;
use Ampache\Module\Playlist\Search\LabelSearch;
use Ampache\Module\Playlist\Search\PlaylistSearch;
use Ampache\Module\Playlist\Search\PodcastEpisodeSearch;
use Ampache\Module\Playlist\Search\PodcastSearch;
use Ampache\Module\Playlist\Search\SongSearch;
use Ampache\Module\Playlist\Search\TagSearch;
use Ampache\Module\Playlist\Search\UserSearch;
use Ampache\Module\Playlist\Search\VideoSearch;
use Ampache\Module\System\Dba;
use Ampache\Module\System\Core;
use Ampache\Repository\MetadataFieldRepositoryInterface;
use Ampache\Repository\LicenseRepositoryInterface;
use Ampache\Repository\UserRepositoryInterface;
/**
* Search-related voodoo. Beware tentacles.
*/
class Search extends playlist_object
{
protected const DB_TABLENAME = 'search';
public const VALID_TYPES = array(
'song',
'album',
'album_disk',
'song_artist',
'album_artist',
'artist',
'genre',
'label',
'playlist',
'podcast',
'podcast_episode',
'tag',
'user',
'video'
);
// override playlist_object
public ?string $type = 'public';
// rules used to run a search (User chooses rules from available types for that object)
public $rules; // JSON string to decoded to array
public ?string $logic_operator = 'AND';
public ?int $random = 0;
public int $limit = 0;
public ?int $last_count = 0;
public $objectType; // the type of object you want to return (self::VALID_TYPES)
public $search_user; // user running the search
public $types = array(); // rules that are available to the objectType (title, year, rating, etc)
public $basetypes = array(); // rule operator subtypes (numeric, text, boolean, etc)
private $searchType; // generate sql for the object type (Ampache\Module\Playlist\Search\*)
private $stars;
private $order_by;
/**
* constructor
* @param int|null $search_id // saved searches have rules already
* @param string $object_type // map to self::VALID_TYPES
* @param User|null $user
*/
public function __construct($search_id = 0, $object_type = 'song', ?User $user = null)
{
$this->search_user = $user;
if (!$this->search_user instanceof User) {
$this->search_user = User::get_from_global() ?? new User(-1);
}
$this->objectType = (in_array(strtolower($object_type), self::VALID_TYPES))
? strtolower($object_type)
: 'song';
$this->user = $this->search_user->id ?? -1; // define a user for live searches (overwriten if saved before)
if ($search_id > 0) {
$info = $this->get_info($search_id, static::DB_TABLENAME);
foreach ($info as $key => $value) {
if ($key == 'rules') {
$this->rules = json_decode((string)$value, true);
if (!is_array($this->rules)) {
debug_event(__CLASS__, "Can't decode key 'rules'. Not a valid json.", 1);
$this->rules = array();
}
} else {
$this->$key = $value;
}
}
if (!is_array($this->rules)) {
$this->rules = array();
}
// make sure saved rules match the correct names
$rule_count = 0;
foreach ($this->rules as $rule) {
$this->rules[$rule_count][0] = $this->_get_rule_name($rule[0]);
$rule_count++;
}
// When loading a search use the owner ID for the search
if ($this->user > 0) {
$this->search_user = new User($this->user);
}
}
$this->stars = array(
T_('0 Stars'),
T_('1 Star'),
T_('2 Stars'),
T_('3 Stars'),
T_('4 Stars'),
T_('5 Stars')
);
// Define our basetypes
$this->_set_basetypes();
switch ($this->objectType) {
case 'album':
$this->_set_types_album();
$this->searchType = new AlbumSearch();
$this->order_by = '`album`.`name`';
break;
case 'album_disk':
$this->_set_types_album();
$this->searchType = new AlbumDiskSearch();
$this->order_by = '`album`.`name`';
break;
case 'artist':
case 'album_artist':
case 'song_artist':
$this->_set_types_artist();
$this->searchType = new ArtistSearch($this->objectType);
$this->order_by = '`artist`.`name`';
$this->objectType = 'artist';
break;
case 'label':
$this->_set_types_label();
$this->searchType = new LabelSearch();
$this->order_by = '`label`.`name`';
break;
case 'playlist':
$this->_set_types_playlist();
$this->searchType = new PlaylistSearch();
$this->order_by = '`playlist`.`name`';
break;
case 'podcast':
$this->_set_types_podcast();
$this->searchType = new PodcastSearch();
$this->order_by = '`podcast`.`title`';
break;
case 'podcast_episode':
$this->_set_types_podcast_episode();
$this->searchType = new PodcastEpisodeSearch();
$this->order_by = '`podcast_episode`.`pubdate` DESC';
break;
case 'song':
$this->_set_types_song();
$this->searchType = new SongSearch();
$this->order_by = '`song`.`file`';
break;
case 'tag':
case 'genre':
$this->_set_types_tag();
$this->searchType = new TagSearch();
$this->order_by = '`tag`.`name`';
break;
case 'user':
$this->_set_types_user();
$this->searchType = new UserSearch();
$this->order_by = '`user`.`username`';
break;
case 'video':
$this->_set_types_video();
$this->searchType = new VideoSearch();
$this->order_by = '`video`.`file`';
break;
} // end switch on objectType
}
public function getId(): int
{
return (int)($this->id ?? 0);
}
public function isNew(): bool
{
return $this->getId() === 0;
}
/**
* _set_basetypes
*
* Function called during construction to set the different types and rules for search
*/
private function _set_basetypes(): void
{
$this->basetypes['numeric'][] = array(
'name' => 'gte',
'description' => T_('is greater than or equal to'),
'sql' => '>='
);
$this->basetypes['numeric'][] = array(
'name' => 'lte',
'description' => T_('is less than or equal to'),
'sql' => '<='
);
$this->basetypes['numeric'][] = array(
'name' => 'equal',
'description' => T_('equals'),
'sql' => '<=>'
);
$this->basetypes['numeric'][] = array(
'name' => 'ne',
'description' => T_('does not equal'),
'sql' => '<>'
);
$this->basetypes['numeric'][] = array(
'name' => 'gt',
'description' => T_('is greater than'),
'sql' => '>'
);
$this->basetypes['numeric'][] = array(
'name' => 'lt',
'description' => T_('is less than'),
'sql' => '<'
);
$this->basetypes['is_true'][] = array(
'name' => 'true',
'description' => T_('is true'),
'sql' => '1'
);
$this->basetypes['boolean'][] = array(
'name' => 'true',
'description' => T_('is true'),
'sql' => '1'
);
$this->basetypes['boolean'][] = array(
'name' => 'false',
'description' => T_('is false'),
'sql' => '0'
);
$this->basetypes['text'][] = array(
'name' => 'contain',
'description' => T_('contains'),
'sql' => 'LIKE',
'preg_match' => array('/^/', '/$/'),
'preg_replace' => array('%', '%')
);
$this->basetypes['text'][] = array(
'name' => 'notcontain',
'description' => T_('does not contain'),
'sql' => 'NOT LIKE',
'preg_match' => array('/^/', '/$/'),
'preg_replace' => array('%', '%')
);
$this->basetypes['text'][] = array(
'name' => 'start',
'description' => T_('starts with'),
'sql' => 'LIKE',
'preg_match' => '/$/',
'preg_replace' => '%'
);
$this->basetypes['text'][] = array(
'name' => 'end',
'description' => T_('ends with'),
'sql' => 'LIKE',
'preg_match' => '/^/',
'preg_replace' => '%'
);
$this->basetypes['text'][] = array(
'name' => 'equal',
'description' => T_('is'),
'sql' => '='
);
$this->basetypes['text'][] = array(
'name' => 'not equal',
'description' => T_('is not'),
'sql' => '!='
);
$this->basetypes['text'][] = array(
'name' => 'sounds',
'description' => T_('sounds like'),
'sql' => 'SOUNDS LIKE'
);
$this->basetypes['text'][] = array(
'name' => 'notsounds',
'description' => T_('does not sound like'),
'sql' => 'NOT SOUNDS LIKE'
);
$this->basetypes['text'][] = array(
'name' => 'regexp',
'description' => T_('matches regular expression'),
'sql' => 'REGEXP'
);
$this->basetypes['text'][] = array(
'name' => 'notregexp',
'description' => T_('does not match regular expression'),
'sql' => 'NOT REGEXP'
);
$this->basetypes['tags'][] = array(
'name' => 'contain',
'description' => T_('contains'),
'sql' => 'LIKE',
'preg_match' => array('/^/', '/$/'),
'preg_replace' => array('%', '%')
);
$this->basetypes['tags'][] = array(
'name' => 'notcontain',
'description' => T_('does not contain'),
'sql' => 'NOT LIKE',
'preg_match' => array('/^/', '/$/'),
'preg_replace' => array('%', '%')
);
$this->basetypes['tags'][] = array(
'name' => 'start',
'description' => T_('starts with'),
'sql' => 'LIKE',
'preg_match' => '/$/',
'preg_replace' => '%'
);
$this->basetypes['tags'][] = array(
'name' => 'end',
'description' => T_('ends with'),
'sql' => 'LIKE',
'preg_match' => '/^/',
'preg_replace' => '%'
);
$this->basetypes['tags'][] = array(
'name' => 'equal',
'description' => T_('is'),
'sql' => '>'
);
$this->basetypes['tags'][] = array(
'name' => 'not equal',
'description' => T_('is not'),
'sql' => '='
);
$this->basetypes['boolean_numeric'][] = array(
'name' => 'equal',
'description' => T_('is'),
'sql' => '<=>'
);
$this->basetypes['boolean_numeric'][] = array(
'name' => 'ne',
'description' => T_('is not'),
'sql' => '<>'
);
$this->basetypes['boolean_subsearch'][] = array(
'name' => 'equal',
'description' => T_('is'),
'sql' => ''
);
$this->basetypes['boolean_subsearch'][] = array(
'name' => 'ne',
'description' => T_('is not'),
'sql' => 'NOT'
);
$this->basetypes['date'][] = array(
'name' => 'lt',
'description' => T_('before'),
'sql' => '<'
);
$this->basetypes['date'][] = array(
'name' => 'gt',
'description' => T_('after'),
'sql' => '>'
);
$this->basetypes['days'][] = array(
'name' => 'lt',
'description' => T_('before (x) days ago'),
'sql' => '<'
);
$this->basetypes['days'][] = array(
'name' => 'gt',
'description' => T_('after (x) days ago'),
'sql' => '>'
);
$this->basetypes['recent_played'][] = array(
'name' => 'ply',
'description' => T_('Limit'),
'sql' => '`date`'
);
$this->basetypes['recent_added'][] = array(
'name' => 'add',
'description' => T_('Limit'),
'sql' => '`addition_time`'
);
$this->basetypes['recent_updated'][] = array(
'name' => 'upd',
'description' => T_('Limit'),
'sql' => '`update_time`'
);
$this->basetypes['user_numeric'][] = array(
'name' => 'love',
'description' => T_('has loved'),
'sql' => 'userflag'
);
$this->basetypes['user_numeric'][] = array(
'name' => '5star',
'description' => T_('has rated 5 stars'),
'sql' => '`rating` = 5'
);
$this->basetypes['user_numeric'][] = array(
'name' => '4star',
'description' => T_('has rated 4 stars'),
'sql' => '`rating` = 4'
);
$this->basetypes['user_numeric'][] = array(
'name' => '3star',
'description' => T_('has rated 3 stars'),
'sql' => '`rating` = 3'
);
$this->basetypes['user_numeric'][] = array(
'name' => '2star',
'description' => T_('has rated 2 stars'),
'sql' => '`rating` = 2'
);
$this->basetypes['user_numeric'][] = array(
'name' => '1star',
'description' => T_('has rated 1 star'),
'sql' => '`rating` = 1'
);
$this->basetypes['user_numeric'][] = array(
'name' => 'unrated',
'description' => T_('has not rated'),
'sql' => 'unrated'
);
$this->basetypes['multiple'] = array_merge($this->basetypes['text'], $this->basetypes['numeric']);
}
/**
* _add_type_numeric
*
* Generic integer searches rules
* @param string $name
* @param string $label
* @param string $type
* @param string $group
*/
private function _add_type_numeric($name, $label, $type = 'numeric', $group = ''): void
{
$this->types[] = array(
'name' => $name,
'label' => $label,
'type' => $type,
'widget' => array('input', 'number'),
'title' => $group
);
}
/**
* _add_type_date
*
* Generic date searches rules
* @param string $name
* @param string $label
* @param string $group
*/
private function _add_type_date($name, $label, $group = ''): void
{
$this->types[] = array(
'name' => $name,
'label' => $label,
'type' => 'date',
'widget' => array('input', 'datetime-local'),
'title' => $group
);
}
/**
* _add_type_text
*
* Generic text rules
* @param string $name
* @param string $label
* @param string $group
*/
private function _add_type_text($name, $label, $group = ''): void
{
$this->types[] = array(
'name' => $name,
'label' => $label,
'type' => 'text',
'widget' => array('input', 'text'),
'title' => $group
);
}
/**
* _add_type_select
*
* Generic rule to select from a list
* @param string $name
* @param string $label
* @param string $type
* @param array $array
* @param string $group
*/
private function _add_type_select($name, $label, $type, $array, $group = ''): void
{
$this->types[] = array(
'name' => $name,
'label' => $label,
'type' => $type,
'widget' => array('select', $array),
'title' => $group
);
}
/**
* _add_type_boolean
*
* True or false generic searches
* @param string $name
* @param string $label
* @param string $type
* @param string $group
*/
private function _add_type_boolean($name, $label, $type = 'boolean', $group = ''): void
{
$this->types[] = array(
'name' => $name,
'label' => $label,
'type' => $type,
'widget' => array('input', 'hidden'),
'title' => $group
);
}
/**
* _set_types_song
*
* this is where all the available rules for songs are defined
*/
private function _set_types_song(): void
{
$this->_add_type_text('anywhere', T_('Any searchable text'));
$t_song_data = T_('Song Data');
$this->_add_type_text('title', T_('Title'), $t_song_data);
$this->_add_type_text('album', T_('Album'), $t_song_data);
$this->_add_type_text('artist', T_('Song Artist'), $t_song_data);
$this->_add_type_text('album_artist', T_('Album Artist'), $t_song_data);
$this->_add_type_text('composer', T_('Composer'), $t_song_data);
$this->_add_type_numeric('track', T_('Track'), 'numeric', $t_song_data);
$this->_add_type_numeric('year', T_('Year'), 'numeric', $t_song_data);
$this->_add_type_numeric('time', T_('Length (in minutes)'), 'numeric', $t_song_data);
$this->_add_type_text('label', T_('Label'), $t_song_data);
$this->_add_type_text('comment', T_('Comment'), $t_song_data);
$this->_add_type_text('lyrics', T_('Lyrics'), $t_song_data);
$t_ratings = T_('Ratings');
if (AmpConfig::get('ratings')) {
$this->_add_type_select('myrating', T_('My Rating'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('rating', T_('Rating (Average)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('albumrating', T_('My Rating (Album)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('artistrating', T_('My Rating (Artist)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_boolean('my_flagged', T_('My Favorite Songs'), 'boolean', $t_ratings);
$this->_add_type_boolean('my_flagged_album', T_('My Favorite Albums'), 'boolean', $t_ratings);
$this->_add_type_boolean('my_flagged_artist', T_('My Favorite Artists'), 'boolean', $t_ratings);
$this->_add_type_text('favorite', T_('Favorites'), $t_ratings);
$this->_add_type_text('favorite_album', T_('Favorites (Album)'), $t_ratings);
$this->_add_type_text('favorite_artist', T_('Favorites (Artist)'), $t_ratings);
$users = $this->getUserRepository()->getValidArray();
$this->_add_type_select('other_user', T_('Another User'), 'user_numeric', $users, $t_ratings);
$this->_add_type_select('other_user_album', T_('Another User (Album)'), 'user_numeric', $users, $t_ratings);
$this->_add_type_select('other_user_artist', T_('Another User (Artist)'), 'user_numeric', $users, $t_ratings);
}
$t_play_data = T_('Play History');
/* HINT: Number of times object has been played */
$this->_add_type_numeric('played_times', T_('# Played'), 'numeric', $t_play_data);
/* HINT: Number of times object has been skipped */
$this->_add_type_numeric('skipped_times', T_('# Skipped'), 'numeric', $t_play_data);
/* HINT: Number of times object has been played OR skipped */
$this->_add_type_numeric('played_or_skipped_times', T_('# Played or Skipped'), 'numeric', $t_play_data);
/* HINT: Percentage of (Times Played / Times skipped) * 100 */
$this->_add_type_numeric('play_skip_ratio', T_('Played/Skipped ratio'), 'numeric', $t_play_data);
$this->_add_type_numeric('last_play', T_('My Last Play'), 'days', $t_play_data);
$this->_add_type_numeric('last_skip', T_('My Last Skip'), 'days', $t_play_data);
$this->_add_type_numeric('last_play_or_skip', T_('My Last Play or Skip'), 'days', $t_play_data);
$this->_add_type_boolean('played', T_('Played'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayed', T_('Played by Me'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayedalbum', T_('Played by Me (Album)'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayedartist', T_('Played by Me (Artist)'), 'boolean', $t_play_data);
$this->_add_type_numeric('recent_played', T_('Recently played'), 'recent_played', $t_play_data);
$t_genre = T_('Genre');
$this->_add_type_text('genre', $t_genre, $t_genre);
$this->_add_type_text('album_genre', T_('Album Genre'), $t_genre);
$this->_add_type_text('artist_genre', T_('Artist Genre'), $t_genre);
$this->_add_type_boolean('no_genre', T_('No Genre'), 'is_true', $t_genre);
$t_playlists = T_('Playlists');
$playlists = Playlist::get_playlist_array($this->user);
if (!empty($playlists)) {
$this->_add_type_select('playlist', T_('Playlist'), 'boolean_subsearch', $playlists, $t_playlists);
}
$playlists = self::get_search_array($this->user);
if (!empty($playlists)) {
$this->_add_type_select('smartplaylist', T_('Smart Playlist'), 'boolean_subsearch', $playlists, $t_playlists);
}
$this->_add_type_text('playlist_name', T_('Playlist Name'), $t_playlists);
$t_file_data = T_('File Data');
$this->_add_type_text('file', T_('Filename'), $t_file_data);
$bitrate_array = array(
'32',
'40',
'48',
'56',
'64',
'80',
'96',
'112',
'128',
'160',
'192',
'224',
'256',
'320',
'640',
'1280'
);
$this->_add_type_select('bitrate', T_('Bitrate'), 'numeric', $bitrate_array, $t_file_data);
$this->_add_type_date('added', T_('Added'), $t_file_data);
$this->_add_type_date('updated', T_('Updated'), $t_file_data);
if (AmpConfig::get('licensing')) {
$licenses = iterator_to_array(
$this->getLicenseRepository()->getList()
);
$this->_add_type_select('license', T_('Music License'), 'boolean_numeric', $licenses, $t_file_data);
}
$this->_add_type_numeric('recent_added', T_('Recently added'), 'recent_added', $t_file_data);
$this->_add_type_numeric('recent_updated', T_('Recently updated'), 'recent_updated', $t_file_data);
$this->_add_type_boolean('possible_duplicate', T_('Possible Duplicate'), 'is_true', $t_file_data);
$this->_add_type_boolean('duplicate_tracks', T_('Duplicate Album Tracks'), 'is_true', $t_file_data);
$this->_add_type_boolean('possible_duplicate_album', T_('Possible Duplicate Albums'), 'is_true', $t_file_data);
$this->_add_type_boolean('orphaned_album', T_('Orphaned Album'), 'is_true', $t_file_data);
$catalogs = array();
foreach (Catalog::get_catalogs('music', $this->user) as $catid) {
$catalog = Catalog::create_from_id($catid);
if ($catalog === null) {
break;
}
$catalogs[$catid] = $catalog->name;
}
if (!empty($catalogs)) {
$this->_add_type_select('catalog', T_('Catalog'), 'boolean_numeric', $catalogs, $t_file_data);
}
$t_musicbrainz = T_('MusicBrainz');
$this->_add_type_text('mbid', T_('MusicBrainz ID'), $t_musicbrainz);
$this->_add_type_text('mbid_album', T_('MusicBrainz ID (Album)'), $t_musicbrainz);
$this->_add_type_text('mbid_artist', T_('MusicBrainz ID (Artist)'), $t_musicbrainz);
$t_metadata = T_('Metadata');
if (AmpConfig::get('enable_custom_metadata')) {
$this->types[] = array(
'name' => 'metadata',
'label' => $t_metadata,
'type' => 'multiple',
'subtypes' => iterator_to_array($this->getMetadataFieldRepository()->getPropertyList()),
'widget' => array('subtypes', array('input', 'text')),
'title' => $t_metadata
);
}
}
/**
* _set_types_artist
*
* this is where all the available rules for artists are defined
*/
private function _set_types_artist(): void
{
$t_artist_data = T_('Artist Data');
$this->_add_type_text('title', T_('Name'), $t_artist_data);
$this->_add_type_text('album', T_('Album Title'), $t_artist_data);
$this->_add_type_text('song', T_('Song Title'), $t_artist_data);
$this->_add_type_text('summary', T_('Summary'), $t_artist_data);
$this->_add_type_numeric('yearformed', T_('Year Formed'), 'numeric', $t_artist_data);
$this->_add_type_text('placeformed', T_('Place Formed'), $t_artist_data);
$this->_add_type_numeric('time', T_('Length (in minutes)'), 'numeric', $t_artist_data);
$this->_add_type_numeric('album_count', T_('Album Count'), 'numeric', $t_artist_data);
$this->_add_type_numeric('song_count', T_('Song Count'), 'numeric', $t_artist_data);
$t_ratings = T_('Ratings');
if (AmpConfig::get('ratings')) {
$this->_add_type_select('myrating', T_('My Rating'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('rating', T_('Rating (Average)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('songrating', T_('My Rating (Song)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('albumrating', T_('My Rating (Album)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_text('favorite', T_('Favorites'), $t_ratings);
$users = $this->getUserRepository()->getValidArray();
$this->_add_type_select('other_user', T_('Another User'), 'user_numeric', $users, $t_ratings);
}
$t_play_data = T_('Play History');
/* HINT: Number of times object has been played */
$this->_add_type_numeric('played_times', T_('# Played'), 'numeric', $t_play_data);
$this->_add_type_numeric('last_play', T_('My Last Play'), 'days', $t_play_data);
$this->_add_type_numeric('last_skip', T_('My Last Skip'), 'days', $t_play_data);
$this->_add_type_numeric('last_play_or_skip', T_('My Last Play or Skip'), 'days', $t_play_data);
$this->_add_type_boolean('played', T_('Played'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayed', T_('Played by Me'), 'boolean', $t_play_data);
$this->_add_type_numeric('recent_played', T_('Recently played'), 'recent_played', $t_play_data);
$t_genre = T_('Genre');
$this->_add_type_text('genre', $t_genre, $t_genre);
$this->_add_type_text('song_genre', T_('Song Genre'), $t_genre);
$this->_add_type_boolean('no_genre', T_('No Genre'), 'is_true', $t_genre);
$t_playlists = T_('Playlists');
$playlists = Playlist::get_playlist_array($this->user);
if (!empty($playlists)) {
$this->_add_type_select('playlist', T_('Playlist'), 'boolean_subsearch', $playlists, $t_playlists);
}
$this->_add_type_text('playlist_name', T_('Playlist Name'), $t_playlists);
$t_file_data = T_('File Data');
$this->_add_type_text('file', T_('Filename'), $t_file_data);
$this->_add_type_boolean('has_image', T_('Local Image'), 'boolean', $t_file_data);
$this->_add_type_numeric('image_width', T_('Image Width'), 'numeric', $t_file_data);
$this->_add_type_numeric('image_height', T_('Image Height'), 'numeric', $t_file_data);
$this->_add_type_boolean('possible_duplicate', T_('Possible Duplicate'), 'is_true', $t_file_data);
$this->_add_type_boolean('possible_duplicate_album', T_('Possible Duplicate Albums'), 'is_true', $t_file_data);
$catalogs = array();
foreach (Catalog::get_catalogs('music', $this->user) as $catid) {
$catalog = Catalog::create_from_id($catid);
if ($catalog === null) {
break;
}
$catalogs[$catid] = $catalog->name;
}
if (!empty($catalogs)) {
$this->_add_type_select('catalog', T_('Catalog'), 'boolean_numeric', $catalogs, $t_file_data);
}
$t_musicbrainz = T_('MusicBrainz');
$this->_add_type_text('mbid', T_('MusicBrainz ID'), $t_musicbrainz);
$this->_add_type_text('mbid_album', T_('MusicBrainz ID (Album)'), $t_musicbrainz);
$this->_add_type_text('mbid_song', T_('MusicBrainz ID (Song)'), $t_musicbrainz);
}
/**
* _set_types_album
*
* this is where all the available rules for albums are defined
*/
private function _set_types_album(): void
{
$t_album_data = T_('Album Data');
$this->_add_type_text('title', T_('Title'), $t_album_data);
$this->_add_type_text('artist', T_('Album Artist'), $t_album_data);
$this->_add_type_text('song_artist', T_('Song Artist'), $t_album_data);
$this->_add_type_text('song', T_('Song Title'), $t_album_data);
$this->_add_type_numeric('year', T_('Year'), 'numeric', $t_album_data);
$this->_add_type_numeric('original_year', T_('Original Year'), 'numeric', $t_album_data);
$this->_add_type_numeric('time', T_('Length (in minutes)'), 'numeric', $t_album_data);
$this->_add_type_text('release_type', T_('Release Type'), $t_album_data);
$this->_add_type_text('release_status', T_('Release Status'), $t_album_data);
$this->_add_type_text('version', T_('Release Comment'), $t_album_data);
$this->_add_type_text('barcode', T_('Barcode'), $t_album_data);
$this->_add_type_text('catalog_number', T_('Catalog Number'), $t_album_data);
$this->_add_type_numeric('song_count', T_('Song Count'), 'numeric', $t_album_data);
$t_ratings = T_('Ratings');
if (AmpConfig::get('ratings')) {
$this->_add_type_select('myrating', T_('My Rating'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('rating', T_('Rating (Average)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('songrating', T_('My Rating (Song)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_select('artistrating', T_('My Rating (Artist)'), 'numeric', $this->stars, $t_ratings);
$this->_add_type_text('favorite', T_('Favorites'), $t_ratings);
$users = $this->getUserRepository()->getValidArray();
$this->_add_type_select('other_user', T_('Another User'), 'user_numeric', $users, $t_ratings);
}
$t_play_data = T_('Play History');
/* HINT: Number of times object has been played */
$this->_add_type_numeric('played_times', T_('# Played'), 'numeric', $t_play_data);
$this->_add_type_numeric('last_play', T_('My Last Play'), 'days', $t_play_data);
$this->_add_type_numeric('last_skip', T_('My Last Skip'), 'days', $t_play_data);
$this->_add_type_numeric('last_play_or_skip', T_('My Last Play or Skip'), 'days', $t_play_data);
$this->_add_type_boolean('played', T_('Played'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayed', T_('Played by Me'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayedartist', T_('Played by Me (Artist)'), 'boolean', $t_play_data);
$this->_add_type_numeric('recent_played', T_('Recently played'), 'recent_played', $t_play_data);
$t_genre = T_('Genre');
$this->_add_type_text('genre', $t_genre, $t_genre);
$this->_add_type_text('song_genre', T_('Song Genre'), $t_genre);
$this->_add_type_boolean('no_genre', T_('No Genre'), 'is_true', $t_genre);
$t_playlists = T_('Playlists');
$playlists = Playlist::get_playlist_array($this->user);
if (!empty($playlists)) {
$this->_add_type_select('playlist', T_('Playlist'), 'boolean_subsearch', $playlists, $t_playlists);
}
$playlists = self::get_search_array($this->user);
if (!empty($playlists)) {
$this->_add_type_select('smartplaylist', T_('Smart Playlist'), 'boolean_subsearch', $playlists, $t_playlists);
}
$this->_add_type_text('playlist_name', T_('Playlist Name'), $t_playlists);
$t_file_data = T_('File Data');
$this->_add_type_text('file', T_('Filename'), $t_file_data);
$this->_add_type_boolean('has_image', T_('Local Image'), 'boolean', $t_file_data);
$this->_add_type_numeric('image_width', T_('Image Width'), 'numeric', $t_file_data);
$this->_add_type_numeric('image_height', T_('Image Height'), 'numeric', $t_file_data);
$this->_add_type_boolean('possible_duplicate', T_('Possible Duplicate'), 'is_true', $t_file_data);
$this->_add_type_boolean('duplicate_tracks', T_('Duplicate Album Tracks'), 'is_true', $t_file_data);
$this->_add_type_boolean('duplicate_mbid_group', T_('Duplicate MusicBrainz Release Group'), 'is_true', $t_file_data);
$this->_add_type_numeric('recent_added', T_('Recently added'), 'recent_added', $t_file_data);
$catalogs = array();
foreach (Catalog::get_catalogs('music', $this->user) as $catid) {
$catalog = Catalog::create_from_id($catid);
if ($catalog === null) {
break;
}
$catalogs[$catid] = $catalog->name;
}
if (!empty($catalogs)) {
$this->_add_type_select('catalog', T_('Catalog'), 'boolean_numeric', $catalogs, $t_file_data);
}
$t_musicbrainz = T_('MusicBrainz');
$this->_add_type_text('mbid', T_('MusicBrainz ID'), $t_musicbrainz);
$this->_add_type_text('mbid_artist', T_('MusicBrainz ID (Artist)'), $t_musicbrainz);
$this->_add_type_text('mbid_song', T_('MusicBrainz ID (Song)'), $t_musicbrainz);
}
/**
* _set_types_video
*
* this is where all the available rules for videos are defined
*/
private function _set_types_video(): void
{
$this->_add_type_text('file', T_('Filename'));
}
/**
* _set_types_playlist
*
* this is where all the available rules for playlists are defined
*/
private function _set_types_playlist(): void
{
$t_playlist = T_('Playlist');
$this->_add_type_text('title', T_('Name'), $t_playlist);
$playlist_types = array(
0 => T_('public'),
1 => T_('private')
);
$this->_add_type_select('type', T_('Type'), 'boolean_numeric', $playlist_types, $t_playlist);
$users = $this->getUserRepository()->getValidArray();
$this->_add_type_select('owner', T_('Owner'), 'user_numeric', $users, $t_playlist);
}
/**
* _set_types_podcast
*
* this is where all the available rules for podcasts are defined
*/
private function _set_types_podcast(): void
{
$t_podcasts = T_('Podcast');
$this->_add_type_text('title', T_('Name'), $t_podcasts);
$this->_add_type_numeric('episode_count', T_('Episode Count'), 'numeric', $t_podcasts);
$t_podcast_episodes = T_('Podcast Episodes');
$this->_add_type_text('podcast_episode', T_('Podcast Episode'), $t_podcast_episodes);
$episode_states = array(
0 => T_('skipped'),
1 => T_('pending'),
2 => T_('completed')
);
$this->_add_type_select('status', T_('Status'), 'boolean_numeric', $episode_states, $t_podcast_episodes);
$this->_add_type_numeric('time', T_('Length (in minutes)'), 'numeric', $t_podcast_episodes);
$t_play_data = T_('Play History');
/* HINT: Number of times object has been played */
$this->_add_type_numeric('played_times', T_('# Played'), 'numeric', $t_play_data);
/* HINT: Number of times object has been skipped */
$this->_add_type_numeric('skipped_times', T_('# Skipped'), 'numeric', $t_play_data);
/* HINT: Number of times object has been played OR skipped */
$this->_add_type_numeric('played_or_skipped_times', T_('# Played or Skipped'), 'numeric', $t_play_data);
$this->_add_type_numeric('last_play', T_('My Last Play'), 'days', $t_play_data);
$this->_add_type_numeric('last_skip', T_('My Last Skip'), 'days', $t_play_data);
$this->_add_type_numeric('last_play_or_skip', T_('My Last Play or Skip'), 'days', $t_play_data);
$this->_add_type_boolean('played', T_('Played'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayed', T_('Played by Me'), 'boolean', $t_play_data);
$this->_add_type_numeric('recent_played', T_('Recently played'), 'recent_played', $t_play_data);
$t_file_data = T_('File Data');
$this->_add_type_text('file', T_('Filename'), $t_file_data);
$this->_add_type_date('pubdate', T_('Publication Date'), $t_file_data);
$this->_add_type_date('added', T_('Added'), $t_file_data);
}
/**
* _set_types_podcast_episode
*
* this is where all the available rules for podcast_episodes are defined
*/
private function _set_types_podcast_episode(): void
{
$t_podcast_episodes = T_('Podcast Episode');
$this->_add_type_text('title', T_('Name'), $t_podcast_episodes);
$this->_add_type_text('podcast', T_('Podcast'), $t_podcast_episodes);
$episode_states = array(
0 => T_('skipped'),
1 => T_('pending'),
2 => T_('completed')
);
$this->_add_type_select('status', T_('Status'), 'boolean_numeric', $episode_states, $t_podcast_episodes);
$this->_add_type_numeric('time', T_('Length (in minutes)'), 'numeric', $t_podcast_episodes);
$t_play_data = T_('Play History');
/* HINT: Number of times object has been played */
$this->_add_type_numeric('played_times', T_('# Played'), 'numeric', $t_play_data);
/* HINT: Number of times object has been skipped */
$this->_add_type_numeric('skipped_times', T_('# Skipped'), 'numeric', $t_play_data);
/* HINT: Number of times object has been played OR skipped */
$this->_add_type_numeric('played_or_skipped_times', T_('# Played or Skipped'), 'numeric', $t_play_data);
$this->_add_type_numeric('last_play', T_('My Last Play'), 'days', $t_play_data);
$this->_add_type_numeric('last_skip', T_('My Last Skip'), 'days', $t_play_data);
$this->_add_type_numeric('last_play_or_skip', T_('My Last Play or Skip'), 'days', $t_play_data);
$this->_add_type_boolean('played', T_('Played'), 'boolean', $t_play_data);
$this->_add_type_boolean('myplayed', T_('Played by Me'), 'boolean', $t_play_data);
$this->_add_type_numeric('recent_played', T_('Recently played'), 'recent_played', $t_play_data);
$t_file_data = T_('File Data');
$this->_add_type_text('file', T_('Filename'), $t_file_data);
$this->_add_type_date('pubdate', T_('Publication Date'), $t_file_data);
$this->_add_type_date('added', T_('Added'), $t_file_data);
}
/**
* _set_types_label
*
* this is where all the available rules for labels are defined
*/
private function _set_types_label(): void
{
$t_label = T_('Label');
$this->_add_type_text('title', T_('Name'), $t_label);
$this->_add_type_text('category', T_('Category'), $t_label);
}
/**
* _set_types_user
*
* this is where all the available rules for users are defined
*/
private function _set_types_user(): void
{
$this->_add_type_text('username', T_('Username'));
}
/**
* _set_types_tag
*
* this is where all the available rules for Genres are defined
*/
private function _set_types_tag(): void
{
$this->_add_type_text('title', T_('Genre'));
}
/**
* _filter_request
*
* Sanitizes raw search data
* @param array $data
* @return array
*/
private static function _filter_request($data): array
{
$request = array();
foreach ($data as $key => $value) {
$prefix = substr($key, 0, 4);
$value = (string)$value;
if ($prefix == 'rule' && strlen((string)$value)) {
$request[$key] = Dba::escape($value);
}
}
// Figure out if they want an AND based search or an OR based search
$operator = $data['operator'] ?? '';
switch (strtolower($operator)) {
case 'or':
$request['operator'] = 'OR';
break;
case 'and':
default:
$request['operator'] = 'AND';
break;
}
if (array_key_exists('limit', $data)) {
$request['limit'] = $data['limit'];
}
if (array_key_exists('offset', $data)) {
$request['offset'] = $data['offset'];
}
if (array_key_exists('random', $data)) {
$request['random'] = (int)$data['random'];
}
// Verify the type
$search_type = strtolower($data['type'] ?? '');
//Search::VALID_TYPES = array('song', 'album', 'album_disk', 'song_artist', 'album_artist', 'artist', 'label', 'playlist', 'podcast', 'podcast_episode', 'tag', 'user', 'video')
switch ($search_type) {
case 'song':
case 'album':
case 'album_disk':
case 'song_artist':
case 'album_artist':
case 'artist':
case 'label':
case 'playlist':
case 'podcast':
case 'podcast_episode':
case 'tag': // for Genres
case 'user':
case 'video':
$request['type'] = $search_type;
break;
case 'genre':
$request['type'] = 'tag';
break;
default:
debug_event(self::class, "_filter_request: search_type '$search_type' reset to: song", 5);
$request['type'] = 'song';
break;
}
return $request;
}
/**
* get_searches
*
* Return the IDs of all saved searches accessible by the current user.
* @param int $user_id
* @return array
*/
public static function get_searches($user_id = null): array
{
if ($user_id === null) {
$user = Core::get_global('user');
$user_id = $user->id ?: 0;
}
$key = 'searches';
if (parent::is_cached($key, $user_id)) {
return parent::get_from_cache($key, $user_id);
}
$is_admin = (Access::check('interface', 100, $user_id) || $user_id == -1);
$sql = "SELECT `id`, `name` FROM `search` ";
$params = array();
if (!$is_admin) {
$sql .= "WHERE (`user` = ? OR `type` = 'public') ";
$params[] = $user_id;
}
$sql .= "ORDER BY `name`";
$db_results = Dba::read($sql, $params);
$results = array();
while ($row = Dba::fetch_assoc($db_results)) {
$results[$row['id']] = $row['name'];
}
parent::add_to_cache($key, $user_id, $results);
return $results;
}
/**
* get_search_array
* Returns a list of searches accessible by the user with formatted name.
* @param int|null $user_id
* @return array
*/
public static function get_search_array($user_id = 0): array
{
if ($user_id === 0) {
$user = Core::get_global('user');
$user_id = $user->id ?? 0;
}
$key = 'searcharray';
if (parent::is_cached($key, $user_id)) {
return parent::get_from_cache($key, $user_id);
}
$is_admin = (Access::check('interface', 100, $user_id) || $user_id == -1);
$sql = "SELECT `id`, IF(`user` = ?, `name`, CONCAT(`name`, ' (', `username`, ')')) AS `name` FROM `search` ";
$params = array($user_id);
if (!$is_admin) {
$sql .= "WHERE (`user` = ? OR `type` = 'public') ";
$params[] = $user_id;
}
$sql .= "ORDER BY `name`";
//debug_event(self::class, 'get_searches query: ' . $sql . "\n" . print_r($params, true), 5);
$db_results = Dba::read($sql, $params);
$results = array();
while ($row = Dba::fetch_assoc($db_results)) {
$results[$row['id']] = $row['name'];
}
parent::add_to_cache($key, $user_id, $results);
return $results;
}
/**
* run
*
* This function actually runs the search and returns an array of the
* results.
* @param array $data
* @param User $user
* @param bool $require_rules // require a valid rule to return search items (instead of returning all items)
* @return int[]
*/
public static function run($data, $user = null, $require_rules = false): array
{
$limit = (int)($data['limit'] ?? 0);
$offset = (int)($data['offset'] ?? 0);
$random = ((int)($data['random'] ?? 0) > 0) ? 1 : 0;
$search = new Search(0, $data['type'], $user);
$search->set_rules($data);
// Generate BASE SQL
$limit_sql = "";
if ($limit > 0) {
$limit_sql = ' LIMIT ';
if ($offset > 0) {
$limit_sql .= $offset . ", ";
}
$limit_sql .= $limit;
}
$search_info = $search->to_sql();
if ($require_rules && empty($search_info['where'])) {
debug_event(self::class, 'require_rules: No rules were set on this search', 5);
return array();
}
$sql = $search_info['base'] . ' ' . $search_info['table_sql'];
if (!empty($search_info['where_sql'])) {
$sql .= ' WHERE ' . $search_info['where_sql'];
}
if (!empty($search_info['group_sql'])) {
$sql .= ' GROUP BY ' . $search_info['group_sql'];
if (!empty($search_info['having_sql'])) {
$sql .= ' HAVING ' . $search_info['having_sql'];
}
}
$sql .= ($random > 0) ? " ORDER BY RAND()" : " ORDER BY " . $search->order_by;
$sql .= ' ' . $limit_sql;
$sql = trim((string)$sql);
//debug_event(self::class, 'SQL run: ' . $sql . "\n" . print_r($search_info['parameters'], true), 5);
$db_results = Dba::read($sql, $search_info['parameters']);
$results = array();
while ($row = Dba::fetch_assoc($db_results)) {
$results[] = (int)$row['id'];
}
return $results;
}
/**
* delete
*
* Does what it says on the tin.
*/
public function delete(): bool
{
$sql = "DELETE FROM `search` WHERE `id` = ?";
Dba::write($sql, array($this->id));
Catalog::count_table('search');
return true;
}
/**
* format
* Gussy up the data
*
* @param bool $details
*/
public function format($details = true): void
{
parent::format($details);
}
/**
* get_items
*
* Return an array of the items output by our search
* (part of the playlist interface).
* @return array
*/
public function get_items(): array
{
$results = array();
if ($this->isNew()) {
return $results;
}
$sqltbl = $this->to_sql();
$sql = $sqltbl['base'] . ' ' . $sqltbl['table_sql'];
if (!empty($sqltbl['where_sql'])) {
$sql .= ' WHERE ' . $sqltbl['where_sql'];
}
if (!empty($sqltbl['group_sql'])) {
$sql .= ' GROUP BY ' . $sqltbl['group_sql'];
}
if (!empty($sqltbl['having_sql'])) {
$sql .= ' HAVING ' . $sqltbl['having_sql'];
}
$sql .= ($this->random > 0) ? " ORDER BY RAND()" : " ORDER BY " . $this->order_by;
if ($this->limit > 0) {
$sql .= " LIMIT " . (string)($this->limit);
}
//debug_event(self::class, 'SQL get_items: ' . $sql . "\n" . print_r($sqltbl['parameters'], true), 5);
$count = 1;
$db_results = Dba::read($sql, $sqltbl['parameters']);
while ($row = Dba::fetch_assoc($db_results)) {
$results[] = array(
'object_id' => $row['id'],
'object_type' => $this->objectType,
'track' => $count++,
'track_id' => $row['id'],
);
}
$this->date = time();
$this->set_last(count($results), 'last_count');
$this->set_last(self::get_total_duration($results), 'last_duration');
return $results;
}
/**
* get_subsearch
*
* get SQL for an item subsearch
* @return array
*/
public function get_subsearch($table): array
{
$sqltbl = $this->to_sql();
$sql = "SELECT DISTINCT(`$table`.`id`) FROM `$table` " . $sqltbl['table_sql'];
if (!empty($sqltbl['where_sql'])) {
$sql .= ' WHERE ' . $sqltbl['where_sql'];
}
if (!empty($sqltbl['group_sql'])) {
$sql .= ' GROUP BY ' . $sqltbl['group_sql'];
}
if (!empty($sqltbl['having_sql'])) {
$sql .= ' HAVING ' . $sqltbl['having_sql'];
}
//$sql .= ($this->random > 0) ? " ORDER BY RAND()" : " ORDER BY " . $this->order_by; // MYSQL would want file for order by
if ($this->limit > 0) {
$sql .= " LIMIT " . (string)($this->limit);
}
//debug_event(self::class, 'SQL get_subsearch: ' . $sql . "\n" . print_r($sqltbl['parameters'], true), 5);
return array(
'sql' => $sql,
'parameters' => $sqltbl['parameters']
);
}
/**
* set_last
*
* @param int $count
* @param string $column
*/
private function set_last($count, $column): void
{
if (in_array($column, array('last_count', 'last_duration'))) {
$search_id = Dba::escape($this->id);
$sql = "UPDATE `search` SET `" . Dba::escape($column) . "` = ? WHERE `id` = ?";
Dba::write($sql, array($count, $search_id));
}
}
/**
* get_random_items
*
* Returns a randomly sorted array (with an optional limit) of the items
* output by our search (part of the playlist interface)
* @param string|null $limit
* @return array
*/
public function get_random_items($limit = ''): array
{
$results = array();
$sqltbl = $this->to_sql();
$sql = $sqltbl['base'] . ' ' . $sqltbl['table_sql'];
if (!empty($sqltbl['where_sql'])) {
$sql .= ' WHERE ' . $sqltbl['where_sql'];
}
$rating_filter = AmpConfig::get_rating_filter();
if ($rating_filter > 0 && $rating_filter <= 5 && !empty(Core::get_global('user')) && Core::get_global('user')->id > 0) {
$user_id = Core::get_global('user')->id;
$sql .= (empty($sqltbl['where_sql']))
? " WHERE "
: " AND ";
$sql .= "`" . $this->objectType . "`.`id` NOT IN (SELECT `object_id` FROM `rating` WHERE `rating`.`object_type` = '" . $this->objectType . "' AND `rating`.`rating` <=$rating_filter AND `rating`.`user` = $user_id)";
}
if (!empty($sqltbl['group_sql'])) {
$sql .= ' GROUP BY ' . $sqltbl['group_sql'];
}
if (!empty($sqltbl['having_sql'])) {
$sql .= ' HAVING ' . $sqltbl['having_sql'];
}
$sql .= " ORDER BY RAND()";
$sql .= (!empty($limit))
? " LIMIT " . $limit
: "";
//debug_event(self::class, 'SQL get_random_items: ' . $sql . "\n" . print_r($sqltbl['parameters'], true), 5);
$db_results = Dba::read($sql, $sqltbl['parameters']);
while ($row = Dba::fetch_assoc($db_results)) {
$results[] = array(
'object_id' => $row['id'],
'object_type' => $this->objectType
);
}
return $results;
}
/**
* get_songs
* This is called by the batch script, because we can't pass in Dynamic objects they pulled once and then their
* target song.id is pushed into the array
* @return int[]
*/
public function get_songs(): array
{
$results = array();
if ($this->isNew()) {
return $results;
}
$sqltbl = $this->to_sql();
$sql = $sqltbl['base'] . ' ' . $sqltbl['table_sql'];
if (!empty($sqltbl['where_sql'])) {
$sql .= ' WHERE ' . $sqltbl['where_sql'];
}
if (!empty($sqltbl['group_sql'])) {
$sql .= ' GROUP BY ' . $sqltbl['group_sql'];
}
if (!empty($sqltbl['having_sql'])) {
$sql .= ' HAVING ' . $sqltbl['having_sql'];
}
$sql .= ($this->random > 0) ? " ORDER BY RAND()" : " ORDER BY " . $this->order_by;
if ($this->limit > 0) {
$sql .= " LIMIT " . (string)($this->limit);
}
//debug_event(self::class, 'SQL get_songs: ' . $sql . "\n" . print_r($sqltbl['parameters'], true), 5);
$db_results = Dba::read($sql, $sqltbl['parameters']);
while ($row = Dba::fetch_assoc($db_results)) {
$results[] = (int)$row['id'];
}
return $results;
}
/**
* get_total_duration
* Get the total duration of all songs.
* @param array $songs
*/
public static function get_total_duration($songs): int
{
$song_ids = array();
foreach ($songs as $objects) {
$song_ids[] = (string)$objects['object_id'];
}
$idlist = '(' . implode(',', $song_ids) . ')';
if ($idlist == '()') {
return 0;
}
$sql = "SELECT SUM(`time`) FROM `song` WHERE `id` IN $idlist";
$db_results = Dba::read($sql);
$row = Dba::fetch_row($db_results);
return (int)($row[0] ?? 0);
}
/**
* _get_rule_name
*
* Iterate over $this->types to validate the rule name and return the rule type
* (text, date, etc)
* @param string $name
*/
private function _get_rule_name($name): string
{
// check that the rule you sent is not an alias (needed for pulling details from the rule)
switch ($this->objectType) {
case 'song':
switch ($name) {
case 'name':
case 'song':
case 'song_title':
$name = 'title';
break;
case 'album_title':
$name = 'album';
break;
case 'album_artist_title':
$name = 'album_artist';
break;
case 'song_artist_title':
$name = 'song_artist';
break;
case 'tag':
case 'song_tag':
case 'song_genre':
$name = 'genre';
break;
case 'album_tag':
$name = 'album_genre';
break;
case 'artist_tag':
$name = 'artist_genre';
break;
case 'no_tag':
$name = 'no_genre';
break;
case 'mbid_song':
$name = 'mbid';
break;
}
break;
case 'album':
case 'album_disk':
switch ($name) {
case 'name':
case 'album':
case 'album_title':
$name = 'title';
break;
case 'song_title':
$name = 'song';
break;
case 'album_artist':
case 'album_artist_title':
case 'artist_title':
$name = 'artist';
break;
case 'tag':
case 'album_tag':
case 'album_genre':
$name = 'genre';
break;
case 'song_tag':
$name = 'song_genre';
break;
case 'no_tag':
$name = 'no_genre';
break;
case 'mbid_album':
$name = 'mbid';
break;
case 'possible_duplicate_album':
$name = 'possible_duplicate';
break;
case 'release_comment':
case 'subtitle':
$name = 'version';
break;
}
break;
case 'artist':
switch ($name) {
case 'name':
case 'artist':
case 'artist_title':
$name = 'title';
break;
case 'album_title':
$name = 'album';
break;
case 'song_title':
$name = 'song';
break;
case 'tag':
case 'artist_tag':
case 'artist_genre':
$name = 'genre';
break;
case 'song_tag':
$name = 'song_genre';
break;
case 'no_tag':
$name = 'no_genre';
break;
case 'mbid_artist':
$name = 'mbid';
break;
}
break;
case 'podcast':
switch ($name) {
case 'name':
$name = 'title';
break;
case 'podcast_episode_title':
$name = 'podcast_episode';
break;
case 'status':
$name = 'state';
break;
}
break;
case 'podcast_episode':
switch ($name) {
case 'name':
$name = 'title';
break;
case 'podcast_title':
$name = 'podcast';
break;
case 'status':
$name = 'state';
break;
}
break;
case 'genre':
case 'tag':
case 'label':
case 'playlist':
switch ($name) {
case 'name':
$name = 'title';
break;
}
break;
}
//debug_event(self::class, '__get_rule_name: ' . $name, 5);
return $name;
}
/**
* get_rule_type
*
* Iterate over $this->types to validate the rule name and return the rule type
* (text, date, etc)
* @param string $name
*/
public function get_rule_type($name): ?string
{
//debug_event(self::class, 'get_rule_type: ' . $name, 5);
foreach ($this->types as $type) {
if ($type['name'] == $name) {
return $type['type'];
}
}
return null;
}
/**
* set_rules
*
* Takes an array of sanitized search data from the form and generates our real array from it.
* @param array $data
*/
public function set_rules($data): void
{
if (isset($data['playlist_name'])) {
$this->name = (string)$data['playlist_name'];
}
if (isset($data['playlist_type'])) {
$this->type = (string)$data['playlist_type'];
}
// check that a limit or random flag and operator have been sent
$this->random = (isset($data['random'])) ? (int)$data['random'] : $this->random;
$this->limit = (isset($data['limit'])) ? (int) $data['limit'] : $this->limit;
// the rules array needs to be filtered to just have rules
$data = self::_filter_request($data);
$this->rules = array();
$user_rules = array();
$this->logic_operator = $data['operator'] ?? 'AND';
// match the numeric rules you send (e.g. rule_1, rule_6000)
foreach ($data as $rule => $value) {
if (preg_match('/^rule_(\d+)$/', $rule, $ruleID)) {
$user_rules[] = $ruleID[1];
}
}
// get the data for each rule group the user sent
foreach ($user_rules as $ruleID) {
$rule_name = $this->_get_rule_name($data["rule_" . $ruleID]);
$rule_type = $this->get_rule_type($rule_name);
if ($rule_type === null) {
continue;
}
$rule_input = (string)($data['rule_' . $ruleID . '_input'] ?? '');
$rule_operator = $this->basetypes[$rule_type][$data['rule_' . $ruleID . '_operator']]['name'] ?? '';
// keep vertical bar in regular expression
$is_regex = in_array($rule_operator, ['regexp', 'notregexp']);
if ($is_regex) {
$rule_input = str_replace("|", "\0", $rule_input);
}
// attach the rules to the search
foreach (explode('|', $rule_input) as $input) {
$this->rules[] = array(
$rule_name, // name
$rule_operator, // operator
($is_regex) ? str_replace("\0", "|", $input) : $input, // input
$data['rule_' . $ruleID . '_subtype'] ?? null, // subtype
);
}
}
}
/**
* create
*
* Save this search to the database for use as a smart playlist
*/
public function create(): ?string
{
$user = Core::get_global('user');
// Make sure we have a unique name
if (empty($this->name)) {
$this->name = $user->username . ' - ' . get_datetime(time());
}
$sql = "SELECT `id` FROM `search` WHERE `name` = ? AND `user` = ? AND `type` = ?;";
$db_results = Dba::read($sql, array($this->name, $user->id, $this->type));
if (Dba::num_rows($db_results)) {
$this->name .= uniqid('', true);
}
$time = time();
$sql = "INSERT INTO `search` (`name`, `type`, `user`, `username`, `rules`, `logic_operator`, `random`, `limit`, `date`, `last_update`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
Dba::write($sql, array(
$this->name,
$this->type,
$user->id,
$user->username,
json_encode($this->rules),
$this->logic_operator,
($this->random > 0) ? 1 : 0,
$this->limit,
$time,
$time
));
$insert_id = Dba::insert_id();
if (!$insert_id) {
return null;
}
$this->id = (int)$insert_id;
Catalog::count_table('search');
return $insert_id;
}
/**
* to_js
*
* Outputs the javascript necessary to re-show the current set of rules.
*/
public function to_js(): string
{
$javascript = "";
foreach ($this->rules as $rule) {
// @see search.js SearchRow.add(ruleType, operator, input, subtype)
$javascript .= '<script>' . 'SearchRow.add("' . scrub_out($rule[0]) . '","' . scrub_out($rule[1]) . '","' . scrub_out($rule[2]) . '", "' . scrub_out($rule[3]) . '"); </script>';
}
return $javascript;
}
/**
* to_sql
*
* Call the appropriate real function.
* @return array
*/
public function to_sql(): array
{
return $this->searchType->getSql($this);
}
/**
* update
*
* This function updates the saved search with the current settings.
* @param array|null $data
*/
public function update(array $data = null): int
{
if ($data && is_array($data)) {
$this->name = $data['name'] ?? $this->name;
$this->type = $data['pl_type'] ?? $this->type;
$this->user = $data['pl_user'] ?? $this->user;
$this->random = $data['random'] ?? $this->random;
$this->limit = $data['limit'] ?? $this->limit;
}
$this->username = User::get_username((int)$this->user);
if ($this->isNew()) {
return 0;
}
$sql = "UPDATE `search` SET `name` = ?, `type` = ?, `user` = ?, `username` = ?, `rules` = ?, `logic_operator` = ?, `random` = ?, `limit` = ?, `last_update` = ? WHERE `id` = ?";
Dba::write($sql, array(
$this->name,
$this->type,
$this->user,
$this->username,
json_encode($this->rules),
$this->logic_operator,
(int)$this->random,
$this->limit,
time(),
$this->id
));
// reformat after an update
$this->format();
return $this->id;
}
/**
* filter_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
* _get_sql_foo.
* @param string $data
* @param string $type
* @param array $operator
* @return bool|int|null|string
*/
public function filter_data(string $data, string $type, array $operator)
{
if (array_key_exists('preg_match', $operator)) {
$data = preg_replace($operator['preg_match'], $operator['preg_replace'], $data);
}
if ($type == 'numeric' || $type == 'days') {
return (int)($data);
}
if ($type == 'boolean') {
return make_bool($data);
}
return $data;
}
/**
* year_search
*
* Build search rules for year -> year album searches for subsonic.
* @param int $fromYear
* @param int $toYear
* @param int $size
* @param int $offset
* @return array
*/
public static function year_search($fromYear, $toYear, $size, $offset): array
{
$search = array();
$search['limit'] = $size;
$search['offset'] = $offset;
$search['type'] = "album";
if ($fromYear) {
$search['rule_0_input'] = $fromYear;
$search['rule_0_operator'] = 0;
$search['rule_0'] = "original_year";
}
if ($toYear) {
$search['rule_1_input'] = $toYear;
$search['rule_1_operator'] = 1;
$search['rule_1'] = "original_year";
}
return $search;
}
/**
* @deprecated
*/
private function getLicenseRepository(): LicenseRepositoryInterface
{
global $dic;
return $dic->get(LicenseRepositoryInterface::class);
}
/**
* @deprecated inject dependency
*/
private function getUserRepository(): UserRepositoryInterface
{
global $dic;
return $dic->get(UserRepositoryInterface::class);
}
/**
* @deprecated inject dependency
*/
private function getMetadataFieldRepository(): MetadataFieldRepositoryInterface
{
global $dic;
return $dic->get(MetadataFieldRepositoryInterface::class);
}
}