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