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