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, ?int $limit=null, ?int $offset=null) : array { |
35
|
|
|
$sql = $this->selectUserEntities('EXISTS '. |
36
|
|
|
'(SELECT 1 FROM `*PREFIX*music_albums` `album` '. |
37
|
|
|
' WHERE `*PREFIX*music_artists`.`id` = `album`.`album_artist_id`)', |
38
|
|
|
($sortBy == SortBy::Name) ? 'ORDER BY LOWER(`name`)' : null); |
39
|
|
|
|
40
|
|
|
$params = [$userId]; |
41
|
|
|
return $this->findEntities($sql, $params, $limit, $offset); |
|
|
|
|
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @param int $genreId |
46
|
|
|
* @param string $userId |
47
|
|
|
* @param int|null $limit |
48
|
|
|
* @param int|null $offset |
49
|
|
|
* @return Artist[] |
50
|
|
|
*/ |
51
|
|
|
public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array { |
52
|
|
|
$sql = $this->selectUserEntities('EXISTS '. |
53
|
|
|
'(SELECT 1 FROM `*PREFIX*music_tracks` `track` |
54
|
|
|
WHERE `*PREFIX*music_artists`.`id` = `track`.`artist_id` |
55
|
|
|
AND `track`.`genre_id` = ?)'); |
56
|
|
|
|
57
|
|
|
$params = [$userId, $genreId]; |
58
|
|
|
return $this->findEntities($sql, $params, $limit, $offset); |
|
|
|
|
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* returns summed track play counts of each aritst of the user, omittig artists which have never been played |
63
|
|
|
* |
64
|
|
|
* @return array [int => int], keys are artist IDs and values are play count sums; ordered largest counts first |
65
|
|
|
*/ |
66
|
|
|
public function getArtistTracksPlayCount(string $userId, ?int $limit=null, ?int $offset=null) : array { |
67
|
|
|
$sql = 'SELECT `artist_id`, SUM(`play_count`) AS `sum_count` |
68
|
|
|
FROM `*PREFIX*music_tracks` |
69
|
|
|
WHERE `user_id` = ? AND `play_count` > 0 |
70
|
|
|
GROUP BY `artist_id` |
71
|
|
|
ORDER BY `sum_count` DESC, `artist_id`'; // the second criterion is just to make the order predictable on even counts |
72
|
|
|
|
73
|
|
|
$result = $this->execute($sql, [$userId], $limit, $offset); |
|
|
|
|
74
|
|
|
$playCountByArtist = []; |
75
|
|
|
while ($row = $result->fetch()) { |
76
|
|
|
$playCountByArtist[$row['artist_id']] = (int)$row['sum_count']; |
77
|
|
|
} |
78
|
|
|
return $playCountByArtist; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* returns the latest play time of each artist of the user, omittig artists which have never been played |
83
|
|
|
* |
84
|
|
|
* @return array [int => string], keys are artist IDs and values are date-times; ordered latest times first |
85
|
|
|
*/ |
86
|
|
|
public function getLatestArtistPlayTimes(string $userId, ?int $limit=null, ?int $offset=null) : array { |
87
|
|
|
$sql = 'SELECT `artist_id`, MAX(`last_played`) AS `latest_time` |
88
|
|
|
FROM `*PREFIX*music_tracks` |
89
|
|
|
WHERE `user_id` = ? AND `last_played` IS NOT NULL |
90
|
|
|
GROUP BY `artist_id` |
91
|
|
|
ORDER BY `latest_time` DESC'; |
92
|
|
|
|
93
|
|
|
$result = $this->execute($sql, [$userId], $limit, $offset); |
|
|
|
|
94
|
|
|
$latestTimeByArtist = []; |
95
|
|
|
while ($row = $result->fetch()) { |
96
|
|
|
$latestTimeByArtist[$row['artist_id']] = $row['latest_time']; |
97
|
|
|
} |
98
|
|
|
return $latestTimeByArtist; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* returns the latest play time of each artist of the user, including artists which have never been played |
103
|
|
|
* |
104
|
|
|
* @return array [int => ?string], keys are artist IDs and values are date-times (or null for never played); |
105
|
|
|
* ordered furthest times first |
106
|
|
|
*/ |
107
|
|
|
public function getFurthestArtistPlayTimes(string $userId, ?int $limit=null, ?int $offset=null) : array { |
108
|
|
|
$sql = 'SELECT `artist_id`, MAX(`last_played`) AS `latest_time` |
109
|
|
|
FROM `*PREFIX*music_tracks` |
110
|
|
|
WHERE `user_id` = ? |
111
|
|
|
GROUP BY `artist_id` |
112
|
|
|
ORDER BY `latest_time` ASC'; |
113
|
|
|
|
114
|
|
|
$result = $this->execute($sql, [$userId], $limit, $offset); |
|
|
|
|
115
|
|
|
$latestTimeByArtist = []; |
116
|
|
|
while ($row = $result->fetch()) { |
117
|
|
|
$latestTimeByArtist[$row['artist_id']] = $row['latest_time']; |
118
|
|
|
} |
119
|
|
|
return $latestTimeByArtist; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* @param integer[] $coverFileIds |
124
|
|
|
* @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted |
125
|
|
|
* @return Artist[] artists which got modified (with incomplete data, only id and user are valid), |
126
|
|
|
* empty array if none |
127
|
|
|
*/ |
128
|
|
|
public function removeCovers(array $coverFileIds, ?array $userIds=null) : array { |
129
|
|
|
// find albums using the given file as cover |
130
|
|
|
$sql = 'SELECT `id`, `user_id` FROM `*PREFIX*music_artists` WHERE `cover_file_id` IN ' . |
131
|
|
|
$this->questionMarks(\count($coverFileIds)); |
132
|
|
|
$params = $coverFileIds; |
133
|
|
|
if ($userIds !== null) { |
134
|
|
|
$sql .= ' AND `user_id` IN ' . $this->questionMarks(\count($userIds)); |
135
|
|
|
$params = \array_merge($params, $userIds); |
136
|
|
|
} |
137
|
|
|
$artists = $this->findEntities($sql, $params); |
|
|
|
|
138
|
|
|
|
139
|
|
|
// if any artists found, remove the cover from those |
140
|
|
|
$count = \count($artists); |
141
|
|
|
if ($count) { |
142
|
|
|
$sql = 'UPDATE `*PREFIX*music_artists` |
143
|
|
|
SET `cover_file_id` = NULL |
144
|
|
|
WHERE `id` IN ' . $this->questionMarks($count); |
145
|
|
|
$params = Util::extractIds($artists); |
146
|
|
|
$this->execute($sql, $params); |
|
|
|
|
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
return $artists; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Overridden from the base implementation to provide support for table-specific rules |
154
|
|
|
* |
155
|
|
|
* {@inheritdoc} |
156
|
|
|
* @see BaseMapper::advFormatSqlCondition() |
157
|
|
|
*/ |
158
|
|
|
protected function advFormatSqlCondition(string $rule, string $sqlOp) : string { |
159
|
|
|
// The extra subquery "mysqlhack" seen around some nested queries is needed in order for these to not be insanely slow on MySQL. |
160
|
|
|
// 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. |
161
|
|
|
switch ($rule) { |
162
|
|
|
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(?))"; |
163
|
|
|
case 'artist': return parent::advFormatSqlCondition('name', $sqlOp); // alias |
164
|
|
|
case 'song': return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE LOWER(`t`.`title`) $sqlOp LOWER(?))"; |
165
|
|
|
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)"; |
166
|
|
|
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)"; |
167
|
|
|
case 'played': // fall through, we give no access to other people's data; not part of the API spec but Ample uses this |
168
|
|
|
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" |
169
|
|
|
case 'album_count': return "`*PREFIX*music_artists`.`id` IN (SELECT * FROM (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` JOIN `*PREFIX*music_albums` `a` ON `t`.`album_id` = `a`.`id` GROUP BY `artist_id` HAVING COUNT(DISTINCT `a`.`id`) $sqlOp ?) mysqlhack)"; |
170
|
|
|
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)"; |
171
|
|
|
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)"; |
172
|
|
|
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 |
173
|
|
|
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(?))"; |
174
|
|
|
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') ? '=' : '!=') . ' "")'; |
175
|
|
|
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`, '|%')))"; |
176
|
|
|
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`, '|%')))"; |
177
|
|
|
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(?))"; |
178
|
|
|
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)"; |
179
|
|
|
case 'mbid_artist': return parent::advFormatSqlCondition('mbid', $sqlOp); // alias |
180
|
|
|
case 'mbid_song': return "`*PREFIX*music_artists`.`id` IN (SELECT `artist_id` FROM `*PREFIX*music_tracks` `t` WHERE `t`.`mbid` $sqlOp ?)"; |
181
|
|
|
case 'mbid_album': return "`*PREFIX*music_artists`.`id` IN (SELECT `album_artist_id` from `*PREFIX*music_albums` `al` WHERE `al`.`mbid` $sqlOp ?)"; |
182
|
|
|
case 'has_image': return "`cover_file_id` $sqlOp"; // operator "IS NULL" or "IS NOT NULL" |
183
|
|
|
default: return parent::advFormatSqlCondition($rule, $sqlOp); |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* {@inheritdoc} |
189
|
|
|
* @see \OCA\Music\Db\BaseMapper::findUniqueEntity() |
190
|
|
|
* @param Artist $artist |
191
|
|
|
* @return Artist |
192
|
|
|
*/ |
193
|
|
|
protected function findUniqueEntity(Entity $artist) : Entity { |
194
|
|
|
$sql = $this->selectUserEntities('`hash` = ?'); |
195
|
|
|
return $this->findEntity($sql, [$artist->getUserId(), $artist->getHash()]); |
|
|
|
|
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.