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