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

ArtistMapper::advFormatSqlCondition()   D

Complexity

Conditions 25
Paths 25

Size

Total Lines 28
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 25
eloc 25
nc 25
nop 2
dl 0
loc 28
rs 4.1666
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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