Passed
Push — feature/909_Ampache_API_improv... ( e0770e...e4874b )
by Pauli
02:36
created

AlbumBusinessLayer::findAllByName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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