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

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