Passed
Push — master ( 7b42d0...d80bc5 )
by Pauli
04:15 queued 21s
created

PlaylistBusinessLayer::generate()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
c 0
b 0
f 0
nc 8
nop 9
dl 0
loc 33
rs 9.3888

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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2016 - 2025
11
 */
12
13
namespace OCA\Music\BusinessLayer;
14
15
use OCA\Music\AppFramework\BusinessLayer\BusinessLayer;
16
use OCA\Music\AppFramework\Core\Logger;
17
18
use OCA\Music\Db\MatchMode;
19
use OCA\Music\Db\Playlist;
20
use OCA\Music\Db\PlaylistMapper;
21
use OCA\Music\Db\SortBy;
22
use OCA\Music\Db\Track;
23
use OCA\Music\Db\TrackMapper;
24
use OCA\Music\Utility\ArrayUtil;
25
use OCA\Music\Utility\Random;
26
use OCA\Music\Utility\StringUtil;
27
28
/**
29
 * Base class functions with actually used inherited types to help IDE and Scrutinizer:
30
 * @method Playlist find(int $playlistId, string $userId)
31
 * @method Playlist[] findAll(string $userId, int $sortBy=SortBy::Name, ?int $limit=null, ?int $offset=null)
32
 * @method Playlist[] findAllByName(string $name, string $userId, int $matchMode=MatchMode::Exact, ?int $limit=null, ?int $offset=null)
33
 * @property PlaylistMapper $mapper
34
 * @phpstan-extends BusinessLayer<Playlist>
35
 */
36
class PlaylistBusinessLayer extends BusinessLayer {
37
	private TrackMapper $trackMapper;
38
	private Logger $logger;
39
40
	public function __construct(
41
			PlaylistMapper $playlistMapper,
42
			TrackMapper $trackMapper,
43
			Logger $logger) {
44
		parent::__construct($playlistMapper);
45
		$this->trackMapper = $trackMapper;
46
		$this->logger = $logger;
47
	}
48
49
	public function setTracks(array $trackIds, int $playlistId, string $userId) : Playlist {
50
		$playlist = $this->find($playlistId, $userId);
51
		$playlist->setTrackIdsFromArray($trackIds);
52
		$this->mapper->update($playlist);
53
		return $playlist;
54
	}
55
56
	public function addTracks(array $trackIds, int $playlistId, string $userId, ?int $insertIndex = null) : Playlist {
57
		$playlist = $this->find($playlistId, $userId);
58
		$allTrackIds = $playlist->getTrackIdsAsArray();
59
		if ($insertIndex === null) {
60
			$allTrackIds = \array_merge($allTrackIds, $trackIds);
61
		} else {
62
			\array_splice($allTrackIds, $insertIndex, 0, $trackIds);
63
		}
64
		$playlist->setTrackIdsFromArray($allTrackIds);
65
		$this->mapper->update($playlist);
66
		return $playlist;
67
	}
68
69
	public function removeTracks(array $trackIndices, int $playlistId, string $userId) : Playlist {
70
		$playlist = $this->find($playlistId, $userId);
71
		$trackIds = $playlist->getTrackIdsAsArray();
72
		$trackIds = \array_diff_key($trackIds, \array_flip($trackIndices));
73
		$playlist->setTrackIdsFromArray($trackIds);
74
		$this->mapper->update($playlist);
75
		return $playlist;
76
	}
77
78
	public function removeAllTracks(int $playlistId, string $userId) : Playlist {
79
		$playlist = $this->find($playlistId, $userId);
80
		$playlist->setTrackIdsFromArray([]);
81
		$this->mapper->update($playlist);
82
		return $playlist;
83
	}
84
85
	public function moveTrack(int $fromIndex, int $toIndex, int $playlistId, string $userId) : Playlist {
86
		$playlist = $this->find($playlistId, $userId);
87
		$trackIds = $playlist->getTrackIdsAsArray();
88
		$movedTrack = \array_splice($trackIds, $fromIndex, 1);
89
		\array_splice($trackIds, $toIndex, 0, $movedTrack);
90
		$playlist->setTrackIdsFromArray($trackIds);
91
		$this->mapper->update($playlist);
92
		return $playlist;
93
	}
94
95
	public function create(string $name, string $userId) : Playlist {
96
		$playlist = new Playlist();
97
		$playlist->setName(StringUtil::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
98
		$playlist->setUserId($userId);
99
100
		return $this->mapper->insert($playlist);
101
	}
102
103
	public function rename(string $name, int $playlistId, string $userId) : Playlist {
104
		$playlist = $this->find($playlistId, $userId);
105
		$playlist->setName(StringUtil::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
106
		$this->mapper->update($playlist);
107
		return $playlist;
108
	}
109
110
	public function setComment(string $comment, int $playlistId, string $userId) : Playlist {
111
		$playlist = $this->find($playlistId, $userId);
112
		$playlist->setComment(StringUtil::truncate($comment, 256)); // some DB setups can't truncate automatically to column max size
113
		$this->mapper->update($playlist);
114
		return $playlist;
115
	}
116
117
	/**
118
	 * removes tracks from all available playlists
119
	 * @param int[] $trackIds array of all track IDs to remove
120
	 */
121
	public function removeTracksFromAllLists(array $trackIds) : void {
122
		foreach ($trackIds as $trackId) {
123
			$affectedLists = $this->mapper->findListsContainingTrack($trackId);
124
125
			foreach ($affectedLists as $playlist) {
126
				$prevTrackIds = $playlist->getTrackIdsAsArray();
127
				$playlist->setTrackIdsFromArray(\array_diff($prevTrackIds, [$trackId]));
128
				$this->mapper->update($playlist);
129
			}
130
		}
131
	}
132
133
	/**
134
	 * @return int[]
135
	 */
136
	public function getPlaylistTrackIds(int $playlistId, string $userId) : array {
137
		$playlist = $this->find($playlistId, $userId);
138
		return $playlist->getTrackIdsAsArray();
139
	}
140
141
	/**
142
	 * get list of Track objects belonging to a given playlist
143
	 * @return Track[]
144
	 */
145
	public function getPlaylistTracks(int $playlistId, string $userId, ?int $limit=null, ?int $offset=null) : array {
146
		$trackIds = $this->getPlaylistTrackIds($playlistId, $userId);
147
148
		$trackIds = \array_slice($trackIds, \intval($offset), $limit);
149
150
		$tracks = empty($trackIds) ? [] : $this->trackMapper->findById($trackIds, $userId);
151
152
		// The $tracks contains the songs in unspecified order and with no duplicates.
153
		// Build a new array where the tracks are in the same order as in $trackIds.
154
		$tracksById = ArrayUtil::createIdLookupTable($tracks);
155
156
		$playlistTracks = [];
157
		foreach ($trackIds as $index => $trackId) {
158
			$track = $tracksById[$trackId] ?? null;
159
			if ($track !== null) {
160
				// in case the same track comes up again in the list, clone the track object
161
				// to have different numbers on the instances
162
				if ($track->getNumberOnPlaylist() !== null) {
163
					$track = clone $track;
164
				}
165
				$track->setNumberOnPlaylist(\intval($offset) + $index + 1);
166
			} else {
167
				$this->logger->log("Invalid track ID $trackId found on playlist $playlistId", 'debug');
168
				$track = Track::emptyInstance();
169
				$track->setId($trackId);
170
			}
171
			$playlistTracks[] = $track;
172
		}
173
174
		return $playlistTracks;
175
	}
176
177
	/**
178
	 * get the total duration of all the tracks on a playlist
179
	 *
180
	 * @return int duration in seconds
181
	 */
182
	public function getDuration(int $playlistId, string $userId) : int {
183
		$trackIds = $this->getPlaylistTrackIds($playlistId, $userId);
184
		$durations = $this->trackMapper->getDurations($trackIds);
185
186
		// We can't simply sum up the values of $durations array, because the playlist may
187
		// contain duplicate entries, and those are not reflected in $durations.
188
		// Be also prepared to invalid playlist entries where corresponding track length does not exist.
189
		$sum = 0;
190
		foreach ($trackIds as $trackId) {
191
			$sum += $durations[$trackId] ?? 0;
192
		}
193
194
		return $sum;
195
	}
196
197
	/**
198
	 * Generate and return a playlist matching the given criteria. The playlist is not persisted.
199
	 *
200
	 * @param string|null $history One of: 'recently-played', 'not-recently-played', 'often-played', 'rarely-played', 'recently-added', 'not-recently-added'
201
	 * @param bool $historyStrict In the "strict" mode, there's no element of randomness when applying the history filter and e.g.
202
	 *             'recently-played' meas "The most recently played" instead of "Among the most recently played"
203
	 * @param int[] $genres Array of genre IDs
204
	 * @param int[] $artists Array of artist IDs
205
	 * @param int|null $fromYear Earliest release year to include
206
	 * @param int|null $toYear Latest release year to include
207
	 * @param string|null $favorite One of: 'track', 'album', 'artists', 'track_album_artist', null
208
	 * @param int $size Size of the playlist to generate, provided that there are enough matching tracks
209
	 * @param string $userId the name of the user
210
	 */
211
	public function generate(
212
			?string $history, bool $historyStrict, array $genres, array $artists,
213
			?int $fromYear, ?int $toYear, ?string $favorite, int $size, string $userId) : Playlist {
214
215
		$now = new \DateTime();
216
		$nowStr = $now->format(PlaylistMapper::SQL_DATE_FORMAT);
217
218
		$playlist = new Playlist();
219
		$playlist->setCreated($nowStr);
220
		$playlist->setUpdated($nowStr);
221
		$playlist->setName('Generated ' . $nowStr);
222
		$playlist->setUserId($userId);
223
224
		list('sortBy' => $sortBy, 'invert' => $invertSort) = self::sortRulesForHistory($history);
225
		$limit = ($sortBy === SortBy::None) ? null : ($historyStrict ? $size : $size * 4);
226
227
		$favoriteMask = self::favoriteMask($favorite);
228
229
		$tracks = $this->trackMapper->findAllByCriteria($genres, $artists, $fromYear, $toYear, $favoriteMask, $sortBy, $invertSort, $userId, $limit);
230
231
		if ($sortBy !== SortBy::None && !$historyStrict) {
232
			// When generating by non-strict history, use a pool of tracks at maximum twice the size of final list.
233
			// However, don't use more than half of the matching tracks unless that is required to satisfy the required list size.
234
			$poolSize = (int)max($size, \count($tracks) / 2);
235
			$tracks = \array_slice($tracks, 0, $poolSize);
236
		}
237
238
		// Pick the final random set of tracks
239
		$tracks = Random::pickItems($tracks, $size);
240
241
		$playlist->setTrackIdsFromArray(ArrayUtil::extractIds($tracks));
242
243
		return $playlist;
244
	}
245
246
	private static function sortRulesForHistory(?string $history) : array {
247
		switch ($history) {
248
			case 'recently-played':
249
				return ['sortBy' => SortBy::LastPlayed, 'invert' => false];
250
			case 'not-recently-played':
251
				return ['sortBy' => SortBy::LastPlayed, 'invert' => true];
252
			case 'often-played':
253
				return ['sortBy' => SortBy::PlayCount, 'invert' => false];
254
			case 'rarely-played':
255
				return ['sortBy' => SortBy::PlayCount, 'invert' => true];
256
			case 'recently-added':
257
				return ['sortBy' => SortBy::Newest, 'invert' => false];
258
			case 'not-recently-added':
259
				return ['sortBy' => SortBy::Newest, 'invert' => true];
260
			default:
261
				return ['sortBy' => SortBy::None, 'invert' => false];
262
		}
263
	}
264
265
	private static function favoriteMask(?string $mode) : ?int {
266
		switch ($mode) {
267
			case 'track':				return TrackMapper::FAVORITE_TRACK;
268
			case 'album':				return TrackMapper::FAVORITE_ALBUM;
269
			case 'artist':				return TrackMapper::FAVORITE_ARTIST;
270
			case 'track_album_artist':	return TrackMapper::FAVORITE_TRACK | TrackMapper::FAVORITE_ALBUM | TrackMapper::FAVORITE_ARTIST;
271
			default:					return null;
272
		}
273
	}
274
}
275