Passed
Push — master ( 3c5a6f...e94730 )
by Pauli
09:42 queued 11s
created

TrackBusinessLayer::addOrUpdateTrack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 12
dl 0
loc 17
rs 9.7998
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
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\TrackMapper;
22
use \OCA\Music\Db\Track;
23
24
use \OCA\Music\Utility\Util;
25
26
use \OCP\AppFramework\Db\DoesNotExistException;
27
use \OCP\Files\Folder;
28
29
/**
30
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
31
 * @method Track find(int $trackId, string $userId)
32
 * @method Track[] findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null)
33
 * @method Track[] findAllByName(string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null)
34
 * @phpstan-extends BusinessLayer<Track>
35
 */
36
class TrackBusinessLayer extends BusinessLayer {
37
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
38
	private $logger;
39
40
	public function __construct(TrackMapper $trackMapper, Logger $logger) {
41
		parent::__construct($trackMapper);
42
		$this->mapper = $trackMapper;
43
		$this->logger = $logger;
44
	}
45
46
	/**
47
	 * Returns all tracks filtered by artist (both album and track artists are considered)
48
	 * @param int $artistId the id of the artist
49
	 * @param string $userId the name of the user
50
	 * @return array of tracks
51
	 */
52
	public function findAllByArtist($artistId, $userId) {
53
		return $this->mapper->findAllByArtist($artistId, $userId);
54
	}
55
56
	/**
57
	 * Returns all tracks filtered by album. Optionally, filter also by the performing artist.
58
	 * @param int $albumId the id of the album
59
	 * @param string $userId the name of the user
60
	 * @return \OCA\Music\Db\Track[] tracks
61
	 */
62
	public function findAllByAlbum($albumId, $userId, $artistId = null) {
63
		return $this->mapper->findAllByAlbum($albumId, $userId, $artistId);
64
	}
65
66
	/**
67
	 * Returns all tracks filtered by parent folder
68
	 * @param integer $folderId the id of the folder
69
	 * @param string $userId the name of the user
70
	 * @return \OCA\Music\Db\Track[] tracks
71
	 */
72
	public function findAllByFolder($folderId, $userId) {
73
		return $this->mapper->findAllByFolder($folderId, $userId);
74
	}
75
76
	/**
77
	 * Returns all tracks filtered by genre
78
	 * @param int $genreId the genre to include
79
	 * @param string $userId the name of the user
80
	 * @param int|null $limit
81
	 * @param int|null $offset
82
	 * @return \OCA\Music\Db\Track[] tracks
83
	 */
84
	public function findAllByGenre($genreId, $userId, $limit=null, $offset=null) {
85
		return $this->mapper->findAllByGenre($genreId, $userId, $limit, $offset);
86
	}
87
88
	/**
89
	 * Returns all tracks filtered by name (of track/album/artist)
90
	 * @param string $name the name of the track/album/artist
91
	 * @param string $userId the name of the user
92
	 * @return \OCA\Music\Db\Track[] tracks
93
	 */
94
	public function findAllByNameRecursive($name, $userId) {
95
		$name = \trim($name);
96
		return $this->mapper->findAllByNameRecursive($name, $userId);
97
	}
98
99
	/**
100
	 * Returns all tracks specified by name and/or artist name
101
	 * @param string|null $name the name of the track
102
	 * @param string|null $artistName the name of the artist
103
	 * @param string $userId the name of the user
104
	 * @return \OCA\Music\Db\Track[] Tracks matching the criteria
105
	 */
106
	public function findAllByNameAndArtistName(?string $name, ?string $artistName, string $userId) : array {
107
		if ($name !== null) {
108
			$name = \trim($name);
109
		}
110
		if ($artistName !== null) {
111
			$artistName = \trim($artistName);
112
		}
113
114
		return $this->mapper->findAllByNameAndArtistName($name, $artistName, /*fuzzy=*/false, $userId);
115
	}
116
117
	/**
118
	 * Find most frequently played tracks
119
	 * @return Track[]
120
	 */
121
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
122
		return $this->mapper->findFrequentPlay($userId, $limit, $offset);
123
	}
124
125
	/**
126
	 * Find most recently played tracks
127
	 * @return Track[]
128
	 */
129
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
130
		return $this->mapper->findRecentPlay($userId, $limit, $offset);
131
	}
132
133
	/**
134
	 * Find least recently played tracks
135
	 * @return Track[]
136
	 */
137
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
138
		return $this->mapper->findNotRecentPlay($userId, $limit, $offset);
139
	}
140
141
	/**
142
	 * Returns the track for a file id
143
	 * @param int $fileId the file id of the track
144
	 * @param string $userId the name of the user
145
	 * @return \OCA\Music\Db\Track|null track
146
	 */
147
	public function findByFileId($fileId, $userId) {
148
		try {
149
			return $this->mapper->findByFileId($fileId, $userId);
150
		} catch (DoesNotExistException $e) {
151
			return null;
152
		}
153
	}
154
155
	/**
156
	 * Returns file IDs of all indexed tracks of the user
157
	 * @param string $userId
158
	 * @return int[]
159
	 */
160
	public function findAllFileIds($userId) {
161
		return $this->mapper->findAllFileIds($userId);
162
	}
163
164
	/**
165
	 * Returns all folders of the user containing indexed tracks, along with the contained track IDs
166
	 * @return array of entries like {id: int, name: string, path: string, parent: ?int, trackIds: int[]}
167
	 */
168
	public function findAllFolders(string $userId, Folder $musicFolder) : array {
169
		// All tracks of the user, grouped by their parent folders. Some of the parent folders
170
		// may be owned by other users and are invisible to this user (in case of shared files).
171
		$tracksByFolder = $this->mapper->findTrackAndFolderIds($userId);
172
173
		// Get the folder names and paths for ordinary local folders directly from the DB.
174
		// This is significantly more efficient than using the Files API because we need to
175
		// run only single DB query instead of one per folder.
176
		$folderNamesAndParents = $this->mapper->findNodeNamesAndParents(
177
				\array_keys($tracksByFolder), $musicFolder->getStorage()->getId());
178
179
		// root folder has to be handled as a special case because shared files from
180
		// many folders may be shown to this user mapped under the root folder
181
		$rootFolderTracks = [];
182
183
		// Build the final results. Use the previously fetched data for the ordinary
184
		// local folders and query the data through the Files API for the more special cases.
185
		$result = [];
186
		foreach ($tracksByFolder as $folderId => $trackIds) {
187
			$entry = self::getFolderEntry($folderNamesAndParents, $folderId, $trackIds, $musicFolder);
188
189
			if ($entry) {
190
				$result[] = $entry;
191
			} else {
192
				$rootFolderTracks = \array_merge($rootFolderTracks, $trackIds);
193
			}
194
		}
195
196
		// add the library root folder
197
		$result[] = [
198
			'name' => '',
199
			'parent' => null,
200
			'trackIds' => $rootFolderTracks,
201
			'id' => $musicFolder->getId()
202
		];
203
204
		// add the intermediate folders which do not directly contain any tracks
205
		$result = \array_merge($result, $this->recursivelyGetMissingParentFolders($result, $result, $musicFolder));
206
207
		return $result;
208
	}
209
210
	private function recursivelyGetMissingParentFolders(array $childEntries, array $existingEntries, Folder $musicFolder) : array {
211
		$result = [];
212
213
		$parentIds = \array_unique(\array_column($childEntries, 'parent'));
214
		$parentIds = Util::arrayDiff($parentIds, \array_column($existingEntries, 'id'));
215
		$parentInfo = $this->mapper->findNodeNamesAndParents($parentIds, $musicFolder->getStorage()->getId());
216
		foreach ($parentIds as $parentId) {
217
			if ($parentId !== null) {
218
				$result[] =  self::getFolderEntry($parentInfo, $parentId, [], $musicFolder);
219
			}
220
		}
221
222
		if (\count($parentIds)) {
223
			$result = \array_merge($result, $this->recursivelyGetMissingParentFolders($result, $existingEntries, $musicFolder));
224
		}
225
226
		return $result;
227
	}
228
229
	private static function getFolderEntry(array $folderNamesAndParents, int $folderId, array $trackIds, Folder $musicFolder) : ?array {
230
		if (isset($folderNamesAndParents[$folderId])) {
231
			// normal folder within the user home storage
232
			$entry = $folderNamesAndParents[$folderId];
233
			// special handling for the root folder
234
			if ($folderId === $musicFolder->getId()) {
235
				$entry = null;
236
			}
237
		} else {
238
			// shared folder or parent folder of a shared file or an externally mounted folder
239
			$folderNode = $musicFolder->getById($folderId)[0] ?? null;
240
			if ($folderNode === null) {
241
				// other user's folder with files shared with this user (mapped under root)
242
				$entry = null;
243
			} else {
244
				$entry = [
245
					'name' => $folderNode->getName(),
246
					'parent' => $folderNode->getParent()->getId()
247
				];
248
			}
249
		}
250
251
		if ($entry) {
252
			$entry['trackIds'] = $trackIds;
253
			$entry['id'] = $folderId;
254
255
			if ($entry['id'] == $musicFolder->getId()) {
256
				// the library root should be reported without a parent folder as that parent does not belong to the library
257
				$entry['parent'] = null;
258
			}
259
		}
260
261
		return $entry;
262
	}
263
264
	/**
265
	 * Returns all genre IDs associated with the given artist
266
	 * @param int $artistId
267
	 * @param string $userId
268
	 * @return int[]
269
	 */
270
	public function getGenresByArtistId($artistId, $userId) {
271
		return $this->mapper->getGenresByArtistId($artistId, $userId);
272
	}
273
274
	/**
275
	 * Returns file IDs of the tracks which do not have genre scanned. This is not the same
276
	 * thing as unknown genre, which is stored as empty string and means that the genre has
277
	 * been scanned but was not found from the track metadata.
278
	 * @param string $userId
279
	 * @return int[]
280
	 */
281
	public function findFilesWithoutScannedGenre($userId) {
282
		return $this->mapper->findFilesWithoutScannedGenre($userId);
283
	}
284
285
	/**
286
	 * @param integer $artistId
287
	 * @return integer
288
	 */
289
	public function countByArtist($artistId) {
290
		return $this->mapper->countByArtist($artistId);
291
	}
292
293
	/**
294
	 * @param integer $albumId
295
	 * @return integer
296
	 */
297
	public function countByAlbum($albumId) {
298
		return $this->mapper->countByAlbum($albumId);
299
	}
300
301
	/**
302
	 * @param integer $albumId
303
	 * @return integer Duration in seconds
304
	 */
305
	public function totalDurationOfAlbum($albumId) {
306
		return $this->mapper->totalDurationOfAlbum($albumId);
307
	}
308
309
	/**
310
	 * Update "last played" timestamp and increment the total play count of the track.
311
	 */
312
	public function recordTrackPlayed(int $trackId, string $userId, ?\DateTime $timeOfPlay = null) : void {
313
		$timeOfPlay = $timeOfPlay ?? new \DateTime();
314
315
		if (!$this->mapper->recordTrackPlayed($trackId, $userId, $timeOfPlay)) {
316
			throw new BusinessLayerException("Track with ID $trackId was not found");
317
		}
318
	}
319
320
	/**
321
	 * Adds a track if it does not exist already or updates an existing track
322
	 * @param string $title the title of the track
323
	 * @param int|null $number the number of the track
324
	 * @param int|null $discNumber the number of the disc
325
	 * @param int|null $year the year of the release
326
	 * @param int $genreId the genre id of the track
327
	 * @param int $artistId the artist id of the track
328
	 * @param int $albumId the album id of the track
329
	 * @param int $fileId the file id of the track
330
	 * @param string $mimetype the mimetype of the track
331
	 * @param string $userId the name of the user
332
	 * @param int $length track length in seconds
333
	 * @param int $bitrate track bitrate in bits (not kbits)
334
	 * @return \OCA\Music\Db\Track The added/updated track
335
	 */
336
	public function addOrUpdateTrack(
337
			$title, $number, $discNumber, $year, $genreId, $artistId, $albumId,
338
			$fileId, $mimetype, $userId, $length=null, $bitrate=null) {
339
		$track = new Track();
340
		$track->setTitle(Util::truncate($title, 256)); // some DB setups can't truncate automatically to column max size
341
		$track->setNumber($number);
342
		$track->setDisk($discNumber);
343
		$track->setYear($year);
344
		$track->setGenreId($genreId);
345
		$track->setArtistId($artistId);
346
		$track->setAlbumId($albumId);
347
		$track->setFileId($fileId);
348
		$track->setMimetype($mimetype);
349
		$track->setUserId($userId);
350
		$track->setLength($length);
351
		$track->setBitrate($bitrate);
352
		return $this->mapper->insertOrUpdate($track);
353
	}
354
355
	/**
356
	 * Deletes a track
357
	 * @param int[] $fileIds file IDs of the tracks to delete
358
	 * @param string[]|null $userIds the target users; if omitted, the tracks matching the
359
	 *                      $fileIds are deleted from all users
360
	 * @return array|false  False is returned if no such track was found; otherwise array of six arrays
361
	 *         (named 'deletedTracks', 'remainingAlbums', 'remainingArtists', 'obsoleteAlbums',
362
	 *         'obsoleteArtists', and 'affectedUsers'). These contain the track, album, artist, and
363
	 *         user IDs of the deleted tracks. The 'obsolete' entities are such which no longer
364
	 *         have any tracks while 'remaining' entities have some left.
365
	 */
366
	public function deleteTracks($fileIds, $userIds = null) {
367
		$tracks = ($userIds !== null)
368
			? $this->mapper->findByFileIds($fileIds, $userIds)
369
			: $this->mapper->findAllByFileIds($fileIds);
370
371
		if (\count($tracks) === 0) {
372
			$result = false;
373
		} else {
374
			// delete all the matching tracks
375
			$trackIds = Util::extractIds($tracks);
376
			$this->deleteById($trackIds);
377
378
			// find all distinct albums, artists, and users of the deleted tracks
379
			$artists = [];
380
			$albums = [];
381
			$users = [];
382
			foreach ($tracks as $track) {
383
				$artists[$track->getArtistId()] = 1;
384
				$albums[$track->getAlbumId()] = 1;
385
				$users[$track->getUserId()] = 1;
386
			}
387
			$artists = \array_keys($artists);
388
			$albums = \array_keys($albums);
389
			$users = \array_keys($users);
390
391
			// categorize each artist as 'remaining' or 'obsolete'
392
			$remainingArtists = [];
393
			$obsoleteArtists = [];
394
			foreach ($artists as $artistId) {
395
				$result = $this->mapper->countByArtist($artistId);
396
				if ($result === '0') {
397
					$obsoleteArtists[] = $artistId;
398
				} else {
399
					$remainingArtists[] = $artistId;
400
				}
401
			}
402
403
			// categorize each album as 'remaining' or 'obsolete'
404
			$remainingAlbums = [];
405
			$obsoleteAlbums = [];
406
			foreach ($albums as $albumId) {
407
				$result = $this->mapper->countByAlbum($albumId);
408
				if ($result === '0') {
409
					$obsoleteAlbums[] = $albumId;
410
				} else {
411
					$remainingAlbums[] = $albumId;
412
				}
413
			}
414
415
			$result = [
416
				'deletedTracks'    => $trackIds,
417
				'remainingAlbums'  => $remainingAlbums,
418
				'remainingArtists' => $remainingArtists,
419
				'obsoleteAlbums'   => $obsoleteAlbums,
420
				'obsoleteArtists'  => $obsoleteArtists,
421
				'affectedUsers'    => $users
422
			];
423
		}
424
425
		return $result;
426
	}
427
}
428