AlbumMapper::formatSortingClause()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 7
rs 10
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 OCA\Music\Utility\ArrayUtil;
18
use OCA\Music\Utility\StringUtil;
19
20
use OCP\IConfig;
21
use OCP\IDBConnection;
22
23
/**
24
 * Type hint a base class method to help Scrutinizer
25
 * @method Album updateOrInsert(Album $album)
26
 * @phpstan-extends BaseMapper<Album>
27
 */
28
class AlbumMapper extends BaseMapper {
29
	public function __construct(IDBConnection $db, IConfig $config) {
30
		parent::__construct($db, $config, 'music_albums', Album::class, 'name', ['user_id', 'hash'], 'album_artist_id');
31
	}
32
33
	/**
34
	 * Override the base implementation to include data from multiple tables
35
	 *
36
	 * {@inheritdoc}
37
	 * @see BaseMapper::selectEntities()
38
	 */
39
	protected function selectEntities(string $condition, ?string $extension=null) : string {
40
		return "SELECT `*PREFIX*music_albums`.*, `artist`.`name` AS `album_artist_name`
41
				FROM `*PREFIX*music_albums`
42
				INNER JOIN `*PREFIX*music_artists` `artist`
43
				ON `*PREFIX*music_albums`.`album_artist_id` = `artist`.`id`
44
				WHERE $condition $extension";
45
	}
46
47
	/**
48
	 * Overridden from \OCA\Music\Db\BaseMapper to add support for sorting by artist.
49
	 *
50
	 * {@inheritdoc}
51
	 * @see BaseMapper::formatSortingClause()
52
	 */
53
	protected function formatSortingClause(int $sortBy, bool $invertSort = false) : ?string {
54
		if ($sortBy === SortBy::Parent) {
55
			// Note: the alternative form "LOWER(`album_artist_name`) wouldn't work on PostgreSQL, see https://github.com/owncloud/music/issues/1046
56
			$dir = $invertSort ? 'DESC' : 'ASC';
57
			return "ORDER BY LOWER(`artist`.`name`) $dir, LOWER(`*PREFIX*music_albums`.`name`) $dir";
58
		} else {
59
			return parent::formatSortingClause($sortBy, $invertSort);
60
		}
61
	}
62
63
	/**
64
	 * returns artist IDs mapped to album IDs
65
	 * does not include album_artist_id
66
	 *
67
	 * @param integer[]|null $albumIds IDs of the albums; get all albums of the user if null given
68
	 * @param string $userId the user ID
69
	 * @return array int => int[], keys are albums IDs and values are arrays of artist IDs
70
	 */
71
	public function getPerformingArtistsByAlbumId(?array $albumIds, string $userId) : array {
72
		$sql = 'SELECT DISTINCT `track`.`album_id`, `track`.`artist_id`
73
				FROM `*PREFIX*music_tracks` `track`
74
				WHERE `track`.`user_id` = ? ';
75
		$params = [$userId];
76
77
		if ($albumIds !== null) {
0 ignored issues
show
introduced by
The condition $albumIds !== null is always true.
Loading history...
78
			$sql .= 'AND `track`.`album_id` IN ' . $this->questionMarks(\count($albumIds));
79
			$params = \array_merge($params, $albumIds);
80
		}
81
82
		$result = $this->execute($sql, $params);
83
		$artistIds = [];
84
		while ($row = $result->fetch()) {
85
			$artistIds[$row['album_id']][] = (int)$row['artist_id'];
86
		}
87
		$result->closeCursor();
88
		return $artistIds;
89
	}
90
91
	/**
92
	 * returns release years mapped to album IDs
93
	 *
94
	 * @param integer[]|null $albumIds IDs of the albums; get all albums of the user if null given
95
	 * @param string $userId the user ID
96
	 * @return array int => int[], keys are albums IDs and values are arrays of years
97
	 */
98
	public function getYearsByAlbumId(?array $albumIds, string $userId) : array {
99
		$sql = 'SELECT DISTINCT `track`.`album_id`, `track`.`year`
100
				FROM `*PREFIX*music_tracks` `track`
101
				WHERE `track`.`user_id` = ?
102
				AND `track`.`year` IS NOT NULL ';
103
		$params = [$userId];
104
105
		if ($albumIds !== null) {
0 ignored issues
show
introduced by
The condition $albumIds !== null is always true.
Loading history...
106
			$sql .= 'AND `track`.`album_id` IN ' . $this->questionMarks(\count($albumIds));
107
			$params = \array_merge($params, $albumIds);
108
		}
109
110
		$result = $this->execute($sql, $params);
111
		$years = [];
112
		while ($row = $result->fetch()) {
113
			$years[$row['album_id']][] = (int)$row['year'];
114
		}
115
		$result->closeCursor();
116
		return $years;
117
	}
118
119
	/**
120
	 * returns genres mapped to album IDs
121
	 *
122
	 * @param integer[]|null $albumIds IDs of the albums; get all albums of the user if null given
123
	 * @param string $userId the user ID
124
	 * @return array int => Genre[], keys are albums IDs and values are arrays of *partial* Genre objects (only id and name properties set)
125
	 */
126
	public function getGenresByAlbumId(?array $albumIds, string $userId) : array {
127
		$sql = 'SELECT DISTINCT `album_id`, `genre_id`, `*PREFIX*music_genres`.`name` AS `genre_name`
128
				FROM `*PREFIX*music_tracks`
129
				LEFT JOIN `*PREFIX*music_genres`
130
				ON `genre_id` = `*PREFIX*music_genres`.`id`
131
				WHERE `*PREFIX*music_tracks`.`user_id` = ?
132
				AND `genre_id` IS NOT NULL ';
133
		$params = [$userId];
134
135
		if ($albumIds !== null) {
0 ignored issues
show
introduced by
The condition $albumIds !== null is always true.
Loading history...
136
			$sql .= 'AND `album_id` IN ' . $this->questionMarks(\count($albumIds));
137
			$params = \array_merge($params, $albumIds);
138
		}
139
140
		$result = $this->execute($sql, $params);
141
		$genres = [];
142
		while ($row = $result->fetch()) {
143
			$genre = new Genre();
144
			$genre->setUserId($userId);
145
			$genre->setId((int)$row['genre_id']);
146
			$genre->setName($row['genre_name']);
147
			$genres[$row['album_id']][] = $genre;
148
		}
149
		$result->closeCursor();
150
		return $genres;
151
	}
152
153
	/**
154
	 * returns number of disks per album ID
155
	 *
156
	 * @param integer[]|null $albumIds IDs of the albums; get all albums of the user if null given
157
	 * @param string $userId the user ID
158
	 * @return array int => int, keys are albums IDs and values are disk counts
159
	 */
160
	public function getDiscCountByAlbumId(?array $albumIds, string $userId) : array {
161
		$sql = 'SELECT `album_id`, MAX(`disk`) AS `disc_count`
162
				FROM `*PREFIX*music_tracks`
163
				WHERE `user_id` = ?
164
				GROUP BY `album_id` ';
165
		$params = [$userId];
166
167
		if ($albumIds !== null) {
0 ignored issues
show
introduced by
The condition $albumIds !== null is always true.
Loading history...
168
			$sql .= 'HAVING `album_id` IN ' . $this->questionMarks(\count($albumIds));
169
			$params = \array_merge($params, $albumIds);
170
		}
171
172
		$result = $this->execute($sql, $params);
173
		$diskCountByAlbum = [];
174
		while ($row = $result->fetch()) {
175
			$diskCountByAlbum[$row['album_id']] = (int)$row['disc_count'];
176
		}
177
		$result->closeCursor();
178
		return $diskCountByAlbum;
179
	}
180
181
	/**
182
	 * returns summed track play counts of each album of the user, omitting albums which have never been played
183
	 *
184
	 * @return array [int => int], keys are album IDs and values are play count sums; ordered largest counts first
185
	 */
186
	public function getAlbumTracksPlayCount(string $userId, ?int $limit=null, ?int $offset=null) : array {
187
		$sql = 'SELECT `album_id`, SUM(`play_count`) AS `sum_count`
188
				FROM `*PREFIX*music_tracks`
189
				WHERE `user_id` = ? AND `play_count` > 0
190
				GROUP BY `album_id`
191
				ORDER BY `sum_count` DESC, `album_id`'; // the second criterion is just to make the order predictable on even counts
192
193
		$result = $this->execute($sql, [$userId], $limit, $offset);
194
		$playCountByAlbum = [];
195
		while ($row = $result->fetch()) {
196
			$playCountByAlbum[$row['album_id']] = (int)$row['sum_count'];
197
		}
198
		$result->closeCursor();
199
		return $playCountByAlbum;
200
	}
201
202
	/**
203
	 * returns the latest play time of each album of the user, omitting albums which have never been played
204
	 *
205
	 * @return array [int => string], keys are album IDs and values are date-times; ordered latest times first
206
	 */
207
	public function getLatestAlbumPlayTimes(string $userId, ?int $limit=null, ?int $offset=null) : array {
208
		$sql = 'SELECT `album_id`, MAX(`last_played`) AS `latest_time`
209
				FROM `*PREFIX*music_tracks`
210
				WHERE `user_id` = ? AND `last_played` IS NOT NULL
211
				GROUP BY `album_id`
212
				ORDER BY `latest_time` DESC';
213
214
		$result = $this->execute($sql, [$userId], $limit, $offset);
215
		$latestTimeByAlbum = [];
216
		while ($row = $result->fetch()) {
217
			$latestTimeByAlbum[$row['album_id']] = $row['latest_time'];
218
		}
219
		$result->closeCursor();
220
		return $latestTimeByAlbum;
221
	}
222
223
	/**
224
	 * returns the latest play time of each album of the user, including albums which have never been played
225
	 *
226
	 * @return array [int => ?string], keys are album IDs and values are date-times (or null for never played);
227
	 *									ordered furthest times first
228
	 */
229
	public function getFurthestAlbumPlayTimes(string $userId, ?int $limit=null, ?int $offset=null) : array {
230
		$sql = 'SELECT `album_id`, MAX(`last_played`) AS `latest_time`
231
				FROM `*PREFIX*music_tracks`
232
				WHERE `user_id` = ?
233
				GROUP BY `album_id`
234
				ORDER BY `latest_time` ASC';
235
236
		$result = $this->execute($sql, [$userId], $limit, $offset);
237
		$latestTimeByAlbum = [];
238
		while ($row = $result->fetch()) {
239
			$latestTimeByAlbum[$row['album_id']] = $row['latest_time'];
240
		}
241
		$result->closeCursor();
242
		return $latestTimeByAlbum;
243
	}
244
245
	/**
246
	 * @return Album[]
247
	 */
248
	public function findAllByNameRecursive(string $name, string $userId, ?int $limit=null, ?int $offset=null) : array {
249
		$condition = '( LOWER(`artist`.`name`) LIKE LOWER(?) OR
250
						LOWER(`*PREFIX*music_albums`.`name`) LIKE LOWER(?) )';
251
		$sql = $this->selectUserEntities($condition, 'ORDER BY LOWER(`*PREFIX*music_albums`.`name`)');
252
		$name = BaseMapper::prepareSubstringSearchPattern($name);
253
		$params = [$userId, $name, $name];
254
		return $this->findEntities($sql, $params, $limit, $offset);
255
	}
256
257
	/**
258
	 * returns albums of a specified artist
259
	 * The artist may be an album_artist or the artist of a track
260
	 *
261
	 * @param integer $artistId
262
	 * @return Album[]
263
	 */
264
	public function findAllByArtist(int $artistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
265
		$sql = $this->selectEntities(
266
				'`*PREFIX*music_albums`.`id` IN (
267
					SELECT DISTINCT `album`.`id`
268
					FROM `*PREFIX*music_albums` `album`
269
					WHERE `album`.`album_artist_id` = ?
270
						UNION
271
					SELECT DISTINCT `track`.`album_id`
272
					FROM `*PREFIX*music_tracks` `track`
273
					WHERE `track`.`artist_id` = ?
274
				) AND `*PREFIX*music_albums`.`user_id` = ?',
275
				'ORDER BY LOWER(`*PREFIX*music_albums`.`name`)');
276
		$params = [$artistId, $artistId, $userId];
277
		return $this->findEntities($sql, $params, $limit, $offset);
278
	}
279
280
	/**
281
	 * returns albums of a specified artists
282
	 * The artist must be album_artist on the album, artists of individual tracks are not considered
283
	 *
284
	 * @param int[] $artistIds
285
	 * @return Album[]
286
	 */
287
	public function findAllByAlbumArtist(array $artistIds, string $userId, ?int $limit=null, ?int $offset=null) : array {
288
		$sql = $this->selectUserEntities('`album_artist_id` IN ' . $this->questionMarks(\count($artistIds)));
289
		$params = \array_merge([$userId], $artistIds);
290
		return $this->findEntities($sql, $params, $limit, $offset);
291
	}
292
293
	/**
294
	 * @return Album[]
295
	 */
296
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
297
		$sql = $this->selectUserEntities('EXISTS '.
298
				'(SELECT 1 FROM `*PREFIX*music_tracks` `track`
299
				  WHERE `*PREFIX*music_albums`.`id` = `track`.`album_id`
300
				  AND `track`.`genre_id` = ?)');
301
302
		$params = [$userId, $genreId];
303
		return $this->findEntities($sql, $params, $limit, $offset);
304
	}
305
306
	/**
307
	 * @return boolean True if one or more albums were influenced
308
	 */
309
	public function updateFolderCover(int $coverFileId, int $folderId) : bool {
310
		$sql = 'SELECT DISTINCT `tracks`.`album_id`
311
				FROM `*PREFIX*music_tracks` `tracks`
312
				JOIN `*PREFIX*filecache` `files` ON `tracks`.`file_id` = `files`.`fileid`
313
				WHERE `files`.`parent` = ?';
314
		$params = [$folderId];
315
		$result = $this->execute($sql, $params);
316
		$albumIds = $result->fetchAll(\PDO::FETCH_COLUMN);
317
		$result->closeCursor();
318
319
		$updated = false;
320
		if (\count($albumIds) > 0) {
321
			$sql = 'UPDATE `*PREFIX*music_albums`
322
					SET `cover_file_id` = ?
323
					WHERE `cover_file_id` IS NULL AND `id` IN '. $this->questionMarks(\count($albumIds));
324
			$params = \array_merge([$coverFileId], $albumIds);
325
			$result = $this->execute($sql, $params);
326
			$updated = $result->rowCount() > 0;
327
			$result->closeCursor();
328
		}
329
330
		return $updated;
331
	}
332
333
	/**
334
	 * Set file ID to be used as cover for an album
335
	 */
336
	public function setCover(?int $coverFileId, int $albumId) : void {
337
		$sql = 'UPDATE `*PREFIX*music_albums`
338
				SET `cover_file_id` = ?
339
				WHERE `id` = ?';
340
		$params = [$coverFileId, $albumId];
341
		$result = $this->execute($sql, $params);
342
		$result->closeCursor();
343
	}
344
345
	/**
346
	 * @param integer[] $coverFileIds
347
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
348
	 * @return Album[] albums which got modified (with incomplete data, only id and user are valid),
349
	 *         empty array if none
350
	 */
351
	public function removeCovers(array $coverFileIds, ?array $userIds=null) : array {
352
		// find albums using the given file as cover
353
		$sql = 'SELECT `id`, `user_id` FROM `*PREFIX*music_albums` WHERE `cover_file_id` IN ' .
354
			$this->questionMarks(\count($coverFileIds));
355
		$params = $coverFileIds;
356
		if ($userIds !== null) {
357
			$sql .= ' AND `user_id` IN ' . $this->questionMarks(\count($userIds));
358
			$params = \array_merge($params, $userIds);
359
		}
360
		$albums = $this->findEntities($sql, $params);
361
362
		// if any albums found, remove the cover from those
363
		$count = \count($albums);
364
		if ($count) {
365
			$sql = 'UPDATE `*PREFIX*music_albums`
366
				SET `cover_file_id` = NULL
367
				WHERE `id` IN ' . $this->questionMarks($count);
368
			$params = ArrayUtil::extractIds($albums);
369
			$result = $this->execute($sql, $params);
370
			$result->closeCursor();
371
		}
372
373
		return $albums;
374
	}
375
376
	/**
377
	 * @param string|null $userId target user; omit to target all users
378
	 * @param ?int[] $parentIds limit search to albums having tracks in these folders; omit for no limit
379
	 * @return array<array{albumId: int, userId: string, parentFolderId: int}>
380
	 */
381
	public function getAlbumsWithoutCover(?string $userId = null, ?array $parentIds = null) : array {
382
		$sql = 'SELECT DISTINCT `albums`.`id`, `albums`.`user_id`, `files`.`parent`
383
				FROM `*PREFIX*music_albums` `albums`
384
				JOIN `*PREFIX*music_tracks` `tracks` ON `albums`.`id` = `tracks`.`album_id`
385
				JOIN `*PREFIX*filecache` `files` ON `tracks`.`file_id` = `files`.`fileid`
386
				WHERE `albums`.`cover_file_id` IS NULL';
387
		$params = [];
388
		if ($userId !== null) {
389
			$sql .= ' AND `albums`.`user_id` = ?';
390
			$params[] = $userId;
391
		}
392
		if (!empty($parentIds)) {
393
			$sql .= ' AND `files`.`parent` IN ' . $this->questionMarks(\count($parentIds));
394
			$params = \array_merge($params, $parentIds);
395
		}
396
397
		$result = $this->execute($sql, $params);
398
		$return = [];
399
		while ($row = $result->fetch()) {
400
			$return[] = [
401
				'albumId' => (int)$row['id'],
402
				'userId' => $row['user_id'],
403
				'parentFolderId' => (int)$row['parent']
404
			];
405
		}
406
		$result->closeCursor();
407
408
		return $return;
409
	}
410
411
	/**
412
	 * @return boolean True if a cover image was found and added for the album
413
	 */
414
	public function findAlbumCover(int $albumId, int $parentFolderId) : bool {
415
		$return = false;
416
		$imagesSql = 'SELECT `fileid`, `name`
417
					FROM `*PREFIX*filecache`
418
					JOIN `*PREFIX*mimetypes` ON `*PREFIX*mimetypes`.`id` = `*PREFIX*filecache`.`mimetype`
419
					WHERE `parent` = ? AND `*PREFIX*mimetypes`.`mimetype` LIKE \'image%\'';
420
		$params = [$parentFolderId];
421
		$result = $this->execute($imagesSql, $params);
422
		$images = $result->fetchAll();
423
		$result->closeCursor();
424
		if (\count($images) > 0) {
425
			$getImageRank = function($imageName) {
426
				$coverNames = ['cover', 'albumart', 'album', 'front', 'folder'];
427
				foreach ($coverNames as $i => $coverName) {
428
					if (StringUtil::startsWith($imageName, $coverName, /*$ignoreCase=*/true)) {
429
						return $i;
430
					}
431
				}
432
				return \count($coverNames);
433
			};
434
435
			\usort($images, fn($imageA, $imageB) =>
436
				$getImageRank($imageA['name']) <=> $getImageRank($imageB['name'])
437
			);
438
			$imageId = (int)$images[0]['fileid'];
439
			$this->setCover($imageId, $albumId);
440
			$return = true;
441
		}
442
		return $return;
443
	}
444
445
	/**
446
	 * Given an array of track IDs, find corresponding unique album IDs, including only
447
	 * those album which have a cover art set.
448
	 * @param int[] $trackIds
449
	 * @return Album[]
450
	 */
451
	public function findAlbumsWithCoversForTracks(array $trackIds, string $userId, int $limit) : array {
452
		$sql = 'SELECT DISTINCT `albums`.*
453
				FROM `*PREFIX*music_albums` `albums`
454
				JOIN `*PREFIX*music_tracks` `tracks` ON `albums`.`id` = `tracks`.`album_id`
455
				WHERE `albums`.`cover_file_id` IS NOT NULL
456
				AND `albums`.`user_id` = ?
457
				AND `tracks`.`id` IN ' . $this->questionMarks(\count($trackIds));
458
		$params = \array_merge([$userId], $trackIds);
459
460
		return $this->findEntities($sql, $params, $limit);
461
	}
462
463
	/**
464
	 * Returns the count of albums where the given Artist is featured in
465
	 * @param integer $artistId
466
	 * @return integer
467
	 */
468
	public function countByArtist(int $artistId) : int {
469
		$sql = 'SELECT COUNT(*) AS count FROM (
470
					SELECT DISTINCT `track`.`album_id`
471
					FROM `*PREFIX*music_tracks` `track`
472
					WHERE `track`.`artist_id` = ?
473
						UNION
474
					SELECT `album`.`id`
475
					FROM `*PREFIX*music_albums` `album`
476
					WHERE `album`.`album_artist_id` = ?
477
				) tmp';
478
		$params = [$artistId, $artistId];
479
		$result = $this->execute($sql, $params);
480
		$row = $result->fetch();
481
		$result->closeCursor();
482
		return (int)$row['count'];
483
	}
484
485
	/**
486
	 * Returns the count of albums where the given artist is the album artist
487
	 * @param integer $artistId
488
	 * @return integer
489
	 */
490
	public function countByAlbumArtist(int $artistId) : int {
491
		$sql = 'SELECT COUNT(*) AS count
492
				FROM `*PREFIX*music_albums` `album`
493
				WHERE `album`.`album_artist_id` = ?';
494
		$params = [$artistId];
495
		$result = $this->execute($sql, $params);
496
		$row = $result->fetch();
497
		$result->closeCursor();
498
		return (int)$row['count'];
499
	}
500
501
	/**
502
	 * Overridden from the base implementation to provide support for table-specific rules
503
	 *
504
	 * {@inheritdoc}
505
	 * @see BaseMapper::advFormatSqlCondition()
506
	 */
507
	protected function advFormatSqlCondition(string $rule, string $sqlOp, string $conv) : string {
508
		// The extra subquery "mysqlhack" seen around some nested queries is needed in order for these to not be insanely slow on MySQL.
509
		// In case of 'recent_played', the MySQL 5.5.62 errored with "1235 This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" without the extra subquery.
510
		$condForRule = [
511
			'album_artist'	=> "$conv(`artist`.`name`) $sqlOp $conv(?)",
512
			'song_artist'	=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_artists` `ar` ON `t`.`artist_id` = `ar`.`id` WHERE $conv(`ar`.`name`) $sqlOp $conv(?))",
513
			'song'			=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` WHERE $conv(`t`.`title`) $sqlOp $conv(?))",
514
			'original_year'	=> "`*PREFIX*music_albums`.`id` IN (SELECT * FROM (SELECT `album_id` FROM `*PREFIX*music_tracks` GROUP BY `album_id` HAVING MIN(`year`) $sqlOp ?) mysqlhack)",
515
			'songrating'	=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` WHERE `t`.`rating` $sqlOp ?)",
516
			'artistrating'	=> "`artist`.rating $sqlOp ?",
517
			'played_times'	=> "`*PREFIX*music_albums`.`id` IN (SELECT * FROM (SELECT `album_id` from `*PREFIX*music_tracks` GROUP BY `album_id` HAVING SUM(`play_count`) $sqlOp ?) mysqlhack)",
518
			'last_play'		=> "`*PREFIX*music_albums`.`id` IN (SELECT * FROM (SELECT `album_id` from `*PREFIX*music_tracks` GROUP BY `album_id` HAVING MAX(`last_played`) $sqlOp ?) mysqlhack)",
519
			'myplayed'		=> "`*PREFIX*music_albums`.`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"
520
			'myplayedartist'=> "`album_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"
521
			'song_count'	=> "`*PREFIX*music_albums`.`id` IN (SELECT * FROM (SELECT `album_id` FROM `*PREFIX*music_tracks` GROUP BY `album_id` HAVING COUNT(`id`) $sqlOp ?) mysqlhack)",
522
			'disk_count'	=> "`*PREFIX*music_albums`.`id` IN (SELECT * FROM (SELECT `album_id` FROM `*PREFIX*music_tracks` GROUP BY `album_id` HAVING MAX(`disk`) $sqlOp ?) mysqlhack)",
523
			'time'			=> "`*PREFIX*music_albums`.`id` IN (SELECT * FROM (SELECT `album_id` FROM `*PREFIX*music_tracks` GROUP BY `album_id` HAVING SUM(`length`) $sqlOp ?) mysqlhack)",
524
			'album_genre'	=> "`*PREFIX*music_albums`.`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)",
525
			'song_genre'	=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_genres` `g` ON `t`.`genre_id` = `g`.`id` WHERE $conv(`g`.`name`) $sqlOp $conv(?))",
526
			'no_genre'		=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_genres` `g` ON `t`.`genre_id` = `g`.`id` WHERE `g`.`name` " . (($sqlOp == 'IS NOT NULL') ? '=' : '!=') . ' "")',
527
			'playlist'		=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` WHERE $sqlOp EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE `p`.`id` = ? AND `p`.`track_ids` LIKE " . $this->sqlConcat("'%|'", "`t`.`id`", "'|%'") . '))',
528
			'playlist_name'	=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` WHERE EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE $conv(`p`.`name`) $sqlOp $conv(?) AND `p`.`track_ids` LIKE " . $this->sqlConcat("'%|'", "`t`.`id`", "'|%'") . '))',
529
			'file'			=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*filecache` `f` ON `t`.`file_id` = `f`.`fileid` WHERE $conv(`f`.`name`) $sqlOp $conv(?))",
530
			'recent_played'	=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM (SELECT `album_id`, MAX(`last_played`) FROM `*PREFIX*music_tracks` WHERE `user_id` = ? GROUP BY `album_id` ORDER BY MAX(`last_played`) DESC LIMIT $sqlOp) mysqlhack)",
531
			'mbid_song'		=> "`*PREFIX*music_albums`.`id` IN (SELECT `album_id` FROM `*PREFIX*music_tracks` `t` WHERE `t`.`mbid` $sqlOp ?)",
532
			'mbid_artist'	=> "`artist`.`mbid` $sqlOp ?",
533
			'has_image'		=> "`*PREFIX*music_albums`.`cover_file_id` $sqlOp" // operator "IS NULL" or "IS NOT NULL"
534
		];
535
536
		// Add alias rules
537
		$condForRule['year'] = $condForRule['original_year'];	// we only have one kind of year
538
		$condForRule['played'] = $condForRule['myplayed'];		// we give no access to other people's data; not part of the API spec but Ample uses this
539
		$condForRule['artist'] = $condForRule['album_artist'];
540
		$condForRule['genre'] = $condForRule['album_genre'];
541
		$condForRule['album'] = parent::advFormatSqlCondition('title', $sqlOp, $conv);
542
		$condForRule['mbid_album'] = parent::advFormatSqlCondition('mbid', $sqlOp, $conv);
543
544
		return $condForRule[$rule] ?? parent::advFormatSqlCondition($rule, $sqlOp, $conv);
545
	}
546
}
547