Passed
Push — master ( 027858...d29526 )
by Pauli
02:14
created

AlbumMapper::findAllByNameRecursive()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

483
		return $this->findEntity($sql, [$album->getUserId(), $album->/** @scrutinizer ignore-call */ getHash()]);
Loading history...
Bug Best Practice introduced by
The expression return $this->findEntity...(), $album->getHash())) returns the type OCP\AppFramework\Db\Entity which includes types incompatible with the type-hinted return OCA\Music\Db\Entity.
Loading history...
484
	}
485
}
486