Passed
Push — master ( b806b4...5b46ab )
by Pauli
02:42
created

AlbumBusinessLayer::albumCoverIsOneOfFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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