AlbumBusinessLayer::findAllByName()   A
last analyzed

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