Passed
Push — master ( 6670ca...130e3c )
by Pauli
03:33
created

TrackBusinessLayer::addOrUpdateTrack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 12
dl 0
loc 18
rs 9.7666
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 - 2025
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\MatchMode;
22
use OCA\Music\Db\SortBy;
23
use OCA\Music\Db\TrackMapper;
24
use OCA\Music\Db\Track;
25
use OCA\Music\Service\FileSystemService;
26
use OCA\Music\Utility\ArrayUtil;
27
use OCA\Music\Utility\StringUtil;
28
29
use OCP\AppFramework\Db\DoesNotExistException;
30
31
/**
32
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
33
 * @method Track find(int $trackId, string $userId)
34
 * @method Track[] findAll(string $userId, int $sortBy=SortBy::Name, ?int $limit=null, ?int $offset=null)
35
 * @method Track[] findAllByName(string $name, string $userId, int $matchMode=MatchMode::Exact, ?int $limit=null, ?int $offset=null)
36
 * @property TrackMapper $mapper
37
 * @phpstan-extends BusinessLayer<Track>
38
 */
39
class TrackBusinessLayer extends BusinessLayer {
40
	private FileSystemService $fileSystemService;
41
	private Logger $logger;
42
43
	public function __construct(TrackMapper $trackMapper, FileSystemService $fileSystemService, Logger $logger) {
44
		parent::__construct($trackMapper);
45
		$this->fileSystemService = $fileSystemService;
46
		$this->logger = $logger;
47
	}
48
49
	/**
50
	 * Returns all tracks filtered by artist (both album and track artists are considered)
51
	 * @param int|int[] $artistId
52
	 * @return Track[]
53
	 */
54
	public function findAllByArtist(/*mixed*/ $artistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
55
		if (empty($artistId)) {
56
			return [];
57
		} else {
58
			if (!\is_array($artistId)) {
59
				$artistId = [$artistId];
60
			}
61
			return $this->mapper->findAllByArtist($artistId, $userId, $limit, $offset);
62
		}
63
	}
64
65
	/**
66
	 * Returns all tracks filtered by album. Optionally, filter also by the performing artist.
67
	 * @param int|int[] $albumId
68
	 * @return Track[]
69
	 */
70
	public function findAllByAlbum(/*mixed*/ $albumId, string $userId, ?int $artistId=null, ?int $limit=null, ?int $offset=null) : array {
71
		if (empty($albumId)) {
72
			return [];
73
		} else {
74
			if (!\is_array($albumId)) {
75
				$albumId = [$albumId];
76
			}
77
			return $this->mapper->findAllByAlbum($albumId, $userId, $artistId, $limit, $offset);
78
		}
79
	}
80
81
	/**
82
	 * Returns all tracks filtered by parent folder
83
	 * @return Track[]
84
	 */
85
	public function findAllByFolder(int $folderId, string $userId, ?int $limit=null, ?int $offset=null) : array {
86
		return $this->mapper->findAllByFolder($folderId, $userId, $limit, $offset);
87
	}
88
89
	/**
90
	 * Returns all tracks filtered by genre
91
	 * @return Track[]
92
	 */
93
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
94
		return $this->mapper->findAllByGenre($genreId, $userId, $limit, $offset);
95
	}
96
97
	/**
98
	 * Returns all tracks filtered by name (of track/album/artist)
99
	 * @param string $name the name of the track/album/artist
100
	 * @param string $userId the name of the user
101
	 * @return Track[]
102
	 */
103
	public function findAllByNameRecursive(string $name, string $userId, ?int $limit=null, ?int $offset=null) : array {
104
		$name = \trim($name);
105
		return $this->mapper->findAllByNameRecursive($name, $userId, $limit, $offset);
106
	}
107
108
	/**
109
	 * Returns all tracks specified by name, artist name, and/or album name
110
	 * @return Track[] Tracks matching the criteria
111
	 */
112
	public function findAllByNameArtistOrAlbum(?string $name, ?string $artistName, ?string $albumName, string $userId) : array {
113
		if ($name !== null) {
114
			$name = \trim($name);
115
		}
116
		if ($artistName !== null) {
117
			$artistName = \trim($artistName);
118
		}
119
120
		return $this->mapper->findAllByNameArtistOrAlbum($name, $artistName, $albumName, $userId);
121
	}
122
123
	/**
124
	 * Find most frequently played tracks
125
	 * @return Track[]
126
	 */
127
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
128
		return $this->mapper->findFrequentPlay($userId, $limit, $offset);
129
	}
130
131
	/**
132
	 * Find most recently played tracks
133
	 * @return Track[]
134
	 */
135
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
136
		return $this->mapper->findRecentPlay($userId, $limit, $offset);
137
	}
138
139
	/**
140
	 * Find least recently played tracks
141
	 * @return Track[]
142
	 */
143
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
144
		return $this->mapper->findNotRecentPlay($userId, $limit, $offset);
145
	}
146
147
	/**
148
	 * Returns the track for a file id
149
	 * @return Track|null
150
	 */
151
	public function findByFileId(int $fileId, string $userId) : ?Track {
152
		try {
153
			return $this->mapper->findByFileId($fileId, $userId);
154
		} catch (DoesNotExistException $e) {
155
			return null;
156
		}
157
	}
158
159
	/**
160
	 * Returns file IDs of all indexed tracks of the user.
161
	 * Optionally, limit the search to files residing (directly or indirectly) in the given folder.
162
	 * @return int[]
163
	 */
164
	public function findAllFileIds(string $userId, ?int $folderId=null) : array {
165
		$parentIds = ($folderId !== null) ? $this->fileSystemService->findAllDescendantFolders($folderId) : null;
166
		return $this->mapper->findAllFileIds($userId, $parentIds);
167
	}
168
169
	/**
170
	 * Returns file IDs of all indexed tracks of the user which should be rescanned to ensure that the library details are up-to-date.
171
	 * The track may be considered "dirty" for one of two reasons:
172
	 * - its 'modified' time in the file system (actually in the cloud's file cache) is later than the 'updated' field of the entity in the database
173
	 * - it has been specifically marked as dirty, maybe in response to being moved to another directory
174
	 * Optionally, limit the search to files residing (directly or indirectly) in the given folder.
175
	 * @return int[]
176
	 */
177
	public function findDirtyFileIds(string $userId, ?int $folderId=null) : array {
178
		$parentIds = ($folderId !== null) ? $this->fileSystemService->findAllDescendantFolders($folderId) : null;
179
		return $this->mapper->findDirtyFileIds($userId, $parentIds);
180
	}
181
182
	/**
183
	 * Returns all genre IDs associated with the given artist
184
	 * @return int[]
185
	 */
186
	public function getGenresByArtistId(int $artistId, string $userId) : array {
187
		return $this->mapper->getGenresByArtistId($artistId, $userId);
188
	}
189
190
	/**
191
	 * Returns file IDs of the tracks which do not have genre scanned. This is not the same
192
	 * thing as unknown genre, which is stored as empty string and means that the genre has
193
	 * been scanned but was not found from the track metadata.
194
	 * @return int[]
195
	 */
196
	public function findFilesWithoutScannedGenre(string $userId) : array {
197
		return $this->mapper->findFilesWithoutScannedGenre($userId);
198
	}
199
200
	public function countByArtist(int $artistId) : int {
201
		return $this->mapper->countByArtist($artistId);
202
	}
203
204
	public function countByAlbum(int $albumId) : int {
205
		return $this->mapper->countByAlbum($albumId);
206
	}
207
208
	/**
209
	 * @return integer Duration in seconds
210
	 */
211
	public function totalDurationOfAlbum(int $albumId) : int {
212
		return $this->mapper->totalDurationOfAlbum($albumId);
213
	}
214
215
	/**
216
	 * @return integer Duration in seconds
217
	 */
218
	public function totalDurationByArtist(int $artistId) : int {
219
		return $this->mapper->totalDurationByArtist($artistId);
220
	}
221
222
	/**
223
	 * Update "last played" timestamp and increment the total play count of the track.
224
	 */
225
	public function recordTrackPlayed(int $trackId, string $userId, ?\DateTime $timeOfPlay = null) : void {
226
		$timeOfPlay = $timeOfPlay ?? new \DateTime();
227
228
		if (!$this->mapper->recordTrackPlayed($trackId, $userId, $timeOfPlay)) {
229
			throw new BusinessLayerException("Track with ID $trackId was not found");
230
		}
231
	}
232
233
	/**
234
	 * Adds a track if it does not exist already or updates an existing track
235
	 * @param string $title the title of the track
236
	 * @param int|null $number the number of the track
237
	 * @param int|null $discNumber the number of the disc
238
	 * @param int|null $year the year of the release
239
	 * @param int $genreId the genre id of the track
240
	 * @param int $artistId the artist id of the track
241
	 * @param int $albumId the album id of the track
242
	 * @param int $fileId the file id of the track
243
	 * @param string $mimetype the mimetype of the track
244
	 * @param string $userId the name of the user
245
	 * @param int $length track length in seconds
246
	 * @param int $bitrate track bitrate in bits (not kbits)
247
	 * @return Track The added/updated track
248
	 */
249
	public function addOrUpdateTrack(
250
			string $title, ?int $number, ?int $discNumber, ?int $year, int $genreId, int $artistId, int $albumId,
251
			int $fileId, string $mimetype, string $userId, ?int $length=null, ?int $bitrate=null) : Track {
252
		$track = new Track();
253
		$track->setTitle(StringUtil::truncate($title, 256)); // some DB setups can't truncate automatically to column max size
254
		$track->setNumber($number);
255
		$track->setDisk($discNumber);
256
		$track->setYear($year);
257
		$track->setGenreId($genreId);
258
		$track->setArtistId($artistId);
259
		$track->setAlbumId($albumId);
260
		$track->setFileId($fileId);
261
		$track->setMimetype($mimetype);
262
		$track->setUserId($userId);
263
		$track->setLength($length);
264
		$track->setBitrate($bitrate);
265
		$track->setDirty(0);
266
		return $this->mapper->insertOrUpdate($track);
267
	}
268
269
	/**
270
	 * Deletes tracks
271
	 * @param int[] $fileIds file IDs of the tracks to delete
272
	 * @param string[]|null $userIds the target users; if omitted, the tracks matching the
273
	 *                      $fileIds are deleted from all users
274
	 * @return array|false  False is returned if no such track was found; otherwise array of six arrays
275
	 *         (named 'deletedTracks', 'remainingAlbums', 'remainingArtists', 'obsoleteAlbums',
276
	 *         'obsoleteArtists', and 'affectedUsers'). These contain the track, album, artist, and
277
	 *         user IDs of the deleted tracks. The 'obsolete' entities are such which no longer
278
	 *         have any tracks while 'remaining' entities have some left.
279
	 */
280
	public function deleteTracks(array $fileIds, ?array $userIds=null) {
281
		$tracks = ($userIds !== null)
282
			? $this->mapper->findByFileIds($fileIds, $userIds)
283
			: $this->mapper->findAllByFileIds($fileIds);
284
285
		if (\count($tracks) === 0) {
286
			$result = false;
287
		} else {
288
			// delete all the matching tracks
289
			$trackIds = ArrayUtil::extractIds($tracks);
290
			$this->deleteById($trackIds);
291
292
			// find all distinct albums, artists, and users of the deleted tracks
293
			$artists = [];
294
			$albums = [];
295
			$users = [];
296
			foreach ($tracks as $track) {
297
				$artists[$track->getArtistId()] = 1;
298
				$albums[$track->getAlbumId()] = 1;
299
				$users[$track->getUserId()] = 1;
300
			}
301
			$artists = \array_keys($artists);
302
			$albums = \array_keys($albums);
303
			$users = \array_keys($users);
304
305
			// categorize each artist as 'remaining' or 'obsolete'
306
			$remainingArtists = [];
307
			$obsoleteArtists = [];
308
			foreach ($artists as $artistId) {
309
				if ($this->mapper->countByArtist($artistId) === 0) {
310
					$obsoleteArtists[] = $artistId;
311
				} else {
312
					$remainingArtists[] = $artistId;
313
				}
314
			}
315
316
			// categorize each album as 'remaining' or 'obsolete'
317
			$remainingAlbums = [];
318
			$obsoleteAlbums = [];
319
			foreach ($albums as $albumId) {
320
				if ($this->mapper->countByAlbum($albumId) === 0) {
321
					$obsoleteAlbums[] = $albumId;
322
				} else {
323
					$remainingAlbums[] = $albumId;
324
				}
325
			}
326
327
			$result = [
328
				'deletedTracks'    => $trackIds,
329
				'remainingAlbums'  => $remainingAlbums,
330
				'remainingArtists' => $remainingArtists,
331
				'obsoleteAlbums'   => $obsoleteAlbums,
332
				'obsoleteArtists'  => $obsoleteArtists,
333
				'affectedUsers'    => $users
334
			];
335
		}
336
337
		return $result;
338
	}
339
340
	/**
341
	 * Marks tracks as dirty, ultimately requesting the user to rescan them
342
	 * @param int[] $fileIds file IDs of the tracks to mark as dirty
343
	 * @param string[]|null $userIds the target users; if omitted, the tracks matching the
344
	 *                      $fileIds are marked for all users
345
	 */
346
	public function markTracksDirty(array $fileIds, ?array $userIds=null) : void {
347
		// be prepared for huge number of file IDs
348
		$chunkMaxSize = self::MAX_SQL_ARGS - \count($userIds ?? []);
349
		$idChunks = \array_chunk($fileIds, $chunkMaxSize);
350
		foreach ($idChunks as $idChunk) {
351
			$this->mapper->markTracksDirty($idChunk, $userIds);
352
		}
353
	}
354
}
355