TrackMapper::findAllByCriteria()   B
last analyzed

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
	 * @return int[]
135
	 */
136
	public function findDirtyFileIds(string $userId) : array {
137
		$updatedEpoch = $this->sqlDateToEpoch('`track`.`updated`');
138
		$sql = "SELECT `track`.`file_id`
139
				FROM `*PREFIX*music_tracks` `track`
140
				INNER JOIN `*PREFIX*filecache` `file`
141
				ON `track`.`file_id` = `file`.`fileid`
142
				WHERE `track`.`user_id` = ?
143
				AND (`track`.`dirty` = '1' OR $updatedEpoch < `file`.`mtime`)";
144
		$result = $this->execute($sql, [$userId]);
145
		return $result->fetchAll(\PDO::FETCH_COLUMN);
146
	}
147
148
	/**
149
	 * Find a track of user matching a file ID
150
	 * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
151
	 */
152
	public function findByFileId(int $fileId, string $userId) : Track {
153
		$sql = $this->selectUserEntities('`file_id` = ?');
154
		$params = [$userId, $fileId];
155
		return $this->findEntity($sql, $params);
156
	}
157
158
	/**
159
	 * Find tracks of user with multiple file IDs
160
	 * @param integer[] $fileIds
161
	 * @param string[] $userIds
162
	 * @return Track[]
163
	 */
164
	public function findByFileIds(array $fileIds, array $userIds) : array {
165
		$sql = $this->selectEntities(
166
				'`*PREFIX*music_tracks`.`user_id` IN ' . $this->questionMarks(\count($userIds)) .
167
				' AND `file_id` IN '. $this->questionMarks(\count($fileIds)));
168
		$params = \array_merge($userIds, $fileIds);
169
		return $this->findEntities($sql, $params);
170
	}
171
172
	/**
173
	 * Finds tracks of all users matching one or multiple file IDs
174
	 * @param integer[] $fileIds
175
	 * @return Track[]
176
	 */
177
	public function findAllByFileIds(array $fileIds) : array {
178
		$sql = $this->selectEntities('`file_id` IN '.
179
				$this->questionMarks(\count($fileIds)));
180
		return $this->findEntities($sql, $fileIds);
181
	}
182
183
	public function countByArtist(int $artistId) : int {
184
		$sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `artist_id` = ?';
185
		$result = $this->execute($sql, [$artistId]);
186
		$row = $result->fetch();
187
		return (int)$row['count'];
188
	}
189
190
	public function countByAlbum(int $albumId) : int {
191
		$sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?';
192
		$result = $this->execute($sql, [$albumId]);
193
		$row = $result->fetch();
194
		return (int)$row['count'];
195
	}
196
197
	/**
198
	 * @return integer Duration in seconds
199
	 */
200
	public function totalDurationOfAlbum(int $albumId) : int {
201
		$sql = 'SELECT SUM(`length`) AS `duration` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?';
202
		$result = $this->execute($sql, [$albumId]);
203
		$row = $result->fetch();
204
		return (int)$row['duration'];
205
	}
206
207
	/**
208
	 * @return integer Duration in seconds
209
	 */
210
	public function totalDurationByArtist(int $artistId) : int {
211
		$sql = 'SELECT SUM(`length`) AS `duration` FROM `*PREFIX*music_tracks` WHERE `artist_id` = ?';
212
		$result = $this->execute($sql, [$artistId]);
213
		$row = $result->fetch();
214
		return (int)$row['duration'];
215
	}
216
217
	/**
218
	 * Get durations of the given tracks.
219
	 * @param integer[] $trackIds
220
	 * @return array {int => int} where keys are track IDs and values are corresponding durations
221
	 */
222
	public function getDurations(array $trackIds) : array {
223
		$result = [];
224
225
		if (!empty($trackIds)) {
226
			$sql = 'SELECT `id`, `length` FROM `*PREFIX*music_tracks` WHERE `id` IN ' .
227
						$this->questionMarks(\count($trackIds));
228
			$rows = $this->execute($sql, $trackIds)->fetchAll();
229
			foreach ($rows as $row) {
230
				$result[$row['id']] = (int)$row['length'];
231
			}
232
		}
233
		return $result;
234
	}
235
236
	/**
237
	 * @return Track[]
238
	 */
239
	public function findAllByNameRecursive(string $name, string $userId, ?int $limit=null, ?int $offset=null) {
240
		$condition = '( LOWER(`artist`.`name`) LIKE LOWER(?) OR
241
						LOWER(`album`.`name`) LIKE LOWER(?) OR
242
						LOWER(`title`) LIKE LOWER(?) )';
243
		$sql = $this->selectUserEntities($condition, 'ORDER BY LOWER(`title`)');
244
		$name = BaseMapper::prepareSubstringSearchPattern($name);
245
		$params = [$userId, $name, $name, $name];
246
		return $this->findEntities($sql, $params, $limit, $offset);
247
	}
248
249
	/**
250
	 * Returns all tracks specified by name, artist name, and/or album name
251
	 * @param string|null $name the name of the track
252
	 * @param string|null $artistName the name of the artist
253
	 * @param string $userId the name of the user
254
	 * @return Track[] Tracks matching the criteria
255
	 */
256
	public function findAllByNameArtistOrAlbum(?string $name, ?string $artistName, ?string $albumName, string $userId) : array {
257
		$sqlConditions = [];
258
		$params = [$userId];
259
260
		if (!empty($name)) {
261
			$sqlConditions[] = 'LOWER(`title`) = LOWER(?)';
262
			$params[] = $name;
263
		}
264
265
		if (!empty($artistName)) {
266
			$sqlConditions[] = 'LOWER(`artist`.`name`) = LOWER(?)';
267
			$params[] = $artistName;
268
		}
269
270
		if (!empty($albumName)) {
271
			$sqlConditions[] = 'LOWER(`album`.`name`) = LOWER(?)';
272
			$params[] = $albumName;
273
		}
274
275
		// at least one condition has to be given, otherwise return an empty set
276
		if (\count($sqlConditions) > 0) {
277
			$sql = $this->selectUserEntities(\implode(' AND ', $sqlConditions));
278
			return $this->findEntities($sql, $params);
279
		} else {
280
			return [];
281
		}
282
	}
283
284
	const FAVORITE_TRACK = 0x1;
285
	const FAVORITE_ALBUM = 0x2;
286
	const FAVORITE_ARTIST = 0x4;
287
288
	/**
289
	 * Returns all tracks specified by various criteria, all of which are optional
290
	 * @param int[] $genres Array of genre IDs
291
	 * @param int[] $artists Array of artist IDs
292
	 * @param int|null $fromYear Earliest release year to include
293
	 * @param int|null $toYear Latest release year to include
294
	 * @param int|null $favorite Bit mask of FAVORITE_TRACK, FAVORITE_ALBUM, FAVORITE_ARTIST (given favorite types are ORed in the query)
295
	 * @param int $sortBy Sorting rule as defined in the class SortBy
296
	 * @param string $userId the name of the user
297
	 * @return Track[] Tracks matching the criteria
298
	 */
299
	public function findAllByCriteria(
300
			array $genres, array $artists, ?int $fromYear, ?int $toYear, ?int $favorite,
301
			int $sortBy, bool $invertSort, string $userId, ?int $limit=null, ?int $offset=null) : array {
302
303
		$sqlConditions = [];
304
		$params = [$userId];
305
306
		if (!empty($genres)) {
307
			$sqlConditions[] = '`genre_id` IN ' . $this->questionMarks(\count($genres));
308
			$params = \array_merge($params, $genres);
309
		}
310
311
		if (!empty($artists)) {
312
			$sqlConditions[] = '`artist_id` IN ' . $this->questionMarks(\count($artists));
313
			$params = \array_merge($params, $artists);
314
		}
315
316
		if (!empty($fromYear)) {
317
			$sqlConditions[] = '`year` >= ?';
318
			$params[] = $fromYear;
319
		}
320
321
		if (!empty($toYear)) {
322
			$sqlConditions[] = '`year` <= ?';
323
			$params[] = $toYear;
324
		}
325
326
		if (!empty($favorite)) {
327
			$favConds = [];
328
			if ($favorite & self::FAVORITE_TRACK) {
329
				$favConds[] = '`*PREFIX*music_tracks`.`starred` IS NOT NULL';
330
			}
331
			if ($favorite & self::FAVORITE_ALBUM) {
332
				$favConds[] = '`album`.`starred` IS NOT NULL';
333
			}
334
			if ($favorite & self::FAVORITE_ARTIST) {
335
				$favConds[] = '`artist`.`starred` IS NOT NULL';
336
			}
337
			$sqlConditions[] = '(' . \implode(' OR ', $favConds) . ')';
338
		} 
339
340
		$sql = $this->selectUserEntities(\implode(' AND ', $sqlConditions), $this->formatSortingClause($sortBy, $invertSort));
341
		return $this->findEntities($sql, $params, $limit, $offset);
342
	}
343
344
	/**
345
	 * Find most frequently played tracks
346
	 * @return Track[]
347
	 */
348
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
349
		$sql = $this->selectUserEntities('`play_count` > 0', 'ORDER BY `play_count` DESC, LOWER(`title`)');
350
		return $this->findEntities($sql, [$userId], $limit, $offset);
351
	}
352
353
	/**
354
	 * Find most recently played tracks
355
	 * @return Track[]
356
	 */
357
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
358
		$sql = $this->selectUserEntities('`last_played` IS NOT NULL', 'ORDER BY `last_played` DESC');
359
		return $this->findEntities($sql, [$userId], $limit, $offset);
360
	}
361
362
	/**
363
	 * Find least recently played tracks
364
	 * @return Track[]
365
	 */
366
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
367
		$sql = $this->selectUserEntities(null, 'ORDER BY `last_played` ASC');
368
		return $this->findEntities($sql, [$userId], $limit, $offset);
369
	}
370
371
	/**
372
	 * Finds all track IDs of the user along with the parent folder ID of each track
373
	 * @return array where keys are folder IDs and values are arrays of track IDs
374
	 */
375
	public function findTrackAndFolderIds(string $userId) : array {
376
		$sql = 'SELECT `track`.`id` AS id, `file`.`name` AS `filename`, `file`.`parent` AS `parent`
377
				FROM `*PREFIX*music_tracks` `track`
378
				JOIN `*PREFIX*filecache` `file`
379
				ON `track`.`file_id` = `file`.`fileid`
380
				WHERE `track`.`user_id` = ?';
381
382
		$rows = $this->execute($sql, [$userId])->fetchAll();
383
384
		// Sort the results according the file names. This can't be made using ORDERBY in the
385
		// SQL query because then we couldn't use the "natural order" comparison algorithm
386
		\usort($rows, fn($a, $b) => \strnatcasecmp($a['filename'], $b['filename']));
387
388
		// group the files to parent folder "buckets"
389
		$result = [];
390
		foreach ($rows as $row) {
391
			$result[(int)$row['parent']][] = (int)$row['id'];
392
		}
393
394
		return $result;
395
	}
396
397
	/**
398
	 * Find names and parents of the file system nodes with given IDs
399
	 * @param int[] $nodeIds
400
	 * @return array where keys are the node IDs and values are associative arrays like { 'name' => string, 'parent' => int };
401
	 */
402
	public function findNodeNamesAndParents(array $nodeIds) : array {
403
		$result = [];
404
405
		if (!empty($nodeIds)) {
406
			$sql = 'SELECT `fileid`, `name`, `parent`
407
					FROM `*PREFIX*filecache` `filecache`
408
					WHERE `filecache`.`fileid` IN '. $this->questionMarks(\count($nodeIds));
409
410
			$rows = $this->execute($sql, $nodeIds)->fetchAll();
411
412
			foreach ($rows as $row) {
413
				$result[$row['fileid']] = [
414
					'name' => $row['name'],
415
					'parent' => (int)$row['parent']
416
				];
417
			}
418
		}
419
420
		return $result;
421
	}
422
423
	/**
424
	 * Returns all genre IDs associated with the given artist
425
	 * @return int[]
426
	 */
427
	public function getGenresByArtistId(int $artistId, string $userId) : array {
428
		$sql = 'SELECT DISTINCT(`genre_id`) FROM `*PREFIX*music_tracks` WHERE
429
				`genre_id` IS NOT NULL AND `user_id` = ? AND `artist_id` = ?';
430
		$rows = $this->execute($sql, [$userId, $artistId]);
431
		return $rows->fetchAll(\PDO::FETCH_COLUMN);
432
	}
433
434
	/**
435
	 * Returns all tracks IDs of the user, organized by the genre_id.
436
	 * @return array where keys are genre IDs and values are arrays of track IDs
437
	 */
438
	public function mapGenreIdsToTrackIds(string $userId) : array {
439
		$sql = 'SELECT `id`, `genre_id` FROM `*PREFIX*music_tracks`
440
				WHERE `genre_id` IS NOT NULL and `user_id` = ?';
441
		$rows = $this->execute($sql, [$userId])->fetchAll();
442
443
		$result = [];
444
		foreach ($rows as $row) {
445
			$result[(int)$row['genre_id']][] = (int)$row['id'];
446
		}
447
448
		return $result;
449
	}
450
451
	/**
452
	 * Returns file IDs of the tracks which do not have genre scanned. This is not the same
453
	 * thing as unknown genre, which means that the genre has been scanned but was not found
454
	 * from the track metadata.
455
	 * @return int[]
456
	 */
457
	public function findFilesWithoutScannedGenre(string $userId) : array {
458
		$sql = 'SELECT `track`.`file_id` FROM `*PREFIX*music_tracks` `track`
459
				INNER JOIN `*PREFIX*filecache` `file`
460
				ON `track`.`file_id` = `file`.`fileid`
461
				WHERE `genre_id` IS NULL and `user_id` = ?';
462
		$rows = $this->execute($sql, [$userId]);
463
		return $rows->fetchAll(\PDO::FETCH_COLUMN);
464
	}
465
466
	/**
467
	 * Update "last played" timestamp and increment the total play count of the track.
468
	 * The DB row is updated *without* updating the `updated` column.
469
	 * @return bool true if the track was found and updated, false otherwise
470
	 */
471
	public function recordTrackPlayed(int $trackId, string $userId, \DateTime $timeOfPlay) : bool {
472
		$sql = 'UPDATE `*PREFIX*music_tracks`
473
				SET `last_played` = ?, `play_count` = `play_count` + 1
474
				WHERE `user_id` = ? AND `id` = ?';
475
		$params = [$timeOfPlay->format(BaseMapper::SQL_DATE_FORMAT), $userId, $trackId];
476
		$result = $this->execute($sql, $params);
477
		return ($result->rowCount() > 0);
478
	}
479
480
	/**
481
	 * Marks tracks as dirty, ultimately requesting the user to rescan them
482
	 * @param int[] $fileIds file IDs of the tracks to mark as dirty
483
	 * @param string[]|null $userIds the target users; if omitted, the tracks matching the
484
	 *                      $fileIds are marked for all users
485
	 * @return int number of rows affected
486
	 */
487
	public function markTracksDirty(array $fileIds, ?array $userIds=null) : int {
488
		$sql = 'UPDATE `*PREFIX*music_tracks`
489
				SET `dirty` = 1
490
				WHERE `file_id` IN ' . $this->questionMarks(\count($fileIds));
491
		$params = $fileIds;
492
493
		if (!empty($userIds)) {
494
			$sql .= ' AND `user_id` IN ' . $this->questionMarks(\count($userIds));
495
			$params = \array_merge($params, $userIds);
496
		}
497
498
		$result = $this->execute($sql, $params);
499
		return $result->rowCount();
500
	}
501
502
	/**
503
	 * Overridden from the base implementation to provide support for table-specific rules
504
	 *
505
	 * {@inheritdoc}
506
	 * @see BaseMapper::advFormatSqlCondition()
507
	 */
508
	protected function advFormatSqlCondition(string $rule, string $sqlOp, string $conv) : string {
509
		// The extra subquery "mysqlhack" seen around some nested queries is needed in order for these to not be insanely slow on MySQL.
510
		$condForRule = [
511
			'anywhere'			=> self::formatAdvSearchAnywhereCond($sqlOp, $conv),
512
			'album'				=> "$conv(`album`.`name`) $sqlOp $conv(?)",
513
			'artist'			=> "$conv(`artist`.`name`) $sqlOp $conv(?)",
514
			'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(?))",
515
			'album_artist_id'	=> "$sqlOp `album_id` IN (SELECT `id` from `*PREFIX*music_albums` WHERE `album_artist_id` = ?)", // our own API extension
516
			'track'				=> "`number` $sqlOp ?",
517
			'year'				=> "`year` $sqlOp ?",
518
			'albumrating'		=> "`album`.`rating` $sqlOp ?",
519
			'artistrating'		=> "`artist`.`rating` $sqlOp ?",
520
			'favorite_album'	=> "$conv(`album`.`name`) $sqlOp $conv(?) AND `album`.`starred` IS NOT NULL",
521
			'favorite_artist'	=> "$conv(`artist`.`name`) $sqlOp $conv(?) AND `artist`.`starred` IS NOT NULL",
522
			'played_times'		=> "`play_count` $sqlOp ?",
523
			'last_play'			=> "`last_played` $sqlOp ?",
524
			'myplayed'			=> "`last_played` $sqlOp", // operator "IS NULL" or "IS NOT NULL"
525
			'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"
526
			'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"
527
			'time'				=> "`length` $sqlOp ?",
528
			'bitrate'			=> "`bitrate` $sqlOp ?",
529
			'song_genre'		=> "$conv(`genre`.`name`) $sqlOp $conv(?)",
530
			'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)",
531
			'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)",
532
			'no_genre'			=> ($sqlOp == 'IS NOT NULL') ? '`genre`.`name` = ""' : '`genre`.`name` != ""',
533
			'playlist'			=> "$sqlOp EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE `p`.`id` = ? AND `p`.`track_ids` LIKE " . $this->sqlConcat("'%|'", "`*PREFIX*music_tracks`.`id`", "'|%'") . ')',
534
			'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`", "'|%'") . ')',
535
			'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)",
536
			'file'				=> "$conv(`file`.`name`) $sqlOp $conv(?)",
537
			'mbid_album'		=> "`album`.`mbid` $sqlOp ?",
538
			'mbid_artist'		=> "`artist`.`mbid` $sqlOp ?"
539
		];
540
541
		// Add alias rules
542
		$condForRule['played'] = $condForRule['myplayed'];		// we give no access to other people's data; not part of the API spec but Ample uses this
543
		$condForRule['genre'] = $condForRule['song_genre'];
544
		$condForRule['song'] = parent::advFormatSqlCondition('title', $sqlOp, $conv);
545
		$condForRule['mbid_song'] = parent::advFormatSqlCondition('mbid', $sqlOp, $conv);
546
547
		return $condForRule[$rule] ?? parent::advFormatSqlCondition($rule, $sqlOp, $conv);
548
	}
549
550
	private static function formatAdvSearchAnywhereCond(string $sqlOp, string $conv) : string {
551
		$fields = [
552
			"`*PREFIX*music_tracks`.`title`",
553
			"`file`.`name`",
554
			"`artist`.`name`",
555
			"`album`.`name`",
556
			"`genre`.`name`"
557
		];
558
		$parts = \array_map(fn($field) => "$conv($field) $sqlOp $conv(?)", $fields);
559
560
		$negativeOp = \in_array($sqlOp, ['NOT LIKE', '!=', 'NOT REGEXP']);
561
		$cond = \implode($negativeOp ? ' AND ' : ' OR ', $parts);
562
563
		return "($cond)";
564
	}
565
}
566