Passed
Push — feature/909_Ampache_API_improv... ( 673c0e...814067 )
by Pauli
02:45
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::findAllByName()
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::findAllByName()
193
	 * @return Album[]
194
	 */
195
	public function findAllAdvanced(string $conjunction, array $rules, string $userId, ?int $limit=null, ?int $offset=null) : array {
196
		$albums = parent::findAllAdvanced($conjunction, $rules, $userId, $limit, $offset);
197
		return $this->injectExtraFields($albums, $userId);
198
	}
199
200
	/**
201
	 * Find most frequently played albums, judged by the total play count of the contained tracks
202
	 * @return Album[]
203
	 */
204
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
205
		$countsPerAlbum = $this->mapper->getAlbumTracksPlayCount($userId, $limit, $offset);
206
		$ids = \array_keys($countsPerAlbum);
207
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
208
	}
209
210
	/**
211
	 * Find most recently played albums
212
	 * @return Album[]
213
	 */
214
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
215
		$playTimePerAlbum = $this->mapper->getLatestAlbumPlayTimes($userId, $limit, $offset);
216
		$ids = \array_keys($playTimePerAlbum);
217
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
218
	}
219
220
	/**
221
	 * Find least recently played albums
222
	 * @return Album[]
223
	 */
224
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
225
		$playTimePerAlbum = $this->mapper->getFurthestAlbumPlayTimes($userId, $limit, $offset);
226
		$ids = \array_keys($playTimePerAlbum);
227
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
228
	}
229
230
	/**
231
	 * Add performing artists, release years, genres, and disk counts to the given album objects
232
	 * @param Album[] $albums
233
	 * @param string $userId
234
	 * @param bool $allAlbums Set to true if $albums contains all albums of the user.
235
	 *                        This has now effect on the outcome but helps in optimizing
236
	 *                        the database query.
237
	 * @return Album[]
238
	 */
239
	private function injectExtraFields(array $albums, string $userId, bool $allAlbums = false) : array {
240
		if (\count($albums) > 0) {
241
			// In case we are injecting data to a lot of albums, do not limit the
242
			// SQL SELECTs to only those albums. Very large amount of SQL host parameters
243
			// could cause problems with SQLite (see #239) and probably it would be bad for
244
			// performance also on other DBMSs. For the proper operation of this function,
245
			// it doesn't matter if we fetch data for some extra albums.
246
			$albumIds = ($allAlbums || \count($albums) >= self::MAX_SQL_ARGS)
247
					? null : Util::extractIds($albums);
248
249
			$artists = $this->mapper->getPerformingArtistsByAlbumId($albumIds, $userId);
250
			$years = $this->mapper->getYearsByAlbumId($albumIds, $userId);
251
			$diskCounts = $this->mapper->getDiscCountByAlbumId($albumIds, $userId);
252
			$genres = $this->mapper->getGenresByAlbumId($albumIds, $userId);
253
254
			foreach ($albums as &$album) {
255
				$albumId = $album->getId();
256
				$album->setArtistIds($artists[$albumId] ?? []);
257
				$album->setNumberOfDisks($diskCounts[$albumId] ?? 1);
258
				$album->setGenres($genres[$albumId] ?? null);
259
				$album->setYears($years[$albumId] ?? null);
260
			}
261
		}
262
		return $albums;
263
	}
264
265
	/**
266
	 * Returns the count of albums where the given Artist is featured in
267
	 * @param integer $artistId
268
	 * @return integer
269
	 */
270
	public function countByArtist(int $artistId) : int {
271
		return $this->mapper->countByArtist($artistId);
272
	}
273
274
	/**
275
	 * Returns the count of albums where the given artist is the album artist
276
	 * @param integer $artistId
277
	 * @return integer
278
	 */
279
	public function countByAlbumArtist(int $artistId) : int {
280
		return $this->mapper->countByAlbumArtist($artistId);
281
	}
282
283
	public function findAlbumOwner(int $albumId) : string {
284
		$entities = $this->findById([$albumId]);
285
		if (\count($entities) != 1) {
286
			throw new BusinessLayerException(
287
					'Expected to find one album but got ' . \count($entities));
288
		} else {
289
			return $entities[0]->getUserId();
290
		}
291
	}
292
293
	/**
294
	 * Adds an album if it does not exist already or updates an existing album
295
	 * @param string|null $name the name of the album
296
	 * @param integer $albumArtistId
297
	 * @param string $userId
298
	 * @return Album The added/updated album
299
	 */
300
	public function addOrUpdateAlbum(?string $name, int $albumArtistId, string $userId) : Album {
301
		$album = new Album();
302
		$album->setName(Util::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
303
		$album->setUserId($userId);
304
		$album->setAlbumArtistId($albumArtistId);
305
306
		// Generate hash from the set of fields forming the album identity to prevent duplicates.
307
		// The uniqueness of album name is evaluated in case-insensitive manner.
308
		$lowerName = \mb_strtolower($album->getName() ?? '');
309
		$hash = \hash('md5', "$lowerName|$albumArtistId");
310
		$album->setHash($hash);
311
312
		return $this->mapper->updateOrInsert($album);
313
	}
314
315
	/**
316
	 * Check if given file is used as cover for the given album
317
	 * @param int $albumId
318
	 * @param int[] $fileIds
319
	 * @return boolean
320
	 */
321
	public function albumCoverIsOneOfFiles(int $albumId, array $fileIds) : bool {
322
		$albums = $this->findById([$albumId]);
323
		return (\count($albums) && \in_array($albums[0]->getCoverFileId(), $fileIds));
324
	}
325
326
	/**
327
	 * updates the cover for albums in the specified folder without cover
328
	 * @param integer $coverFileId the file id of the cover image
329
	 * @param integer $folderId the file id of the folder where the albums are looked from
330
	 * @return boolean True if one or more albums were influenced
331
	 */
332
	public function updateFolderCover(int $coverFileId, int $folderId) : bool {
333
		return $this->mapper->updateFolderCover($coverFileId, $folderId);
334
	}
335
336
	/**
337
	 * set cover file for a specified album
338
	 * @param int|null $coverFileId the file id of the cover image
339
	 * @param int $albumId the id of the album to be modified
340
	 */
341
	public function setCover(?int $coverFileId, int $albumId) {
342
		$this->mapper->setCover($coverFileId, $albumId);
343
	}
344
345
	/**
346
	 * removes the cover art from albums, replacement covers will be searched in a background task
347
	 * @param integer[] $coverFileIds the file IDs of the cover images
348
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
349
	 * @return Album[] albums which got modified, empty array if none
350
	 */
351
	public function removeCovers(array $coverFileIds, array $userIds=null) : array {
352
		return $this->mapper->removeCovers($coverFileIds, $userIds);
353
	}
354
355
	/**
356
	 * try to find cover arts for albums without covers
357
	 * @param string|null $userId target user; omit to target all users
358
	 * @return string[] users whose collections got modified
359
	 */
360
	public function findCovers(string $userId = null) : array {
361
		$affectedUsers = [];
362
		$albums = $this->mapper->getAlbumsWithoutCover($userId);
363
		foreach ($albums as $album) {
364
			if ($this->mapper->findAlbumCover($album['albumId'], $album['parentFolderId'])) {
365
				$affectedUsers[$album['userId']] = 1;
366
			}
367
		}
368
		return \array_keys($affectedUsers);
369
	}
370
371
	/**
372
	 * Given an array of track IDs, find corresponding unique album IDs, including only
373
	 * those album which have a cover art set.
374
	 * @param int[] $trackIds
375
	 * @return Album[] *Partial* albums, without any injected extra fields
376
	 */
377
	public function findAlbumsWithCoversForTracks(array $trackIds, string $userId, int $limit) : array {
378
		if (\count($trackIds) === 0) {
379
			return [];
380
		} else {
381
			$result = [];
382
			$idChunks = \array_chunk($trackIds, self::MAX_SQL_ARGS - 1);
383
			foreach ($idChunks as $idChunk) {
384
				$resultChunk = $this->mapper->findAlbumsWithCoversForTracks($idChunk, $userId, $limit);
385
				$result = \array_merge($result, $resultChunk);
386
				$limit -= \count($resultChunk);
387
				if ($limit <= 0) {
388
					break;
389
				}
390
			}
391
			return $result;
392
		}
393
	}
394
395
	/**
396
	 * Given an array of Track objects, inject the corresponding Album object to each of them
397
	 * @param Track[] $tracks (in|out)
398
	 */
399
	public function injectAlbumsToTracks(array &$tracks, string $userId) {
400
		$albumIds = [];
401
402
		// get unique album IDs
403
		foreach ($tracks as $track) {
404
			$albumIds[$track->getAlbumId()] = 1;
405
		}
406
		$albumIds = \array_keys($albumIds);
407
408
		// get the corresponding entities from the business layer
409
		if (\count($albumIds) < self::MAX_SQL_ARGS && \count($albumIds) < $this->count($userId)) {
410
			$albums = $this->findById($albumIds, $userId);
411
		} else {
412
			$albums = $this->findAll($userId);
413
		}
414
415
		// create hash tables "id => entity" for the albums for fast access
416
		$albumMap = Util::createIdLookupTable($albums);
417
418
		// finally, set the references on the tracks
419
		foreach ($tracks as &$track) {
420
			$track->setAlbum($albumMap[$track->getAlbumId()] ?? new Album());
421
		}
422
	}
423
}
424