Passed
Push — master ( 3c5a6f...e94730 )
by Pauli
09:42 queued 11s
created

TrackBusinessLayer::findNotRecentPlay()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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