Passed
Push — master ( d93244...31d6f2 )
by Pauli
03:37
created

ArtistMapper::findUniqueEntity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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