Passed
Push — master ( 91b14a...fc89b7 )
by Pauli
02:49
created

ArtistMapper::getLatestArtistPlayTimes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

243
		return $this->findEntity($sql, [$artist->getUserId(), $artist->/** @scrutinizer ignore-call */ getHash()]);
Loading history...
244
	}
245
}
246