Passed
Push — master ( 91b14a...fc89b7 )
by Pauli
02:49
created

ArtistBusinessLayer::findAllHavingTracks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 10
dl 0
loc 5
rs 10

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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2017 - 2024
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\Artist;
21
use OCA\Music\Db\ArtistMapper;
22
use OCA\Music\Db\MatchMode;
23
use OCA\Music\Db\SortBy;
24
25
use OCA\Music\Utility\Util;
26
27
use OCP\IL10N;
28
use OCP\Files\File;
29
30
/**
31
 * Base class functions with the actually used inherited types to help IDE and Scrutinizer:
32
 * @method Artist find(int $trackId, string $userId)
33
 * @method Artist[] findAll(string $userId, int $sortBy=SortBy::Name, int $limit=null, int $offset=null)
34
 * @method Artist[] findAllByName(?string $name, string $userId, int $matchMode=MatchMode::Exact, int $limit=null, int $offset=null)
35
 * @method Artist[] findById(int[] $ids, string $userId=null, bool $preserveOrder=false)
36
 * @phpstan-extends BusinessLayer<Artist>
37
 */
38
class ArtistBusinessLayer extends BusinessLayer {
39
	protected $mapper; // eclipse the definition from the base class, to help IDE and Scrutinizer to know the actual type
40
	private $logger;
41
42
	private const FORBIDDEN_CHARS_IN_FILE_NAME = '<>:"/\|?*'; // chars forbidden in Windows, on Linux only '/' is technically forbidden
43
44
	public function __construct(ArtistMapper $artistMapper, Logger $logger) {
45
		parent::__construct($artistMapper);
46
		$this->mapper = $artistMapper;
47
		$this->logger = $logger;
48
	}
49
50
	/**
51
	 * Finds all artists who have at least one album
52
	 * @param ?string $name Optionally filter by artist name
53
	 * @param int $matchMode Name match mode, disregarded if @a $name is null
54
	 * @param string|null $createdMin Optional minimum `created` timestamp.
55
	 * @param string|null $createdMax Optional maximum `created` timestamp.
56
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
57
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
58
	 * @return Artist[] artists
59
	 */
60
	public function findAllHavingAlbums(string $userId, int $sortBy=SortBy::Name,
61
			?int $limit=null, ?int $offset=null, ?string $name=null, int $matchMode=MatchMode::Exact,
62
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
63
		return $this->mapper->findAllHavingAlbums(
64
			$userId, $sortBy, $limit, $offset, $name, $matchMode, $createdMin, $createdMax, $updatedMin, $updatedMax);
65
	}
66
67
	/**
68
	 * Finds all artists who have at least one track
69
	 * @param ?string $name Optionally filter by artist name
70
	 * @param int $matchMode Name match mode, disregarded if @a $name is null
71
	 * @param string|null $createdMin Optional minimum `created` timestamp.
72
	 * @param string|null $createdMax Optional maximum `created` timestamp.
73
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
74
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
75
	 * @return Artist[] artists
76
	 */
77
	public function findAllHavingTracks(string $userId, int $sortBy=SortBy::Name,
78
			?int $limit=null, ?int $offset=null, ?string $name=null, int $matchMode=MatchMode::Exact,
79
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
80
		return $this->mapper->findAllHavingTracks(
81
			$userId, $sortBy, $limit, $offset, $name, $matchMode, $createdMin, $createdMax, $updatedMin, $updatedMax);
82
	}
83
84
	/**
85
	 * Returns all artists filtered by genre
86
	 * @return Artist[] artists
87
	 */
88
	public function findAllByGenre(int $genreId, string $userId, ?int $limit=null, ?int $offset=null) : array {
89
		return $this->mapper->findAllByGenre($genreId, $userId, $limit, $offset);
90
	}
91
92
	/**
93
	 * Find most frequently played artists, judged by the total play count of the contained tracks
94
	 * @return Artist[]
95
	 */
96
	public function findFrequentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
97
		$countsPerArtist = $this->mapper->getArtistTracksPlayCount($userId, $limit, $offset);
98
		$ids = \array_keys($countsPerArtist);
99
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
100
	}
101
102
	/**
103
	 * Find most recently played artists
104
	 * @return Artist[]
105
	 */
106
	public function findRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
107
		$playTimePerArtist = $this->mapper->getLatestArtistPlayTimes($userId, $limit, $offset);
108
		$ids = \array_keys($playTimePerArtist);
109
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
110
	}
111
112
	/**
113
	 * Find least recently played artists
114
	 * @return Artist[]
115
	 */
116
	public function findNotRecentPlay(string $userId, ?int $limit=null, ?int $offset=null) : array {
117
		$playTimePerArtist = $this->mapper->getFurthestArtistPlayTimes($userId, $limit, $offset);
118
		$ids = \array_keys($playTimePerArtist);
119
		return $this->findById($ids, $userId, /*preserveOrder=*/true);
120
	}
121
122
	/**
123
	 * Adds an artist if it does not exist already or updates an existing artist
124
	 * @param string|null $name the name of the artist
125
	 * @param string $userId the name of the user
126
	 * @return Artist The added/updated artist
127
	 */
128
	public function addOrUpdateArtist(?string $name, string $userId) : Artist {
129
		$artist = new Artist();
130
		$artist->setName(Util::truncate($name, 256)); // some DB setups can't truncate automatically to column max size
131
		$artist->setUserId($userId);
132
		$artist->setHash(\hash('md5', \mb_strtolower($name ?? '')));
133
		return $this->mapper->updateOrInsert($artist);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mapper->updateOrInsert($artist) returns the type OCA\Music\Db\Entity which includes types incompatible with the type-hinted return OCA\Music\Db\Artist.
Loading history...
134
	}
135
136
	/**
137
	 * Use the given file as cover art for an artist if there exists an artist
138
	 * with name matching the file name.
139
	 * @param File $imageFile
140
	 * @param string $userId
141
	 * @return int[] IDs of the modified artists; usually there should be 0 or 1 of these but
142
	 *					in some special occasions there could be more
143
	 */
144
	public function updateCover(File $imageFile, string $userId, IL10N $l10n) : array {
145
		$name = \pathinfo($imageFile->getName(), PATHINFO_FILENAME);
146
		\assert(\is_string($name)); // for scrutinizer
147
148
		$matches = $this->findAllByNameMatchingFilename($name, $userId, $l10n);
149
150
		$artistIds = [];
151
		foreach ($matches as $artist) {
152
			$artist->setCoverFileId($imageFile->getId());
153
			$this->mapper->update($artist);
154
			$artistIds[] = $artist->getId();
155
		}
156
157
		return $artistIds;
158
	}
159
160
	/**
161
	 * Match the given files by file name to the artist names. If there is a matching
162
	 * artist with no cover image already set, the matched file is set to be used as
163
	 * cover for this artist.
164
	 * @param File[] $imageFiles
165
	 * @param string $userId
166
	 * @return bool true if any artist covers were updated; false otherwise
167
	 */
168
	public function updateCovers(array $imageFiles, string $userId, IL10N $l10n) : bool {
169
		$updated = false;
170
171
		// Construct a lookup table for the images as there may potentially be
172
		// a huge amount of them. Any of the characters forbidden in Windows file names
173
		// may be replaced with an underscore, which is taken into account when building
174
		// the LUT.
175
		$replacedChars = \str_split(self::FORBIDDEN_CHARS_IN_FILE_NAME);
176
		\assert(\is_array($replacedChars)); // for scrutinizer
177
		$imageLut = [];
178
		foreach ($imageFiles as $imageFile) {
179
			$imageName = \pathinfo($imageFile->getName(), PATHINFO_FILENAME);
180
			$lookupName = \str_replace($replacedChars, '_', $imageName);
181
			$imageLut[$lookupName][] = ['name' => $imageName, 'file' => $imageFile];
182
		}
183
184
		$artists = $this->findAll($userId);
185
186
		foreach ($artists as $artist) {
187
			if ($artist->getCoverFileId() === null) {
188
				$artistLookupName = \str_replace($replacedChars, '_', $artist->getNameString($l10n));
189
				$lutEntries = $imageLut[$artistLookupName] ?? [];
190
				foreach ($lutEntries as $lutEntry) {
191
					if (self::filenameMatchesArtist($lutEntry['name'], $artist, $l10n)) {
192
						$artist->setCoverFileId($lutEntry['file']->getId());
193
						$this->mapper->update($artist);
194
						$updated = true;
195
						break;
196
					}
197
				}
198
			}
199
		}
200
201
		return $updated;
202
	}
203
204
	/**
205
	 * removes the given cover art files from artists
206
	 * @param integer[] $coverFileIds the file IDs of the cover images
207
	 * @param string[]|null $userIds the users whose music library is targeted; all users are targeted if omitted
208
	 * @return Artist[] artists which got modified, empty array if none
209
	 */
210
	public function removeCovers(array $coverFileIds, ?array $userIds=null) : array {
211
		return $this->mapper->removeCovers($coverFileIds, $userIds);
212
	}
213
214
	/**
215
	 * Find artists by name so that the characters forbidden on Windows file system are allowed to be
216
	 * replaced with underscores. In Linux, '/' would be the only truly forbidden character in paths
217
	 * but using characters forbidden in Windows might cause difficulties with interoperability.
218
	 * Support also finding by the localized "Unknown artist" string.
219
	 */
220
	private function findAllByNameMatchingFilename(string $name, string $userId, IL10N $l10n) : array {
221
		// we want to make '_' match any forbidden character on Linux or Windows but '%' in the
222
		// search pattern should not be handled as a wildcard but as literal
223
		$name = \str_replace('%', '\%', $name);
224
225
		$potentialMatches = $this->findAllByName($name, $userId, MatchMode::Wildcards);
226
227
		$matches = \array_filter($potentialMatches, function(Artist $artist) use ($name, $l10n) : bool {
228
			return self::filenameMatchesArtist($name, $artist, $l10n);
229
		});
230
231
		if ($name == Artist::unknownNameString($l10n)) {
232
			$matches = \array_merge($matches, $this->findAllByName(null, $userId));
233
		}
234
235
		return $matches;
236
	}
237
238
	/**
239
	 * Check if the given file name matches the given artist. The file name should be given without the extension.
240
	 */
241
	private static function filenameMatchesArtist(string $filename, Artist $artist, IL10N $l10n) : bool {
242
		$length = \strlen($filename);
243
		$artistName = $artist->getNameString($l10n);
244
		if ($length !== \strlen($artistName)) {
245
			return false;
246
		} elseif ($filename == $artistName) {
247
			return true; // exact match
248
		} else {
249
			// iterate over all the bytes and require that all the other bytes are equal but
250
			// underscores are allowed to match any forbidden filesystem chracter
251
			$matchedChars = self::FORBIDDEN_CHARS_IN_FILE_NAME . '_';
252
			for ($i = 0; $i < $length; ++$i) {
253
				if ($filename[$i] === '_') {
254
					if (\strpos($matchedChars, $artistName[$i]) === false) {
255
						return false;
256
					}
257
				} elseif ($filename[$i] !== $artistName[$i]) {
258
					return false;
259
				}
260
			}
261
			return true;
262
		}
263
	}
264
}
265