Passed
Pull Request — master (#1078)
by Pauli
05:42 queued 02:56
created

ArtistMapper::findAllHavingAlbums()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 3
eloc 12
nc 4
nop 10
dl 0
loc 21
rs 9.8666
c 2
b 0
f 1

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 - 2023
13
 */
14
15
namespace OCA\Music\Db;
16
17
use OCA\Music\Utility\Util;
18
19
use OCP\IDBConnection;
20
21
/**
22
 * @phpstan-extends BaseMapper<Artist>
23
 */
24
class ArtistMapper extends BaseMapper {
25
	public function __construct(IDBConnection $db) {
26
		parent::__construct($db, 'music_artists', Artist::class, 'name');
27
	}
28
29
	/**
30
	 * @param string $userId
31
	 * @param integer $sortBy sort order of the result set
32
	 * @return Artist[]
33
	 */
34
	public function findAllHavingAlbums(string $userId, int $sortBy=SortBy::None,
35
			?int $limit=null, ?int $offset=null, ?string $name=null, int $matchMode=MatchMode::Exact,
36
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
37
		$params = [$userId];
38
		$condition = $this->formatExcludeChildlessCondition();
39
40
		if ($name !== null) {
41
			[$nameCond, $nameParams] = $this->formatNameConditions($name, $matchMode);
42
			$condition .= " AND $nameCond";
43
			$params = \array_merge($params, $nameParams);
44
		}
45
46
		[$timestampConds, $timestampParams] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
47
		if (!empty($timestampConds)) {
48
			$condition .= " AND $timestampConds";
49
			$params = \array_merge($params, $timestampParams);
50
		}
51
52
		$sql = $this->selectUserEntities($condition, $this->formatSortingClause($sortBy));
53
54
		return $this->findEntities($sql, $params, $limit, $offset);
55
	}
56
57
	/**
58
	 * @param int $genreId
59
	 * @param string $userId
60
	 * @param int|null $limit
61
	 * @param int|null $offset
62
	 * @return Artist[]
63
	 */
64
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
65
		$sql = $this->selectUserEntities('EXISTS '.
66
				'(SELECT 1 FROM `*PREFIX*music_tracks` `track`
67
				  WHERE `*PREFIX*music_artists`.`id` = `track`.`artist_id`
68
				  AND `track`.`genre_id` = ?)');
69
70
		$params = [$userId, $genreId];
71
		return $this->findEntities($sql, $params, $limit, $offset);
72
	}
73
74
	/**
75
	 * returns summed track play counts of each aritst of the user, omittig artists which have never been played
76
	 *
77
	 * @return array [int => int], keys are artist IDs and values are play count sums; ordered largest counts first
78
	 */
79
	public function getArtistTracksPlayCount(string $userId, ?int $limit=null, ?int $offset=null) : array {
80
		$sql = 'SELECT `artist_id`, SUM(`play_count`) AS `sum_count`
81
				FROM `*PREFIX*music_tracks`
82
				WHERE `user_id` = ? AND `play_count` > 0
83
				GROUP BY `artist_id`
84
				ORDER BY `sum_count` DESC, `artist_id`'; // the second criterion is just to make the order predictable on even counts
85
86
		$result = $this->execute($sql, [$userId], $limit, $offset);
87
		$playCountByArtist = [];
88
		while ($row = $result->fetch()) {
89
			$playCountByArtist[$row['artist_id']] = (int)$row['sum_count'];
90
		}
91
		return $playCountByArtist;
92
	}
93
94
	/**
95
	 * returns the latest play time of each artist of the user, omittig artists which have never been played
96
	 *
97
	 * @return array [int => string], keys are artist IDs and values are date-times; ordered latest times first
98
	 */
99
	public function getLatestArtistPlayTimes(string $userId, ?int $limit=null, ?int $offset=null) : array {
100
		$sql = 'SELECT `artist_id`, MAX(`last_played`) AS `latest_time`
101
				FROM `*PREFIX*music_tracks`
102
				WHERE `user_id` = ? AND `last_played` IS NOT NULL
103
				GROUP BY `artist_id`
104
				ORDER BY `latest_time` DESC';
105
106
		$result = $this->execute($sql, [$userId], $limit, $offset);
107
		$latestTimeByArtist = [];
108
		while ($row = $result->fetch()) {
109
			$latestTimeByArtist[$row['artist_id']] = $row['latest_time'];
110
		}
111
		return $latestTimeByArtist;
112
	}
113
114
	/**
115
	 * returns the latest play time of each artist of the user, including artists which have never been played
116
	 *
117
	 * @return array [int => ?string], keys are artist IDs and values are date-times (or null for never played);
118
	 *									ordered furthest times first
119
	 */
120
	public function getFurthestArtistPlayTimes(string $userId, ?int $limit=null, ?int $offset=null) : array {
121
		$sql = 'SELECT `artist_id`, MAX(`last_played`) AS `latest_time`
122
				FROM `*PREFIX*music_tracks`
123
				WHERE `user_id` = ?
124
				GROUP BY `artist_id`
125
				ORDER BY `latest_time` ASC';
126
127
		$result = $this->execute($sql, [$userId], $limit, $offset);
128
		$latestTimeByArtist = [];
129
		while ($row = $result->fetch()) {
130
			$latestTimeByArtist[$row['artist_id']] = $row['latest_time'];
131
		}
132
		return $latestTimeByArtist;
133
	}
134
135
	/**
136
	 * @param integer[] $coverFileIds
137
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
138
	 * @return Artist[] artists which got modified (with incomplete data, only id and user are valid),
139
	 *         empty array if none
140
	 */
141
	public function removeCovers(array $coverFileIds, ?array $userIds=null) : array {
142
		// find albums using the given file as cover
143
		$sql = 'SELECT `id`, `user_id` FROM `*PREFIX*music_artists` WHERE `cover_file_id` IN ' .
144
		$this->questionMarks(\count($coverFileIds));
145
		$params = $coverFileIds;
146
		if ($userIds !== null) {
147
			$sql .= ' AND `user_id` IN ' . $this->questionMarks(\count($userIds));
148
			$params = \array_merge($params, $userIds);
149
		}
150
		$artists = $this->findEntities($sql, $params);
151
152
		// if any artists found, remove the cover from those
153
		$count = \count($artists);
154
		if ($count) {
155
			$sql = 'UPDATE `*PREFIX*music_artists`
156
					SET `cover_file_id` = NULL
157
					WHERE `id` IN ' . $this->questionMarks($count);
158
			$params = Util::extractIds($artists);
159
			$this->execute($sql, $params);
160
		}
161
162
		return $artists;
163
	}
164
165
	/**
166
	 * Overridden from the base implementation
167
	 *
168
	 * @see BaseMapper::formatExcludeChildlessCondition()
169
	 */
170
	protected function formatExcludeChildlessCondition() : string {
171
		return 'EXISTS (SELECT 1 FROM `*PREFIX*music_albums` `album` WHERE `*PREFIX*music_artists`.`id` = `album`.`album_artist_id`)';
172
	}
173
174
	/**
175
	 * Overridden from the base implementation to provide support for table-specific rules
176
	 *
177
	 * {@inheritdoc}
178
	 * @see BaseMapper::advFormatSqlCondition()
179
	 */
180
	protected function advFormatSqlCondition(string $rule, string $sqlOp) : string {
181
		// The extra subquery "mysqlhack" seen around some nested queries is needed in order for these to not be insanely slow on MySQL.
182
		// In case of 'recent_played', the MySQL 5.5.62 errored with "1235 This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" without the extra subquery.
183
		switch ($rule) {
184
			case 'album':			return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_albums` `a` ON `t`.`album_id` = `a`.`id` WHERE LOWER(`a`.`name`) $sqlOp LOWER(?))";
185
			case 'artist':			return parent::advFormatSqlCondition('name', $sqlOp); // alias
186
			case 'song':			return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE LOWER(`t`.`title`) $sqlOp LOWER(?))";
187
			case 'songrating':		return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE `t`.`rating` $sqlOp ?)";
188
			case 'albumrating':		return "`*PREFIX*music_artists`.`id` IN (SELECT `album_artist_id` from `*PREFIX*music_albums` `al` WHERE `al`.`rating` $sqlOp ?)";
189
			case 'played_times':	return "`*PREFIX*music_artists`.`id` IN (SELECT * FROM (SELECT `artist_id` from `*PREFIX*music_tracks` GROUP BY `artist_id` HAVING SUM(`play_count`) $sqlOp ?) mysqlhack)";
190
			case 'last_play':		return "`*PREFIX*music_artists`.`id` IN (SELECT * FROM (SELECT `artist_id` from `*PREFIX*music_tracks` GROUP BY `artist_id` HAVING MAX(`last_played`) $sqlOp ?) mysqlhack)";
191
			case 'played':			// fall through, we give no access to other people's data; not part of the API spec but Ample uses this
192
			case 'myplayed':		return "`*PREFIX*music_artists`.`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"
193
			case 'album_count':		return "`*PREFIX*music_artists`.`id` IN (SELECT * FROM (SELECT `album_artist_id` FROM `*PREFIX*music_albums` GROUP BY `album_artist_id` HAVING COUNT(`id`) $sqlOp ?) mysqlhack)";
194
			case 'song_count':		return "`*PREFIX*music_artists`.`id` IN (SELECT * FROM (SELECT `artist_id` FROM `*PREFIX*music_tracks` GROUP BY `artist_id` HAVING COUNT(`id`) $sqlOp ?) mysqlhack)";
195
			case 'time':			return "`*PREFIX*music_artists`.`id` IN (SELECT * FROM (SELECT `artist_id` FROM `*PREFIX*music_tracks` GROUP BY `artist_id` HAVING SUM(`length`) $sqlOp ?) mysqlhack)";
196
			case 'genre':			return "`*PREFIX*music_artists`.`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 LOWER(GROUP_CONCAT(`g`.`name`)) $sqlOp LOWER(?)) mysqlhack)"; // GROUP_CONCAT not available on PostgreSQL
197
			case 'song_genre':		return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_genres` `g` ON `t`.`genre_id` = `g`.`id` WHERE LOWER(`g`.`name`) $sqlOp LOWER(?))";
198
			case 'no_genre':		return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_genres` `g` ON `t`.`genre_id` = `g`.`id` WHERE `g`.`name` " . (($sqlOp == 'IS NOT NULL') ? '=' : '!=') . ' "")';
199
			case 'playlist':		return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE $sqlOp EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE `p`.`id` = ? AND `p`.`track_ids` LIKE CONCAT('%|',`t`.`id`, '|%')))";
200
			case 'playlist_name':	return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE EXISTS (SELECT 1 from `*PREFIX*music_playlists` `p` WHERE `p`.`name` $sqlOp ? AND `p`.`track_ids` LIKE CONCAT('%|',`t`.`id`, '|%')))";
201
			case 'file':			return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*filecache` `f` ON `t`.`file_id` = `f`.`fileid` WHERE LOWER(`f`.`name`) $sqlOp LOWER(?))";
202
			case 'recent_played':	return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM (SELECT `artist_id`, MAX(`last_played`) FROM `*PREFIX*music_tracks` WHERE `user_id` = ? GROUP BY `artist_id` ORDER BY MAX(`last_played`) DESC LIMIT $sqlOp) mysqlhack)";
203
			case 'mbid_artist':		return parent::advFormatSqlCondition('mbid', $sqlOp); // alias
204
			case 'mbid_song':		return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE `t`.`mbid` $sqlOp ?)";
205
			case 'mbid_album':		return "`*PREFIX*music_artists`.`id` IN (SELECT `album_artist_id` from `*PREFIX*music_albums` `al` WHERE `al`.`mbid` $sqlOp ?)";
206
			case 'has_image':		return "`cover_file_id` $sqlOp"; // operator "IS NULL" or "IS NOT NULL"
207
			default:				return parent::advFormatSqlCondition($rule, $sqlOp);
208
		}
209
	}
210
211
	/**
212
	 * {@inheritdoc}
213
	 * @see \OCA\Music\Db\BaseMapper::findUniqueEntity()
214
	 * @param Artist $artist
215
	 * @return Artist
216
	 */
217
	protected function findUniqueEntity(Entity $artist) : Entity {
218
		$sql = $this->selectUserEntities('`hash` = ?');
219
		return $this->findEntity($sql, [$artist->getUserId(), $artist->getHash()]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findEntity...), $artist->getHash())) returns the type OCP\AppFramework\Db\Entity which includes types incompatible with the type-hinted return OCA\Music\Db\Entity.
Loading history...
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

219
		return $this->findEntity($sql, [$artist->getUserId(), $artist->/** @scrutinizer ignore-call */ getHash()]);
Loading history...
220
	}
221
}
222