Passed
Push — master ( 4b5769...35b363 )
by Pauli
12:54
created

TrackMapper::findAllByNameAndArtistName()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

392
		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

392
		return $this->findByFileId(/** @scrutinizer ignore-type */ $track->getFileId(), $track->getUserId());
Loading history...
393
	}
394
}
395