Passed
Push — feature/909_Ampache_API_improv... ( f93b1c...7380bc )
by Pauli
02:41
created

AlbumBusinessLayer::findAll()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 4
nc 12
nop 8
dl 0
loc 6
rs 8.8333
c 0
b 0
f 0

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\BusinessLayer;
16
17
use OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
18
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
19
use OCA\Music\AppFramework\Core\Logger;
20
21
use OCA\Music\Db\AlbumMapper;
22
use OCA\Music\Db\Album;
23
use OCA\Music\Db\Entity;
24
use OCA\Music\Db\MatchMode;
25
use OCA\Music\Db\SortBy;
26
use OCA\Music\Db\Track;
27
28
use OCA\Music\Utility\Util;
29
30
/**
31
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
32
 * @phpstan-extends BusinessLayer<Album>
33
 */
34
class AlbumBusinessLayer extends BusinessLayer {
35
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
36
	private $logger;
37
38
	private const MAX_SQL_ALBUM_ARGS = 998; // Some SQLite installations can't handle more than 999 query args. 1 slot reserved for `user_id`.
39
40
	public function __construct(AlbumMapper $albumMapper, Logger $logger) {
41
		parent::__construct($albumMapper);
42
		$this->mapper = $albumMapper;
43
		$this->logger = $logger;
44
	}
45
46
	/**
47
	 * {@inheritdoc}
48
	 * @see BusinessLayer::find()
49
	 * @return Album
50
	 */
51
	public function find(int $albumId, string $userId) : Entity {
52
		$album = parent::find($albumId, $userId);
53
		return $this->injectExtraFields([$album], $userId)[0];
54
	}
55
56
	/**
57
	 * {@inheritdoc}
58
	 * @see BusinessLayer::findById()
59
	 * @return Album[]
60
	 */
61
	public function findById(array $ids, string $userId=null, bool $preserveOrder=false) : array {
62
		$albums = parent::findById($ids, $userId, $preserveOrder);
63
		if ($userId !== null) {
64
			return $this->injectExtraFields($albums, $userId);
65
		} else {
66
			return $albums; // can't inject the extra fields without a user
67
		}
68
	}
69
70
	/**
71
	 * {@inheritdoc}
72
	 * @see BusinessLayer::findAll()
73
	 * @return Album[]
74
	 */
75
	public function findAll(string $userId, int $sortBy=SortBy::None, ?int $limit=null, ?int $offset=null,
76
							?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
77
		$albums = parent::findAll($userId, $sortBy, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
78
		$effectivelyLimited = ($limit !== null && $limit < \count($albums));
79
		$everyAlbumIncluded = (!$effectivelyLimited && !$offset && !$createdMin && !$createdMax && !$updatedMin && !$updatedMax);
0 ignored issues
show
Bug Best Practice introduced by
The expression $offset of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
80
		return $this->injectExtraFields($albums, $userId, $everyAlbumIncluded);
81
	}
82
83
	/**
84
	 * Returns all albums filtered by name of album or artist
85
	 * @return Album[]
86
	 */
87
	public function findAllByNameRecursive(string $name, string $userId, ?int $limit=null, ?int $offset=null) : array {
88
		$name = \trim($name);
89
		$albums = $this->mapper->findAllByNameRecursive($name, $userId, $limit, $offset);
90
		return $this->injectExtraFields($albums, $userId);
91
	}
92
93
	/**
94
	 * Returns all albums filtered by artist (both album and track artists are considered)
95
	 * @return Album[] albums
96
	 */
97
	public function findAllByArtist(int $artistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
98
		$albums = $this->mapper->findAllByArtist($artistId, $userId, $limit, $offset);
99
		return $this->injectExtraFields($albums, $userId);
100
	}
101
102
	/**
103
	 * Returns all albums filtered by album artist
104
	 * @return Album[] albums
105
	 */
106
	public function findAllByAlbumArtist(int $artistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
107
		$albums = $this->mapper->findAllByAlbumArtist($artistId, $userId, $limit, $offset);
108
		$albums = $this->injectExtraFields($albums, $userId);
109
		\usort($albums, ['\OCA\Music\Db\Album', 'compareYearAndName']);
110
		return $albums;
111
	}
112
113
	/**
114
	 * Returns all albums filtered by genre
115
	 * @param int $genreId the genre to include
116
	 * @param string $userId the name of the user
117
	 * @param int|null $limit
118
	 * @param int|null $offset
119
	 * @return Album[] albums
120
	 */
121
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
122
		$albums = $this->mapper->findAllByGenre($genreId, $userId, $limit, $offset);
123
		return $this->injectExtraFields($albums, $userId);
124
	}
125
126
	/**
127
	 * Returns all albums filtered by release year
128
	 * @param int $fromYear
129
	 * @param int $toYear
130
	 * @param string $userId the name of the user
131
	 * @param int|null $limit
132
	 * @param int|null $offset
133
	 * @return Album[] albums
134
	 */
135
	public function findAllByYearRange(
136
			int $fromYear, int $toYear, string $userId, ?int $limit=null, ?int $offset=null) : array {
137
		$reverseOrder = false;
138
		if ($fromYear > $toYear) {
139
			$reverseOrder = true;
140
			Util::swap($fromYear, $toYear);
141
		}
142
143
		// Implement all the custom logic of this function here, without special Mapper function
144
		$albums = \array_filter($this->findAll($userId), function ($album) use ($fromYear, $toYear) {
145
			$years = $album->getYears();
146
			return (!empty($years) && \min($years) <= $toYear && \max($years) >= $fromYear);
147
		});
148
149
		\usort($albums, function ($album1, $album2) use ($reverseOrder) {
150
			return $reverseOrder
151
				? $album2->yearToAPI() - $album1->yearToAPI()
152
				: $album1->yearToAPI() - $album2->yearToAPI();
153
		});
154
155
		if ($limit !== null || $offset !== null) {
156
			$albums = \array_slice($albums, $offset ?: 0, $limit);
157
		}
158
159
		return $albums;
160
	}
161
162
	/**
163
	 * {@inheritdoc}
164
	 * @see BusinessLayer::findAllByName()
165
	 * @return Album[]
166
	 */
167
	public function findAllByName(
168
			?string $name, string $userId, int $matchMode=MatchMode::Exact, ?int $limit=null, ?int $offset=null,
169
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
170
		$albums = parent::findAllByName($name, $userId, $matchMode, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
171
		return $this->injectExtraFields($albums, $userId);
172
	}
173
174
	/**
175
	 * {@inheritdoc}
176
	 * @see BusinessLayer::findAllByName()
177
	 * @return Album[]
178
	 */
179
	public function findAllStarred(string $userId, ?int $limit=null, ?int $offset=null) : array {
180
		$albums = parent::findAllStarred($userId, $limit, $offset);
181
		return $this->injectExtraFields($albums, $userId);
182
	}
183
184
	/**
185
	 * {@inheritdoc}
186
	 * @see BusinessLayer::findAllByName()
187
	 * @return Album[]
188
	 */
189
	public function findAllAdvanced(string $conjunction, array $rules, bool $random, string $userId, ?int $limit=null, ?int $offset=null) : array {
190
		$albums = parent::findAllAdvanced($conjunction, $rules, $random, $userId, $limit, $offset);
191
		return $this->injectExtraFields($albums, $userId);
192
	}
193
194
	/**
195
	 * Find most frequently played albums, judged by the total play count of the contained tracks
196
	 * @return Album[]
197
	 */
198
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
199
		$countsPerAlbum = $this->mapper->getAlbumTracksPlayCount($userId, $limit, $offset);
200
		$ids = \array_keys($countsPerAlbum);
201
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
202
	}
203
204
	/**
205
	 * Find most recently played albums
206
	 * @return Album[]
207
	 */
208
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
209
		$playTimePerAlbum = $this->mapper->getLatestAlbumPlayTimes($userId, $limit, $offset);
210
		$ids = \array_keys($playTimePerAlbum);
211
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
212
	}
213
214
	/**
215
	 * Find least recently played albums
216
	 * @return Album[]
217
	 */
218
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
219
		$playTimePerAlbum = $this->mapper->getFurthestAlbumPlayTimes($userId, $limit, $offset);
220
		$ids = \array_keys($playTimePerAlbum);
221
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
222
	}
223
224
	/**
225
	 * Add performing artists, release years, genres, and disk counts to the given album objects
226
	 * @param Album[] $albums
227
	 * @param string $userId
228
	 * @param bool $allAlbums Set to true if $albums contains all albums of the user.
229
	 *                        This has now effect on the outcome but helps in optimizing
230
	 *                        the database query.
231
	 * @return Album[]
232
	 */
233
	private function injectExtraFields(array $albums, string $userId, bool $allAlbums = false) : array {
234
		if (\count($albums) > 0) {
235
			// In case we are injecting data to a lot of albums, do not limit the
236
			// SQL SELECTs to only those albums. Very large amount of SQL host parameters
237
			// could cause problems with SQLite (see #239) and probably it would be bad for
238
			// performance also on other DBMSs. For the proper operation of this function,
239
			// it doesn't matter if we fetch data for some extra albums.
240
			$albumIds = ($allAlbums || \count($albums) >= self::MAX_SQL_ALBUM_ARGS)
241
					? null : Util::extractIds($albums);
242
243
			$artists = $this->mapper->getPerformingArtistsByAlbumId($albumIds, $userId);
244
			$years = $this->mapper->getYearsByAlbumId($albumIds, $userId);
245
			$diskCounts = $this->mapper->getDiscCountByAlbumId($albumIds, $userId);
246
			$genres = $this->mapper->getGenresByAlbumId($albumIds, $userId);
247
248
			foreach ($albums as &$album) {
249
				$albumId = $album->getId();
250
				$album->setArtistIds($artists[$albumId] ?? []);
251
				$album->setNumberOfDisks($diskCounts[$albumId] ?? 1);
252
				$album->setGenres($genres[$albumId] ?? null);
253
				$album->setYears($years[$albumId] ?? null);
254
			}
255
		}
256
		return $albums;
257
	}
258
259
	/**
260
	 * Returns the count of albums where the given Artist is featured in
261
	 * @param integer $artistId
262
	 * @return integer
263
	 */
264
	public function countByArtist(int $artistId) : int {
265
		return $this->mapper->countByArtist($artistId);
266
	}
267
268
	/**
269
	 * Returns the count of albums where the given artist is the album artist
270
	 * @param integer $artistId
271
	 * @return integer
272
	 */
273
	public function countByAlbumArtist(int $artistId) : int {
274
		return $this->mapper->countByAlbumArtist($artistId);
275
	}
276
277
	public function findAlbumOwner(int $albumId) : string {
278
		$entities = $this->findById([$albumId]);
279
		if (\count($entities) != 1) {
280
			throw new BusinessLayerException(
281
					'Expected to find one album but got ' . \count($entities));
282
		} else {
283
			return $entities[0]->getUserId();
284
		}
285
	}
286
287
	/**
288
	 * Adds an album if it does not exist already or updates an existing album
289
	 * @param string|null $name the name of the album
290
	 * @param integer $albumArtistId
291
	 * @param string $userId
292
	 * @return Album The added/updated album
293
	 */
294
	public function addOrUpdateAlbum(?string $name, int $albumArtistId, string $userId) : Album {
295
		$album = new Album();
296
		$album->setName(Util::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
297
		$album->setUserId($userId);
298
		$album->setAlbumArtistId($albumArtistId);
299
300
		// Generate hash from the set of fields forming the album identity to prevent duplicates.
301
		// The uniqueness of album name is evaluated in case-insensitive manner.
302
		$lowerName = \mb_strtolower($album->getName() ?? '');
303
		$hash = \hash('md5', "$lowerName|$albumArtistId");
304
		$album->setHash($hash);
305
306
		return $this->mapper->updateOrInsert($album);
307
	}
308
309
	/**
310
	 * Check if given file is used as cover for the given album
311
	 * @param int $albumId
312
	 * @param int[] $fileIds
313
	 * @return boolean
314
	 */
315
	public function albumCoverIsOneOfFiles(int $albumId, array $fileIds) : bool {
316
		$albums = $this->findById([$albumId]);
317
		return (\count($albums) && \in_array($albums[0]->getCoverFileId(), $fileIds));
318
	}
319
320
	/**
321
	 * updates the cover for albums in the specified folder without cover
322
	 * @param integer $coverFileId the file id of the cover image
323
	 * @param integer $folderId the file id of the folder where the albums are looked from
324
	 * @return boolean True if one or more albums were influenced
325
	 */
326
	public function updateFolderCover(int $coverFileId, int $folderId) : bool {
327
		return $this->mapper->updateFolderCover($coverFileId, $folderId);
328
	}
329
330
	/**
331
	 * set cover file for a specified album
332
	 * @param int|null $coverFileId the file id of the cover image
333
	 * @param int $albumId the id of the album to be modified
334
	 */
335
	public function setCover(?int $coverFileId, int $albumId) {
336
		$this->mapper->setCover($coverFileId, $albumId);
337
	}
338
339
	/**
340
	 * removes the cover art from albums, replacement covers will be searched in a background task
341
	 * @param integer[] $coverFileIds the file IDs of the cover images
342
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
343
	 * @return Album[] albums which got modified, empty array if none
344
	 */
345
	public function removeCovers(array $coverFileIds, array $userIds=null) : array {
346
		return $this->mapper->removeCovers($coverFileIds, $userIds);
347
	}
348
349
	/**
350
	 * try to find cover arts for albums without covers
351
	 * @param string|null $userId target user; omit to target all users
352
	 * @return string[] users whose collections got modified
353
	 */
354
	public function findCovers(string $userId = null) : array {
355
		$affectedUsers = [];
356
		$albums = $this->mapper->getAlbumsWithoutCover($userId);
357
		foreach ($albums as $album) {
358
			if ($this->mapper->findAlbumCover($album['albumId'], $album['parentFolderId'])) {
359
				$affectedUsers[$album['userId']] = 1;
360
			}
361
		}
362
		return \array_keys($affectedUsers);
363
	}
364
365
	/**
366
	 * Given an array of track IDs, find corresponding unique album IDs, including only
367
	 * those album which have a cover art set.
368
	 * @param int[] $trackIds
369
	 * @return Album[] *Partial* albums, without any injected extra fields
370
	 */
371
	public function findAlbumsWithCoversForTracks(array $trackIds, string $userId, int $limit) : array {
372
		if (\count($trackIds) === 0) {
373
			return [];
374
		} else {
375
			$result = [];
376
			$idChunks = \array_chunk($trackIds, self::MAX_SQL_ALBUM_ARGS);
377
			foreach ($idChunks as $idChunk) {
378
				$resultChunk = $this->mapper->findAlbumsWithCoversForTracks($idChunk, $userId, $limit);
379
				$result = \array_merge($result, $resultChunk);
380
				$limit -= \count($resultChunk);
381
				if ($limit <= 0) {
382
					break;
383
				}
384
			}
385
			return $result;
386
		}
387
	}
388
389
	/**
390
	 * Given an array of Track objects, inject the corresponding Album object to each of them
391
	 * @param Track[] $tracks (in|out)
392
	 */
393
	public function injectAlbumsToTracks(array &$tracks, string $userId) {
394
		$albumIds = [];
395
396
		// get unique album IDs
397
		foreach ($tracks as $track) {
398
			$albumIds[$track->getAlbumId()] = 1;
399
		}
400
		$albumIds = \array_keys($albumIds);
401
402
		// get the corresponding entities from the business layer
403
		if (\count($albumIds) < $this->count($userId)) {
404
			$albums = $this->findById($albumIds, $userId);
405
		} else {
406
			$albums = $this->findAll($userId);
407
		}
408
409
		// create hash tables "id => entity" for the albums for fast access
410
		$albumMap = Util::createIdLookupTable($albums);
411
412
		// finally, set the references on the tracks
413
		foreach ($tracks as &$track) {
414
			$track->setAlbum($albumMap[$track->getAlbumId()] ?? new Album());
415
		}
416
	}
417
}
418