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

addExternalMountsToFoldersLut()   B

Complexity

Conditions 10
Paths 9

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 20
c 0
b 0
f 0
nc 9
nop 3
dl 0
loc 33
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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