Passed
Push — master ( c7cb4e...ce9a66 )
by Pauli
03:03
created

TrackMapper::markTracksDirty()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

561
		return $this->findByFileId($track->/** @scrutinizer ignore-call */ getFileId(), $track->getUserId());
Loading history...
Bug introduced by
It seems like $track->getFileId() can also be of type null; however, parameter $fileId of OCA\Music\Db\TrackMapper::findByFileId() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

561
		return $this->findByFileId(/** @scrutinizer ignore-type */ $track->getFileId(), $track->getUserId());
Loading history...
562
	}
563
}
564