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