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