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