Passed
Push — master ( e287c9...75fd8e )
by Pauli
03:09 queued 11s
created

TrackBusinessLayer::getFolderEntry()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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