Passed
Push — master ( d93244...31d6f2 )
by Pauli
03:37
created

TrackMapper::findAllByCriteria()   B

Complexity

Conditions 9
Paths 144

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 25
c 0
b 0
f 0
nc 144
nop 10
dl 0
loc 43
rs 7.6888

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2016 - 2025
13
 */
14
15
namespace OCA\Music\Db;
16
17
use OCP\IConfig;
18
use OCP\IDBConnection;
19
20
/**
21
 * @method Track findEntity(string $sql, array $params)
22
 * @method Track[] findEntities(string $sql, array $params, ?int $limit=null, ?int $offset=null)
23
 * @phpstan-extends BaseMapper<Track>
24
 */
25
class TrackMapper extends BaseMapper {
26
	public function __construct(IDBConnection $db, IConfig $config) {
27
		parent::__construct($db, $config, 'music_tracks', Track::class, 'title', ['file_id', 'user_id'], 'album_id');
28
	}
29
30
	/**
31
	 * Override the base implementation to include data from multiple tables
32
	 *
33
	 * {@inheritdoc}
34
	 * @see BaseMapper::selectEntities()
35
	 */
36
	protected function selectEntities(string $condition, ?string $extension=null) : string {
37
		return "SELECT `*PREFIX*music_tracks`.*, `file`.`name` AS `filename`, `file`.`size`, `file`.`mtime` AS `file_mod_time`, `file`.`parent` AS `folder_id`,
38
						`album`.`name` AS `album_name`, `artist`.`name` AS `artist_name`, `genre`.`name` AS `genre_name`
39
				FROM `*PREFIX*music_tracks`
40
				INNER JOIN `*PREFIX*filecache` `file`
41
				ON `*PREFIX*music_tracks`.`file_id` = `file`.`fileid`
42
				INNER JOIN `*PREFIX*music_albums` `album`
43
				ON `*PREFIX*music_tracks`.`album_id` = `album`.`id`
44
				INNER JOIN `*PREFIX*music_artists` `artist`
45
				ON `*PREFIX*music_tracks`.`artist_id` = `artist`.`id`
46
				LEFT JOIN `*PREFIX*music_genres` `genre`
47
				ON `*PREFIX*music_tracks`.`genre_id` = `genre`.`id`
48
				WHERE $condition $extension";
49
	}
50
51
	/**
52
	 * Overridden from the base implementation to add support for sorting by artist, play_count, and last_played.
53
	 *
54
	 * {@inheritdoc}
55
	 * @see BaseMapper::formatSortingClause()
56
	 */
57
	protected function formatSortingClause(int $sortBy, bool $invertSort = false) : ?string {
58
		switch ($sortBy) {
59
			case SortBy::Parent:
60
				$dir = $invertSort ? 'DESC' : 'ASC';
61
				// Note: the alternative form "LOWER(`artist_name`) wouldn't work on PostgreSQL, see https://github.com/owncloud/music/issues/1046 for a similar case
62
				return "ORDER BY LOWER(`artist`.`name`) $dir, LOWER(`title`) $dir";
63
			case SortBy::PlayCount:
64
				$dir = $invertSort ? 'ASC' : 'DESC';
65
				return "ORDER BY `play_count` $dir";
66
			case SortBy::LastPlayed:
67
				$dir = $invertSort ? 'ASC' : 'DESC';
68
				return "ORDER BY `last_played` $dir";
69
			default:
70
				return parent::formatSortingClause($sortBy, $invertSort);
71
		}
72
	}
73
74
	/**
75
	 * Returns all tracks of the given artist (both album and track artists are considered)
76
	 * @param int[] $artistIds
77
	 * @return Track[]
78
	 */
79
	public function findAllByArtist(array $artistIds, string $userId, ?int $limit=null, ?int $offset=null) : array {
80
		$questionMarks = $this->questionMarks(\count($artistIds));
81
		$sql = $this->selectUserEntities(
82
				"`artist_id` IN $questionMarks OR `album_id` IN (SELECT `id` from `*PREFIX*music_albums` WHERE `album_artist_id` IN $questionMarks)",
83
				'ORDER BY LOWER(`title`)');
84
		$params = \array_merge([$userId], $artistIds, $artistIds);
85
		return $this->findEntities($sql, $params, $limit, $offset);
86
	}
87
88
	/**
89
	 * @param int[] $albumIds
90
	 * @return Track[]
91
	 */
92
	public function findAllByAlbum(array $albumIds, string $userId, ?int $artistId=null, ?int $limit=null, ?int $offset=null) : array {
93
		$condition = '`album_id` IN ' . $this->questionMarks(\count($albumIds));
94
		$params = \array_merge([$userId], $albumIds);
95
96
		if ($artistId !== null) {
97
			$condition .= ' AND `artist_id` = ? ';
98
			$params[] = $artistId;
99
		}
100
101
		$sql = $this->selectUserEntities($condition,
102
				'ORDER BY `*PREFIX*music_tracks`.`disk`, `number`, LOWER(`file`.`name`)');
103
		return $this->findEntities($sql, $params, $limit, $offset);
104
	}
105
106
	/**
107
	 * @return Track[]
108
	 */
109
	public function findAllByFolder(int $folderId, string $userId, ?int $limit=null, ?int $offset=null) : array {
110
		$sql = $this->selectUserEntities('`file`.`parent` = ?', 'ORDER BY LOWER(`file`.`name`)');
111
		$params = [$userId, $folderId];
112
		return $this->findEntities($sql, $params, $limit, $offset);
113
	}
114
115
	/**
116
	 * @return Track[]
117
	 */
118
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
119
		$sql = $this->selectUserEntities('`genre_id` = ?', 'ORDER BY LOWER(`title`)');
120
		$params = [$userId, $genreId];
121
		return $this->findEntities($sql, $params, $limit, $offset);
122
	}
123
124
	/**
125
	 * @return int[]
126
	 */
127
	public function findAllFileIds(string $userId) : array {
128
		$sql = 'SELECT `file_id` FROM `*PREFIX*music_tracks` WHERE `user_id` = ?';
129
		$result = $this->execute($sql, [$userId]);
130
		return $result->fetchAll(\PDO::FETCH_COLUMN);
131
	}
132
133
	/**
134
	 * Find a track of user matching a file ID
135
	 * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
136
	 */
137
	public function findByFileId(int $fileId, string $userId) : Track {
138
		$sql = $this->selectUserEntities('`file_id` = ?');
139
		$params = [$userId, $fileId];
140
		return $this->findEntity($sql, $params);
141
	}
142
143
	/**
144
	 * Find tracks of user with multiple file IDs
145
	 * @param integer[] $fileIds
146
	 * @param string[] $userIds
147
	 * @return Track[]
148
	 */
149
	public function findByFileIds(array $fileIds, array $userIds) : array {
150
		$sql = $this->selectEntities(
151
				'`*PREFIX*music_tracks`.`user_id` IN ' . $this->questionMarks(\count($userIds)) .
152
				' AND `file_id` IN '. $this->questionMarks(\count($fileIds)));
153
		$params = \array_merge($userIds, $fileIds);
154
		return $this->findEntities($sql, $params);
155
	}
156
157
	/**
158
	 * Finds tracks of all users matching one or multiple file IDs
159
	 * @param integer[] $fileIds
160
	 * @return Track[]
161
	 */
162
	public function findAllByFileIds(array $fileIds) : array {
163
		$sql = $this->selectEntities('`file_id` IN '.
164
				$this->questionMarks(\count($fileIds)));
165
		return $this->findEntities($sql, $fileIds);
166
	}
167
168
	public function countByArtist(int $artistId) : int {
169
		$sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `artist_id` = ?';
170
		$result = $this->execute($sql, [$artistId]);
171
		$row = $result->fetch();
172
		return (int)$row['count'];
173
	}
174
175
	public function countByAlbum(int $albumId) : int {
176
		$sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?';
177
		$result = $this->execute($sql, [$albumId]);
178
		$row = $result->fetch();
179
		return (int)$row['count'];
180
	}
181
182
	/**
183
	 * @return integer Duration in seconds
184
	 */
185
	public function totalDurationOfAlbum(int $albumId) : int {
186
		$sql = 'SELECT SUM(`length`) AS `duration` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?';
187
		$result = $this->execute($sql, [$albumId]);
188
		$row = $result->fetch();
189
		return (int)$row['duration'];
190
	}
191
192
	/**
193
	 * @return integer Duration in seconds
194
	 */
195
	public function totalDurationByArtist(int $artistId) : int {
196
		$sql = 'SELECT SUM(`length`) AS `duration` FROM `*PREFIX*music_tracks` WHERE `artist_id` = ?';
197
		$result = $this->execute($sql, [$artistId]);
198
		$row = $result->fetch();
199
		return (int)$row['duration'];
200
	}
201
202
	/**
203
	 * Get durations of the given tracks.
204
	 * @param integer[] $trackIds
205
	 * @return array {int => int} where keys are track IDs and values are corresponding durations
206
	 */
207
	public function getDurations(array $trackIds) : array {
208
		$result = [];
209
210
		if (!empty($trackIds)) {
211
			$sql = 'SELECT `id`, `length` FROM `*PREFIX*music_tracks` WHERE `id` IN ' .
212
						$this->questionMarks(\count($trackIds));
213
			$rows = $this->execute($sql, $trackIds)->fetchAll();
214
			foreach ($rows as $row) {
215
				$result[$row['id']] = (int)$row['length'];
216
			}
217
		}
218
		return $result;
219
	}
220
221
	/**
222
	 * @return Track[]
223
	 */
224
	public function findAllByNameRecursive(string $name, string $userId, ?int $limit=null, ?int $offset=null) {
225
		$condition = '( LOWER(`artist`.`name`) LIKE LOWER(?) OR
226
						LOWER(`album`.`name`) LIKE LOWER(?) OR
227
						LOWER(`title`) LIKE LOWER(?) )';
228
		$sql = $this->selectUserEntities($condition, 'ORDER BY LOWER(`title`)');
229
		$name = BaseMapper::prepareSubstringSearchPattern($name);
230
		$params = [$userId, $name, $name, $name];
231
		return $this->findEntities($sql, $params, $limit, $offset);
232
	}
233
234
	/**
235
	 * Returns all tracks specified by name, artist name, and/or album name
236
	 * @param string|null $name the name of the track
237
	 * @param string|null $artistName the name of the artist
238
	 * @param string $userId the name of the user
239
	 * @return Track[] Tracks matching the criteria
240
	 */
241
	public function findAllByNameArtistOrAlbum(?string $name, ?string $artistName, ?string $albumName, string $userId) : array {
242
		$sqlConditions = [];
243
		$params = [$userId];
244
245
		if (!empty($name)) {
246
			$sqlConditions[] = 'LOWER(`title`) = LOWER(?)';
247
			$params[] = $name;
248
		}
249
250
		if (!empty($artistName)) {
251
			$sqlConditions[] = 'LOWER(`artist`.`name`) = LOWER(?)';
252
			$params[] = $artistName;
253
		}
254
255
		if (!empty($albumName)) {
256
			$sqlConditions[] = 'LOWER(`album`.`name`) = LOWER(?)';
257
			$params[] = $albumName;
258
		}
259
260
		// at least one condition has to be given, otherwise return an empty set
261
		if (\count($sqlConditions) > 0) {
262
			$sql = $this->selectUserEntities(\implode(' AND ', $sqlConditions));
263
			return $this->findEntities($sql, $params);
264
		} else {
265
			return [];
266
		}
267
	}
268
269
	const FAVORITE_TRACK = 0x1;
270
	const FAVORITE_ALBUM = 0x2;
271
	const FAVORITE_ARTIST = 0x4;
272
273
	/**
274
	 * Returns all tracks specified by various criteria, all of which are optional
275
	 * @param int[] $genres Array of genre IDs
276
	 * @param int[] $artists Array of artist IDs
277
	 * @param int|null $fromYear Earliest release year to include
278
	 * @param int|null $toYear Latest release year to include
279
	 * @param int|null $favorite Bit mask of FAVORITE_TRACK, FAVORITE_ALBUM, FAVORITE_ARTIST (given favorite types are ORed in the query)
280
	 * @param int $sortBy Sorting rule as defined in the class SortBy
281
	 * @param string $userId the name of the user
282
	 * @return Track[] Tracks matching the criteria
283
	 */
284
	public function findAllByCriteria(
285
			array $genres, array $artists, ?int $fromYear, ?int $toYear, ?int $favorite,
286
			int $sortBy, bool $invertSort, string $userId, ?int $limit=null, ?int $offset=null) : array {
287
288
		$sqlConditions = [];
289
		$params = [$userId];
290
291
		if (!empty($genres)) {
292
			$sqlConditions[] = '`genre_id` IN ' . $this->questionMarks(\count($genres));
293
			$params = \array_merge($params, $genres);
294
		}
295
296
		if (!empty($artists)) {
297
			$sqlConditions[] = '`artist_id` IN ' . $this->questionMarks(\count($artists));
298
			$params = \array_merge($params, $artists);
299
		}
300
301
		if (!empty($fromYear)) {
302
			$sqlConditions[] = '`year` >= ?';
303
			$params[] = $fromYear;
304
		}
305
306
		if (!empty($toYear)) {
307
			$sqlConditions[] = '`year` <= ?';
308
			$params[] = $toYear;
309
		}
310
311
		if (!empty($favorite)) {
312
			$favConds = [];
313
			if ($favorite & self::FAVORITE_TRACK) {
314
				$favConds[] = '`*PREFIX*music_tracks`.`starred` IS NOT NULL';
315
			}
316
			if ($favorite & self::FAVORITE_ALBUM) {
317
				$favConds[] = '`album`.`starred` IS NOT NULL';
318
			}
319
			if ($favorite & self::FAVORITE_ARTIST) {
320
				$favConds[] = '`artist`.`starred` IS NOT NULL';
321
			}
322
			$sqlConditions[] = '(' . \implode(' OR ', $favConds) . ')';
323
		} 
324
325
		$sql = $this->selectUserEntities(\implode(' AND ', $sqlConditions), $this->formatSortingClause($sortBy, $invertSort));
326
		return $this->findEntities($sql, $params, $limit, $offset);
327
	}
328
329
	/**
330
	 * Find most frequently played tracks
331
	 * @return Track[]
332
	 */
333
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
334
		$sql = $this->selectUserEntities('`play_count` > 0', 'ORDER BY `play_count` DESC, LOWER(`title`)');
335
		return $this->findEntities($sql, [$userId], $limit, $offset);
336
	}
337
338
	/**
339
	 * Find most recently played tracks
340
	 * @return Track[]
341
	 */
342
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
343
		$sql = $this->selectUserEntities('`last_played` IS NOT NULL', 'ORDER BY `last_played` DESC');
344
		return $this->findEntities($sql, [$userId], $limit, $offset);
345
	}
346
347
	/**
348
	 * Find least recently played tracks
349
	 * @return Track[]
350
	 */
351
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
352
		$sql = $this->selectUserEntities(null, 'ORDER BY `last_played` ASC');
353
		return $this->findEntities($sql, [$userId], $limit, $offset);
354
	}
355
356
	/**
357
	 * Finds all track IDs of the user along with the parent folder ID of each track
358
	 * @return array where keys are folder IDs and values are arrays of track IDs
359
	 */
360
	public function findTrackAndFolderIds(string $userId) : array {
361
		$sql = 'SELECT `track`.`id` AS id, `file`.`name` AS `filename`, `file`.`parent` AS `parent`
362
				FROM `*PREFIX*music_tracks` `track`
363
				JOIN `*PREFIX*filecache` `file`
364
				ON `track`.`file_id` = `file`.`fileid`
365
				WHERE `track`.`user_id` = ?';
366
367
		$rows = $this->execute($sql, [$userId])->fetchAll();
368
369
		// Sort the results according the file names. This can't be made using ORDERBY in the
370
		// SQL query because then we couldn't use the "natural order" comparison algorithm
371
		\usort($rows, fn($a, $b) => \strnatcasecmp($a['filename'], $b['filename']));
372
373
		// group the files to parent folder "buckets"
374
		$result = [];
375
		foreach ($rows as $row) {
376
			$result[(int)$row['parent']][] = (int)$row['id'];
377
		}
378
379
		return $result;
380
	}
381
382
	/**
383
	 * Find names and parents of the file system nodes with given IDs
384
	 * @param int[] $nodeIds
385
	 * @return array where keys are the node IDs and values are associative arrays like { 'name' => string, 'parent' => int };
386
	 */
387
	public function findNodeNamesAndParents(array $nodeIds) : array {
388
		$result = [];
389
390
		if (!empty($nodeIds)) {
391
			$sql = 'SELECT `fileid`, `name`, `parent`
392
					FROM `*PREFIX*filecache` `filecache`
393
					WHERE `filecache`.`fileid` IN '. $this->questionMarks(\count($nodeIds));
394
395
			$rows = $this->execute($sql, $nodeIds)->fetchAll();
396
397
			foreach ($rows as $row) {
398
				$result[$row['fileid']] = [
399
					'name' => $row['name'],
400
					'parent' => (int)$row['parent']
401
				];
402
			}
403
		}
404
405
		return $result;
406
	}
407
408
	/**
409
	 * Returns all genre IDs associated with the given artist
410
	 * @return int[]
411
	 */
412
	public function getGenresByArtistId(int $artistId, string $userId) : array {
413
		$sql = 'SELECT DISTINCT(`genre_id`) FROM `*PREFIX*music_tracks` WHERE
414
				`genre_id` IS NOT NULL AND `user_id` = ? AND `artist_id` = ?';
415
		$rows = $this->execute($sql, [$userId, $artistId]);
416
		return $rows->fetchAll(\PDO::FETCH_COLUMN);
417
	}
418
419
	/**
420
	 * Returns all tracks IDs of the user, organized by the genre_id.
421
	 * @return array where keys are genre IDs and values are arrays of track IDs
422
	 */
423
	public function mapGenreIdsToTrackIds(string $userId) : array {
424
		$sql = 'SELECT `id`, `genre_id` FROM `*PREFIX*music_tracks`
425
				WHERE `genre_id` IS NOT NULL and `user_id` = ?';
426
		$rows = $this->execute($sql, [$userId])->fetchAll();
427
428
		$result = [];
429
		foreach ($rows as $row) {
430
			$result[(int)$row['genre_id']][] = (int)$row['id'];
431
		}
432
433
		return $result;
434
	}
435
436
	/**
437
	 * Returns file IDs of the tracks which do not have genre scanned. This is not the same
438
	 * thing as unknown genre, which means that the genre has been scanned but was not found
439
	 * from the track metadata.
440
	 * @return int[]
441
	 */
442
	public function findFilesWithoutScannedGenre(string $userId) : array {
443
		$sql = 'SELECT `track`.`file_id` FROM `*PREFIX*music_tracks` `track`
444
				INNER JOIN `*PREFIX*filecache` `file`
445
				ON `track`.`file_id` = `file`.`fileid`
446
				WHERE `genre_id` IS NULL and `user_id` = ?';
447
		$rows = $this->execute($sql, [$userId]);
448
		return $rows->fetchAll(\PDO::FETCH_COLUMN);
449
	}
450
451
	/**
452
	 * Update "last played" timestamp and increment the total play count of the track.
453
	 * The DB row is updated *without* updating the `updated` column.
454
	 * @return bool true if the track was found and updated, false otherwise
455
	 */
456
	public function recordTrackPlayed(int $trackId, string $userId, \DateTime $timeOfPlay) : bool {
457
		$sql = 'UPDATE `*PREFIX*music_tracks`
458
				SET `last_played` = ?, `play_count` = `play_count` + 1
459
				WHERE `user_id` = ? AND `id` = ?';
460
		$params = [$timeOfPlay->format(BaseMapper::SQL_DATE_FORMAT), $userId, $trackId];
461
		$result = $this->execute($sql, $params);
462
		return ($result->rowCount() > 0);
463
	}
464
465
	/**
466
	 * Marks tracks as dirty, ultimately requesting the user to rescan them
467
	 * @param int[] $fileIds file IDs of the tracks to mark as dirty
468
	 * @param string[]|null $userIds the target users; if omitted, the tracks matching the
469
	 *                      $fileIds are marked for all users
470
	 * @return int number of rows affected
471
	 */
472
	public function markTracksDirty(array $fileIds, ?array $userIds=null) : int {
473
		$sql = 'UPDATE `*PREFIX*music_tracks`
474
				SET `dirty` = 1
475
				WHERE `file_id` IN ' . $this->questionMarks(\count($fileIds));
476
		$params = $fileIds;
477
478
		if (!empty($userIds)) {
479
			$sql .= ' AND `user_id` IN ' . $this->questionMarks(\count($userIds));
480
			$params = \array_merge($params, $userIds);
481
		}
482
483
		$result = $this->execute($sql, $params);
484
		return $result->rowCount();
485
	}
486
487
	/**
488
	 * Overridden from the base implementation to provide support for table-specific rules
489
	 *
490
	 * {@inheritdoc}
491
	 * @see BaseMapper::advFormatSqlCondition()
492
	 */
493
	protected function advFormatSqlCondition(string $rule, string $sqlOp, string $conv) : string {
494
		// The extra subquery "mysqlhack" seen around some nested queries is needed in order for these to not be insanely slow on MySQL.
495
		$condForRule = [
496
			'anywhere'			=> self::formatAdvSearchAnywhereCond($sqlOp, $conv),
497
			'album'				=> "$conv(`album`.`name`) $sqlOp $conv(?)",
498
			'artist'			=> "$conv(`artist`.`name`) $sqlOp $conv(?)",
499
			'album_artist'		=> "`album_id` IN (SELECT `al`.`id` from `*PREFIX*music_albums` `al` JOIN `*PREFIX*music_artists` `ar` ON `al`.`album_artist_id` = `ar`.`id` WHERE $conv(`ar`.`name`) $sqlOp $conv(?))",
500
			'album_artist_id'	=> "$sqlOp `album_id` IN (SELECT `id` from `*PREFIX*music_albums` WHERE `album_artist_id` = ?)", // our own API extension
501
			'track'				=> "`number` $sqlOp ?",
502
			'year'				=> "`year` $sqlOp ?",
503
			'albumrating'		=> "`album`.`rating` $sqlOp ?",
504
			'artistrating'		=> "`artist`.`rating` $sqlOp ?",
505
			'favorite_album'	=> "$conv(`album`.`name`) $sqlOp $conv(?) AND `album`.`starred` IS NOT NULL",
506
			'favorite_artist'	=> "$conv(`artist`.`name`) $sqlOp $conv(?) AND `artist`.`starred` IS NOT NULL",
507
			'played_times'		=> "`play_count` $sqlOp ?",
508
			'last_play'			=> "`last_played` $sqlOp ?",
509
			'myplayed'			=> "`last_played` $sqlOp", // operator "IS NULL" or "IS NOT NULL"
510
			'myplayedalbum'		=> "`album_id` IN (SELECT * FROM (SELECT `album_id` from `*PREFIX*music_tracks` GROUP BY `album_id` HAVING MAX(`last_played`) $sqlOp) mysqlhack)", // operator "IS NULL" or "IS NOT NULL"
511
			'myplayedartist'	=> "`artist_id` IN (SELECT * FROM (SELECT `artist_id` from `*PREFIX*music_tracks` GROUP BY `artist_id` HAVING MAX(`last_played`) $sqlOp) mysqlhack)", // operator "IS NULL" or "IS NOT NULL"
512
			'time'				=> "`length` $sqlOp ?",
513
			'bitrate'			=> "`bitrate` $sqlOp ?",
514
			'song_genre'		=> "$conv(`genre`.`name`) $sqlOp $conv(?)",
515
			'album_genre'		=> "`album_id` IN (SELECT * FROM (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_genres` `g` ON `t`.`genre_id` = `g`.`id` GROUP BY `album_id` HAVING $conv(" . $this->sqlGroupConcat('`g`.`name`') . ") $sqlOp $conv(?)) mysqlhack)",
516
			'artist_genre'		=> "`artist_id` IN (SELECT * FROM (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_genres` `g` ON `t`.`genre_id` = `g`.`id` GROUP BY `artist_id` HAVING $conv(" . $this->sqlGroupConcat('`g`.`name`') . ") $sqlOp $conv(?)) mysqlhack)",
517
			'no_genre'			=> ($sqlOp == 'IS NOT NULL') ? '`genre`.`name` = ""' : '`genre`.`name` != ""',
518
			'playlist'			=> "$sqlOp EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE `p`.`id` = ? AND `p`.`track_ids` LIKE " . $this->sqlConcat("'%|'", "`*PREFIX*music_tracks`.`id`", "'|%'") . ')',
519
			'playlist_name'		=> "EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE $conv(`p`.`name`) $sqlOp $conv(?) AND `p`.`track_ids` LIKE " . $this->sqlConcat("'%|'", "`*PREFIX*music_tracks`.`id`", "'|%'") . ')',
520
			'recent_played'		=> "`*PREFIX*music_tracks`.`id` IN (SELECT * FROM (SELECT `id` FROM `*PREFIX*music_tracks` WHERE `user_id` = ? ORDER BY `last_played` DESC LIMIT $sqlOp) mysqlhack)",
521
			'file'				=> "$conv(`file`.`name`) $sqlOp $conv(?)",
522
			'mbid_album'		=> "`album`.`mbid` $sqlOp ?",
523
			'mbid_artist'		=> "`artist`.`mbid` $sqlOp ?"
524
		];
525
526
		// Add alias rules
527
		$condForRule['played'] = $condForRule['myplayed'];		// we give no access to other people's data; not part of the API spec but Ample uses this
528
		$condForRule['genre'] = $condForRule['song_genre'];
529
		$condForRule['song'] = parent::advFormatSqlCondition('title', $sqlOp, $conv);
530
		$condForRule['mbid_song'] = parent::advFormatSqlCondition('mbid', $sqlOp, $conv);
531
532
		return $condForRule[$rule] ?? parent::advFormatSqlCondition($rule, $sqlOp, $conv);
533
	}
534
535
	private static function formatAdvSearchAnywhereCond(string $sqlOp, string $conv) : string {
536
		$fields = [
537
			"`*PREFIX*music_tracks`.`title`",
538
			"`file`.`name`",
539
			"`artist`.`name`",
540
			"`album`.`name`",
541
			"`genre`.`name`"
542
		];
543
		$parts = \array_map(fn($field) => "$conv($field) $sqlOp $conv(?)", $fields);
544
545
		$negativeOp = \in_array($sqlOp, ['NOT LIKE', '!=', 'NOT REGEXP']);
546
		$cond = \implode($negativeOp ? ' AND ' : ' OR ', $parts);
547
548
		return "($cond)";
549
	}
550
}
551