Passed
Push — master ( 3c5a6f...e94730 )
by Pauli
09:42 queued 11s
created

TrackMapper::findAllByGenre()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 4
dl 0
loc 4
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 - 2021
13
 */
14
15
namespace OCA\Music\Db;
16
17
use \OCP\IDBConnection;
18
19
/**
20
 * @phpstan-extends BaseMapper<Track>
21
 */
22
class TrackMapper extends BaseMapper {
23
	public function __construct(IDBConnection $db) {
24
		parent::__construct($db, 'music_tracks', Track::class, 'title');
25
	}
26
27
	/**
28
	 * Override the base implementation to include data from multiple tables
29
	 *
30
	 * {@inheritdoc}
31
	 * @see BaseMapper::selectEntities()
32
	 */
33
	protected function selectEntities(string $condition, string $extension=null) : string {
34
		return "SELECT `*PREFIX*music_tracks`.*, `file`.`name` AS `filename`, `file`.`size`,
35
						`album`.`name` AS `album_name`, `artist`.`name` AS `artist_name`, `genre`.`name` AS `genre_name`
36
				FROM `*PREFIX*music_tracks`
37
				INNER JOIN `*PREFIX*filecache` `file`
38
				ON `*PREFIX*music_tracks`.`file_id` = `file`.`fileid`
39
				INNER JOIN `*PREFIX*music_albums` `album`
40
				ON `*PREFIX*music_tracks`.`album_id` = `album`.`id`
41
				INNER JOIN `*PREFIX*music_artists` `artist`
42
				ON `*PREFIX*music_tracks`.`artist_id` = `artist`.`id`
43
				LEFT JOIN `*PREFIX*music_genres` `genre`
44
				ON `*PREFIX*music_tracks`.`genre_id` = `genre`.`id`
45
				WHERE $condition $extension";
46
	}
47
48
	/**
49
	 * Overridden from the base implementation to add support for sorting by artist.
50
	 *
51
	 * {@inheritdoc}
52
	 * @see BaseMapper::formatSortingClause()
53
	 */
54
	protected function formatSortingClause(int $sortBy) : ?string {
55
		if ($sortBy === SortBy::Parent) {
56
			return 'ORDER BY LOWER(`artist_name`), LOWER(`title`)';
57
		} else {
58
			return parent::formatSortingClause($sortBy);
59
		}
60
	}
61
62
	/**
63
	 * Returns all tracks of the given artist (both album and track artists are considered)
64
	 * @param integer $artistId
65
	 * @param string $userId
66
	 * @return Track[]
67
	 */
68
	public function findAllByArtist($artistId, $userId) {
69
		$sql = $this->selectUserEntities(
70
				'`artist_id` = ? OR `album_id` IN (SELECT `id` from `*PREFIX*music_albums` WHERE `album_artist_id` = ?) ',
71
				'ORDER BY LOWER(`title`)');
72
		$params = [$userId, $artistId, $artistId];
73
		return $this->findEntities($sql, $params);
74
	}
75
76
	/**
77
	 * @param integer $albumId
78
	 * @param string $userId
79
	 * @param integer|null $artistId
80
	 * @return Track[]
81
	 */
82
	public function findAllByAlbum($albumId, $userId, $artistId = null) {
83
		$condition = '`album_id` = ?';
84
		$params = [$userId, $albumId];
85
86
		if ($artistId !== null) {
87
			$condition .= ' AND `artist_id` = ? ';
88
			$params[] = $artistId;
89
		}
90
91
		$sql = $this->selectUserEntities($condition,
92
				'ORDER BY `*PREFIX*music_tracks`.`disk`, `number`, LOWER(`title`)');
93
		return $this->findEntities($sql, $params);
94
	}
95
96
	/**
97
	 * @param integer $folderId
98
	 * @param string $userId
99
	 * @return Track[]
100
	 */
101
	public function findAllByFolder($folderId, $userId) {
102
		$sql = $this->selectUserEntities('`file`.`parent` = ?', 'ORDER BY LOWER(`title`)');
103
		$params = [$userId, $folderId];
104
		return $this->findEntities($sql, $params);
105
	}
106
107
	/**
108
	 * @param int $genreId
109
	 * @param string $userId
110
	 * @param int|null $limit
111
	 * @param int|null $offset
112
	 * @return Track[] tracks
113
	 */
114
	public function findAllByGenre($genreId, $userId, $limit=null, $offset=null) {
115
		$sql = $this->selectUserEntities('`genre_id` = ?', 'ORDER BY LOWER(`title`)');
116
		$params = [$userId, $genreId];
117
		return $this->findEntities($sql, $params, $limit, $offset);
118
	}
119
120
	/**
121
	 * @param string $userId
122
	 * @return int[]
123
	 */
124
	public function findAllFileIds($userId) {
125
		$sql = 'SELECT `file_id` FROM `*PREFIX*music_tracks` WHERE `user_id` = ?';
126
		$result = $this->execute($sql, [$userId]);
127
128
		return \array_map(function ($i) {
129
			return (int)$i['file_id'];
130
		}, $result->fetchAll());
131
	}
132
133
	/**
134
	 * Find a track of user matching a file ID
135
	 * @param integer $fileId
136
	 * @param string $userId
137
	 * @return Track
138
	 * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
139
	 */
140
	public function findByFileId($fileId, $userId) {
141
		$sql = $this->selectUserEntities('`file_id` = ?');
142
		$params = [$userId, $fileId];
143
		return $this->findEntity($sql, $params);
144
	}
145
146
	/**
147
	 * Find tracks of user with multiple file IDs
148
	 * @param integer[] $fileIds
149
	 * @param string[] $userIds
150
	 * @return Track[]
151
	 */
152
	public function findByFileIds($fileIds, $userIds) {
153
		$sql = $this->selectEntities(
154
				'`*PREFIX*music_tracks`.`user_id` IN ' . $this->questionMarks(\count($userIds)) .
155
				' AND `file_id` IN '. $this->questionMarks(\count($fileIds)));
156
		$params = \array_merge($userIds, $fileIds);
157
		return $this->findEntities($sql, $params);
158
	}
159
160
	/**
161
	 * Finds tracks of all users matching one or multiple file IDs
162
	 * @param integer[] $fileIds
163
	 * @return Track[]
164
	 */
165
	public function findAllByFileIds($fileIds) {
166
		$sql = $this->selectEntities('`file_id` IN '.
167
				$this->questionMarks(\count($fileIds)));
168
		return $this->findEntities($sql, $fileIds);
169
	}
170
171
	/**
172
	 * @param integer $artistId
173
	 * @return integer
174
	 */
175
	public function countByArtist($artistId) {
176
		$sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `artist_id` = ?';
177
		$result = $this->execute($sql, [$artistId]);
178
		$row = $result->fetch();
179
		return (int)$row['count'];
180
	}
181
182
	/**
183
	 * @param integer $albumId
184
	 * @return integer
185
	 */
186
	public function countByAlbum($albumId) {
187
		$sql = 'SELECT COUNT(*) AS `count` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?';
188
		$result = $this->execute($sql, [$albumId]);
189
		$row = $result->fetch();
190
		return (int)$row['count'];
191
	}
192
193
	/**
194
	 * @param integer $albumId
195
	 * @return integer Duration in seconds
196
	 */
197
	public function totalDurationOfAlbum($albumId) {
198
		$sql = 'SELECT SUM(`length`) AS `duration` FROM `*PREFIX*music_tracks` WHERE `album_id` = ?';
199
		$result = $this->execute($sql, [$albumId]);
200
		$row = $result->fetch();
201
		return (int)$row['duration'];
202
	}
203
204
	/**
205
	 * Get durations of the given tracks.
206
	 * @param integer[] $trackIds
207
	 * @return array {int => int} where keys are track IDs and values are corresponding durations
208
	 */
209
	public function getDurations($trackIds) {
210
		$result = [];
211
212
		if (!empty($trackIds)) {
213
			$sql = 'SELECT `id`, `length` FROM `*PREFIX*music_tracks` WHERE `id` IN ' .
214
						$this->questionMarks(\count($trackIds));
215
			$rows = $this->execute($sql, $trackIds)->fetchAll();
216
			foreach ($rows as $row) {
217
				$result[$row['id']] = (int)$row['length'];
218
			}
219
		}
220
		return $result;
221
	}
222
223
	/**
224
	 * @param string $name
225
	 * @param string $userId
226
	 * @return Track[]
227
	 */
228
	public function findAllByNameRecursive($name, $userId) {
229
		$condition = '( LOWER(`artist`.`name`) LIKE LOWER(?) OR
230
						LOWER(`album`.`name`) LIKE LOWER(?) OR
231
						LOWER(`title`) LIKE LOWER(?) )';
232
		$sql = $this->selectUserEntities($condition, 'ORDER BY LOWER(`title`)');
233
		$name = '%' . $name . '%';
234
		$params = [$userId, $name, $name, $name];
235
		return $this->findEntities($sql, $params);
236
	}
237
238
	/**
239
	 * Returns all tracks specified by name and/or artist name
240
	 * @param string|null $name the name of the track
241
	 * @param string|null $artistName the name of the artist
242
	 * @param bool $fuzzy match names using case-insensitive substring search
243
	 * @param string $userId the name of the user
244
	 * @return \OCA\Music\Db\Track[] Tracks matching the criteria
245
	 */
246
	public function findAllByNameAndArtistName($name, $artistName, $fuzzy, $userId) {
247
		$sqlConditions = [];
248
		$params = [$userId];
249
250
		if (!empty($name)) {
251
			if ($fuzzy) {
252
				$sqlConditions[] = 'LOWER(`title`) LIKE LOWER(?)';
253
				$params[] = "%$name%";
254
			} else {
255
				$sqlConditions[] = '`title` = ?';
256
				$params[] = $name;
257
			}
258
		}
259
260
		if (!empty($artistName)) {
261
			if ($fuzzy) {
262
				$sqlConditions[] = 'LOWER(`artist`.`name`) LIKE LOWER(?)';
263
				$params[] = "%$artistName%";
264
			} else {
265
				$sqlConditions[] = '`artist`.`name` = ?';
266
				$params[] = $artistName;
267
			}
268
		}
269
270
		// at least one condition has to be given, otherwise return an empty set
271
		if (\count($sqlConditions) > 0) {
272
			$sql = $this->selectUserEntities(\implode(' AND ', $sqlConditions));
273
			return $this->findEntities($sql, $params);
274
		} else {
275
			return [];
276
		}
277
	}
278
279
	/**
280
	 * Find most frequently played tracks
281
	 * @return Track[]
282
	 */
283
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
284
		$sql = $this->selectUserEntities('`play_count` > 0', 'ORDER BY `play_count` DESC, LOWER(`title`)');
285
		return $this->findEntities($sql, [$userId], $limit, $offset);
286
	}
287
288
	/**
289
	 * Find most recently played tracks
290
	 * @return Track[]
291
	 */
292
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
293
		$sql = $this->selectUserEntities('`last_played` IS NOT NULL', 'ORDER BY `last_played` DESC');
294
		return $this->findEntities($sql, [$userId], $limit, $offset);
295
	}
296
297
	/**
298
	 * Find least recently played tracks
299
	 * @return Track[]
300
	 */
301
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
302
		$sql = $this->selectUserEntities(null, 'ORDER BY `last_played` ASC');
303
		return $this->findEntities($sql, [$userId], $limit, $offset);
304
	}
305
306
	/**
307
	 * Finds all track IDs of the user along with the parent folder ID of each track
308
	 * @param string $userId
309
	 * @return array where keys are folder IDs and values are arrays of track IDs
310
	 */
311
	public function findTrackAndFolderIds($userId) {
312
		$sql = 'SELECT `track`.`id` AS id, `file`.`name` AS `filename`, `file`.`parent` AS parent
313
				FROM `*PREFIX*music_tracks` `track`
314
				JOIN `*PREFIX*filecache` `file`
315
				ON `track`.`file_id` = `file`.`fileid`
316
				WHERE `track`.`user_id` = ?';
317
318
		$rows = $this->execute($sql, [$userId])->fetchAll();
319
320
		// Sort the results according the file names. This can't be made using ORDERBY in the
321
		// SQL query because then we couldn't use the "natural order" comparison algorithm
322
		\usort($rows, function ($a, $b) {
323
			return \strnatcasecmp($a['filename'], $b['filename']);
324
		});
325
326
		// group the files to parent folder "buckets"
327
		$result = [];
328
		foreach ($rows as $row) {
329
			$result[(int)$row['parent']][] = (int)$row['id'];
330
		}
331
332
		return $result;
333
	}
334
335
	/**
336
	 * Find names and parents of the file system nodes with given IDs within the given storage
337
	 * @param int[] $nodeIds
338
	 * @param string $storageId
339
	 * @return array where keys are the node IDs and values are associative arrays
340
	 *         like { 'name' => string, 'parent' => int };
341
	 */
342
	public function findNodeNamesAndParents($nodeIds, $storageId) {
343
		$result = [];
344
345
		if (!empty($nodeIds)) {
346
			$sql = 'SELECT `fileid`, `name`, `parent` '.
347
					'FROM `*PREFIX*filecache` `filecache` '.
348
					'JOIN `*PREFIX*storages` `storages` '.
349
					'ON `filecache`.`storage` = `storages`.`numeric_id` '.
350
					'WHERE `storages`.`id` = ? '.
351
					'AND `filecache`.`fileid` IN '. $this->questionMarks(\count($nodeIds));
352
353
			$rows = $this->execute($sql, \array_merge([$storageId], $nodeIds))->fetchAll();
354
355
			foreach ($rows as $row) {
356
				$result[$row['fileid']] = [
357
					'name' => $row['name'],
358
					'parent' => (int)$row['parent']
359
				];
360
			}
361
		}
362
363
		return $result;
364
	}
365
366
	/**
367
	 * Returns all genre IDs associated with the given artist
368
	 * @param int $artistId
369
	 * @param string $userId
370
	 * @return int[]
371
	 */
372
	public function getGenresByArtistId($artistId, $userId) {
373
		$sql = 'SELECT DISTINCT(`genre_id`) FROM `*PREFIX*music_tracks` WHERE
374
				`genre_id` IS NOT NULL AND `user_id` = ? AND `artist_id` = ?';
375
		$rows = $this->execute($sql, [$userId, $artistId]);
376
		return $rows->fetchAll(\PDO::FETCH_COLUMN);
377
	}
378
379
	/**
380
	 * Returns all tracks IDs of the user, organized by the genre_id.
381
	 * @param string $userId
382
	 * @return array where keys are genre IDs and values are arrays of track IDs
383
	 */
384
	public function mapGenreIdsToTrackIds($userId) {
385
		$sql = 'SELECT `id`, `genre_id` FROM `*PREFIX*music_tracks`
386
				WHERE `genre_id` IS NOT NULL and `user_id` = ?';
387
		$rows = $this->execute($sql, [$userId])->fetchAll();
388
389
		$result = [];
390
		foreach ($rows as $row) {
391
			$result[(int)$row['genre_id']][] = (int)$row['id'];
392
		}
393
394
		return $result;
395
	}
396
397
	/**
398
	 * Returns file IDs of the tracks which do not have genre scanned. This is not the same
399
	 * thing as unknown genre, which means that the genre has been scanned but was not found
400
	 * from the track metadata.
401
	 * @param string $userId
402
	 * @return int[]
403
	 */
404
	public function findFilesWithoutScannedGenre($userId) {
405
		$sql = 'SELECT `track`.`file_id` FROM `*PREFIX*music_tracks` `track`
406
				INNER JOIN `*PREFIX*filecache` `file`
407
				ON `track`.`file_id` = `file`.`fileid`
408
				WHERE `genre_id` IS NULL and `user_id` = ?';
409
		$rows = $this->execute($sql, [$userId]);
410
		return $rows->fetchAll(\PDO::FETCH_COLUMN);
411
	}
412
413
	/**
414
	 * Update "last played" timestamp and increment the total play count of the track.
415
	 * The DB row is updated *without* updating the `updated` column.
416
	 * @return bool true if the track was found and updated, false otherwise
417
	 */
418
	public function recordTrackPlayed(int $trackId, string $userId, \DateTime $timeOfPlay) : bool {
419
		$sql = 'UPDATE `*PREFIX*music_tracks`
420
				SET `last_played` = ?, `play_count` = `play_count` + 1
421
				WHERE `user_id` = ? AND `id` = ?';
422
		$params = [$timeOfPlay->format(BaseMapper::SQL_DATE_FORMAT), $userId, $trackId];
423
		$result = $this->execute($sql, $params);
424
		return ($result->rowCount() > 0);
425
	}
426
427
	/**
428
	 * @see \OCA\Music\Db\BaseMapper::findUniqueEntity()
429
	 * @param Track $track
430
	 * @return Track
431
	 */
432
	protected function findUniqueEntity(Entity $track) : Entity {
433
		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

433
		return $this->findByFileId($track->/** @scrutinizer ignore-call */ getFileId(), $track->getUserId());
Loading history...
434
	}
435
}
436