Completed
Push — scanner_improvements ( 4bed41 )
by Pauli
10:32
created

Scanner::update()   D

Complexity

Conditions 9
Paths 10

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 4.909
c 0
b 0
f 0
cc 9
eloc 20
nc 10
nop 4
1
<?php
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
 * @copyright Morris Jobke 2013, 2014
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use OC\Hooks\PublicEmitter;
16
17
use \OCP\Files\Folder;
18
use \OCP\IConfig;
19
20
use \OCA\Music\AppFramework\Core\Logger;
21
22
use \OCA\Music\BusinessLayer\ArtistBusinessLayer;
23
use \OCA\Music\BusinessLayer\AlbumBusinessLayer;
24
use \OCA\Music\BusinessLayer\TrackBusinessLayer;
25
use \OCA\Music\BusinessLayer\PlaylistBusinessLayer;
26
use \OCA\Music\Db\Cache;
27
use OCP\IDBConnection;
28
use Symfony\Component\Console\Output\OutputInterface;
29
30
31
class Scanner extends PublicEmitter {
32
33
	private $extractor;
34
	private $artistBusinessLayer;
35
	private $albumBusinessLayer;
36
	private $trackBusinessLayer;
37
	private $playlistBusinessLayer;
38
	private $cache;
39
	private $logger;
40
	/** @var IDBConnection  */
41
	private $db;
42
	private $configManager;
43
	private $appName;
44
	private $rootFolder;
45
46
	public function __construct(Extractor $extractor,
47
								ArtistBusinessLayer $artistBusinessLayer,
48
								AlbumBusinessLayer $albumBusinessLayer,
49
								TrackBusinessLayer $trackBusinessLayer,
50
								PlaylistBusinessLayer $playlistBusinessLayer,
51
								Cache $cache,
52
								Logger $logger,
53
								IDBConnection $db,
54
								IConfig $configManager,
55
								$appName,
56
								Folder $rootFolder){
57
		$this->extractor = $extractor;
58
		$this->artistBusinessLayer = $artistBusinessLayer;
59
		$this->albumBusinessLayer = $albumBusinessLayer;
60
		$this->trackBusinessLayer = $trackBusinessLayer;
61
		$this->playlistBusinessLayer = $playlistBusinessLayer;
62
		$this->cache = $cache;
63
		$this->logger = $logger;
64
		$this->db = $db;
65
		$this->configManager = $configManager;
66
		$this->appName = $appName;
67
		$this->rootFolder = $rootFolder;
68
69
		// Trying to enable stream support
70
		if(ini_get('allow_url_fopen') !== '1') {
71
			$this->logger->log('allow_url_fopen is disabled. It is strongly advised to enable it in your php.ini', 'warn');
72
			@ini_set('allow_url_fopen', '1');
73
		}
74
	}
75
76
	/**
77
	 * Gets called by 'post_write' (file creation, file update) and 'post_share' hooks
78
	 * @param \OCP\Files\File $file the file
79
	 * @param string userId
80
	 * @param \OCP\Files\Folder $userHome
81
	 * @param string|null $filePath Deducted from $file if not given
82
	 */
83
	public function update($file, $userId, $userHome, $filePath = null){
84
		if (!$filePath) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filePath of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
85
			$filePath = $file->getPath();
86
		}
87
88
		// debug logging
89
		$this->logger->log("update - $filePath", 'debug');
90
91
		if(!($file instanceof \OCP\Files\File) || !$userId || !($userHome instanceof \OCP\Files\Folder)) {
0 ignored issues
show
Bug introduced by
The class OCP\Files\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
92
			$this->logger->log('Invalid arguments given to Scanner.update - file='.get_class($file).
93
					", userId=$userId, userHome=".get_class($userHome), 'warn');
94
			return;
95
		}
96
97
		// skip files that aren't inside the user specified path
98
		$musicFolder = $this->getUserMusicFolder($userId, $userHome);
99
		$musicPath = $musicFolder->getPath();
100
		if(!self::startsWith($filePath, $musicPath)) {
101
			$this->logger->log("skipped - file is outside of specified path $musicPath", 'debug');
102
			return;
103
		}
104
105
		$mimetype = $file->getMimeType();
106
107
		// debug logging
108
		$this->logger->log("update - mimetype $mimetype", 'debug');
109
		$this->emit('\OCA\Music\Utility\Scanner', 'update', array($filePath));
110
111
		if(self::startsWith($mimetype, 'image')) {
112
			$this->updateImage($file, $userId);
113
		}
114
		else if(self::startsWith($mimetype, 'audio') || self::startsWith($mimetype, 'application/ogg')) {
115
			$this->updateAudio($file, $userId, $userHome, $filePath, $mimetype);
116
		}
117
	}
118
119
	private function updateImage($file, $userId) {
120
		$coverFileId = $file->getId();
121
		$parentFolderId = $file->getParent()->getId();
122
		if ($this->albumBusinessLayer->updateFolderCover($coverFileId, $parentFolderId)) {
123
			$this->logger->log('updateImage - the image was set as cover for some album(s)', 'debug');
124
			$this->cache->remove($userId);
125
		}
126
	}
127
128
	private function updateAudio($file, $userId, $userHome, $filePath, $mimetype) {
129
		if(ini_get('allow_url_fopen')) {
130
131
			$meta = $this->extractMetadata($file, $userHome, $filePath);
132
			$fileId = $file->getId();
133
134
			// debug logging
135
			$this->logger->log('extracted metadata - ' . json_encode($meta), 'debug');
136
137
			// add/update artist and get artist entity
138
			$artist = $this->artistBusinessLayer->addOrUpdateArtist($meta['artist'], $userId);
139
			$artistId = $artist->getId();
140
141
			// add/update albumArtist and get artist entity
142
			$albumArtist = $this->artistBusinessLayer->addOrUpdateArtist($meta['albumArtist'], $userId);
143
			$albumArtistId = $albumArtist->getId();
144
145
			// add/update album and get album entity
146
			$album = $this->albumBusinessLayer->addOrUpdateAlbum(
147
					$meta['album'], $meta['year'], $meta['discNumber'], $albumArtistId, $userId);
148
			$albumId = $album->getId();
149
150
			// add/update track and get track entity
151
			$track = $this->trackBusinessLayer->addOrUpdateTrack($meta['title'], $meta['trackNumber'],
152
					$artistId, $albumId, $fileId, $mimetype, $userId, $meta['length'], $meta['bitrate']);
153
154
			// if present, use the embedded album art as cover for the respective album
155
			if($meta['picture'] != null) {
156
				$this->albumBusinessLayer->setCover($fileId, $albumId);
157
			}
158
			// if this file is an existing file which previously was used as cover for an album but now
159
			// the file no longer contains any embedded album art
160
			else if($this->albumBusinessLayer->albumCoverIsOneOfFiles($albumId, [$fileId])) {
161
				$this->albumBusinessLayer->removeCovers([$fileId]);
162
				$this->findEmbeddedCoverForAlbum($albumId, $userId, $userHome);
163
			}
164
165
			// invalidate the cache as the music collection was changed
166
			$this->cache->remove($userId);
167
		
168
			// debug logging
169
			$this->logger->log('imported entities - ' .
170
					"artist: $artistId, albumArtist: $albumArtistId, album: $albumId, track: {$track->getId()}",
171
					'debug');
172
		}
173
	}
174
175
	private function extractMetadata($file, $userHome, $filePath) {
176
		$fieldsFromFileName = self::parseFileName($file->getName());
177
		$fileInfo = $this->extractor->extract($file);
178
		$meta = [];
179
180
		// Track artist and album artist
181
		$meta['artist'] = self::getId3Tag($fileInfo, 'artist');
182
		$meta['albumArtist'] = self::getFirstOfId3Tags($fileInfo, ['band', 'albumartist', 'album artist', 'album_artist']);
183
184
		// use artist and albumArtist as fallbacks for each other
185
		if(self::isNullOrEmpty($meta['albumArtist'])){
186
			$meta['albumArtist'] = $meta['artist'];
187
		}
188
189
		if(self::isNullOrEmpty($meta['artist'])){
190
			$meta['artist'] = $meta['albumArtist'];
191
		}
192
193
		// set 'Unknown Artist' in case neither artist nor albumArtist was found
194
		if(self::isNullOrEmpty($meta['artist'])){
195
			$meta['artist'] = null;
196
			$meta['albumArtist'] = null;
197
		}
198
199
		// title
200
		$meta['title'] = self::getId3Tag($fileInfo, 'title');
201
		if(self::isNullOrEmpty($meta['title'])){
202
			$meta['title'] = $fieldsFromFileName['title'];
203
		}
204
205
		// album
206
		$meta['album'] = self::getId3Tag($fileInfo, 'album');
207
		if(self::isNullOrEmpty($meta['album'])){
208
			// album name not set in fileinfo, use parent folder name as album name unless it is the root folder
209
			$dirPath = dirname($filePath);
210
			if ($userHome->getPath() === $dirPath) {
211
				$meta['album'] = null;
212
			} else {
213
				$meta['album'] = basename($dirPath);
214
			}
215
		}
216
217
		// track number
218
		$meta['trackNumber'] = self::getFirstOfId3Tags($fileInfo, ['track_number', 'tracknumber', 'track'],
219
				$fieldsFromFileName['track_number']);
220
		$meta['trackNumber'] = self::normalizeOrdinal($meta['trackNumber']);
221
222
		// disc number
223
		$meta['discNumber'] = self::getFirstOfId3Tags($fileInfo, ['discnumber', 'part_of_a_set'], '1');
224
		$meta['discNumber'] = self::normalizeOrdinal($meta['discNumber']);
225
226
		// year
227
		$meta['year'] = self::getFirstOfId3Tags($fileInfo, ['year', 'date']);
228
		$meta['year'] = self::normalizeYear($meta['year']);
229
230
		$meta['picture'] = self::getId3Tag($fileInfo, 'picture');
231
232
		if (array_key_exists('playtime_seconds', $fileInfo)) {
233
			$meta['length'] = ceil($fileInfo['playtime_seconds']);
234
		} else {
235
			$meta['length'] = null;
236
		}
237
238
		if (array_key_exists('audio', $fileInfo) && array_key_exists('bitrate', $fileInfo['audio'])) {
239
			$meta['bitrate'] = $fileInfo['audio']['bitrate'];
240
		} else {
241
			$meta['bitrate'] = null;
242
		}
243
244
		return $meta;
245
	}
246
247
	/**
248
	 * @param \OCP\Files\Node $musicFile
249
	 * @return Array with image MIME and content or null
250
	 */
251
	public function parseEmbeddedCoverArt($musicFile){
252
		$fileInfo = $this->extractor->extract($musicFile);
253
		return self::getId3Tag($fileInfo, 'picture');
254
	}
255
256
	private function deleteAudio($fileIds, $userId=null){
257
		$this->logger->log('deleteAudio - '. implode(', ', $fileIds) , 'debug');
258
		$this->emit('\OCA\Music\Utility\Scanner', 'delete', array($fileIds, $userId));
259
260
		$result = $this->trackBusinessLayer->deleteTracks($fileIds, $userId);
261
262
		if ($result) { // one or more tracks were removed
263
			// remove obsolete artists and albums, and track references in playlists
264
			$this->albumBusinessLayer->deleteById($result['obsoleteAlbums']);
265
			$this->artistBusinessLayer->deleteById($result['obsoleteArtists']);
266
			$this->playlistBusinessLayer->removeTracksFromAllLists($result['deletedTracks']);
267
268
			// check if a removed track was used as embedded cover art file for a remaining album
269
			foreach ($result['remainingAlbums'] as $albumId) {
270
				if ($this->albumBusinessLayer->albumCoverIsOneOfFiles($albumId, $fileIds)) {
271
					$this->albumBusinessLayer->setCover(null, $albumId);
272
					$this->findEmbeddedCoverForAlbum($albumId);
273
				}
274
			}
275
276
			// invalidate the cache of all affected users as their music collections were changed
277
			foreach ($result['affectedUsers'] as $affectedUser) {
278
				$this->cache->remove($affectedUser);
279
			}
280
281
			$this->logger->log('removed entities - ' . json_encode($result), 'debug');
282
		}
283
284
		return $result !== false; // true if anything was removed
285
	}
286
287
	private function deleteImage($fileIds, $userId=null){
288
		$this->logger->log('deleteImage - '. implode(', ', $fileIds) , 'debug');
289
290
		$affectedUsers = $this->albumBusinessLayer->removeCovers($fileIds, $userId);
291
		$deleted = (count($affectedUsers) > 0);
292
		if ($deleted) {
293
			foreach ($affectedUsers as $affectedUser) {
294
				$this->cache->remove($affectedUser);
295
			}
296
		}
297
298
		return $deleted;
299
	}
300
301
	/**
302
	 * Gets called by 'unshare' hook and 'delete' hook
303
	 *
304
	 * @param int $fileId ID of the deleted files
305
	 * @param string|null $userId the ID of the user to remove the file from; if omitted,
306
	 *                            the file is removed from all users (ie. owner and sharees)
307
	 */
308
	public function delete($fileId, $userId=null){
309
		if (!$this->deleteAudio([$fileId], $userId) && !$this->deleteImage([$fileId], $userId)) {
310
			$this->logger->log("deleted file $fileId was not an indexed " .
311
					'audio file or a cover image' , 'debug');
312
		}
313
	}
314
315
	/**
316
	 * Remove all audio files and cover images in the given folder from the database.
317
	 * This gets called when a folder is deleted or unshared from the user.
318
	 * 
319
	 * @param \OCP\Files\Folder $folder
320
	 * @param string|null $userId the id of the user to remove the file from; if omitted,
321
	 *                            the file is removed from all users (ie. owner and sharees)
322
	 */
323
	public function deleteFolder($folder, $userId=null) {
324
		$audioFiles = array_merge(
325
				$folder->searchByMime('audio'),
326
				$folder->searchByMime('application/ogg')
327
		);
328
		$this->deleteAudio(self::idsFromArray($audioFiles), $userId);
329
330
		$imageFiles = $folder->searchByMime('image');
331
		$this->deleteImage(self::idsFromArray($imageFiles), $userId);
332
	}
333
334
	public function getUserMusicFolder($userId, $userHome) {
335
		$musicPath = $this->configManager->getUserValue($userId, $this->appName, 'path');
336
337
		if ($musicPath !== null && $musicPath !== '/' && $musicPath !== '') {
338
			return $userHome->get($musicPath);
339
		} else {
340
			return $userHome;
341
		}
342
	}
343
344
	/**
345
	 * search for files by mimetype inside an optional user specified path
346
	 *
347
	 * @return \OCP\Files\File[]
348
	 */
349
	public function getMusicFiles($userId, $userHome) {
350
		try {
351
			$folder = $this->getUserMusicFolder($userId, $userHome);
352
		} catch (\OCP\Files\NotFoundException $e) {
353
			return array();
354
		}
355
356
		$audio = $folder->searchByMime('audio');
357
		$ogg = $folder->searchByMime('application/ogg');
358
359
		return array_merge($audio, $ogg);
360
	}
361
362
	public function getScannedFiles($userId) {
363
		return $this->trackBusinessLayer->findAllFileIds($userId);
364
	}
365
366
	public function getUnscannedMusicFileIds($userId, $userHome) {
367
		$scannedIds = $this->getScannedFiles($userId);
368
		$musicFiles = $this->getMusicFiles($userId, $userHome);
369
		$allIds = self::idsFromArray($musicFiles);
370
		$unscannedIds = array_values(array_diff($allIds, $scannedIds));
371
372
		$count = count($unscannedIds);
373
		if ($count) {
374
			$this->logger->log("Found $count unscanned music files for user $userId", 'info');
375
		} else {
376
			$this->logger->log("No unscanned music files for user $userId", 'debug');
377
		}
378
379
		return $unscannedIds;
380
	}
381
382
	public function scanFiles($userId, $userHome, $fileIds, OutputInterface $debugOutput = null) {
383
		$count = count($fileIds);
384
		$this->logger->log("Scanning $count files of user $userId", 'debug');
385
386
		// back up the execution time limit
387
		$executionTime = intval(ini_get('max_execution_time'));
388
		// set execution time limit to unlimited
389
		set_time_limit(0);
390
391
		$count = 0;
392
		foreach ($fileIds as $fileId) {
393
			$fileNodes = $userHome->getById($fileId);
394
			if (count($fileNodes) > 0) {
395
				$file = $fileNodes[0];
396
				if($debugOutput) {
397
					$before = memory_get_usage(true);
398
				}
399
				$this->update($file, $userId, $userHome);
400
				if($debugOutput) {
401
					$after = memory_get_usage(true);
402
					$diff = $after - $before;
403
					$afterFileSize = new FileSize($after);
404
					$diffFileSize = new FileSize($diff);
405
					$humanFilesizeAfter = $afterFileSize->getHumanReadable();
406
					$humanFilesizeDiff = $diffFileSize->getHumanReadable();
407
					$path = $file->getPath();
408
					$debugOutput->writeln("\e[1m $count \e[0m $humanFilesizeAfter \e[1m $diff \e[0m ($humanFilesizeDiff) $path");
409
				}
410
				$count++;
411
			}
412
			else {
413
				$this->logger->log("File with id $fileId not found for user $userId", 'warn');
414
			}
415
		}
416
417
		// reset execution time limit
418
		set_time_limit($executionTime);
419
420
		return $count;
421
	}
422
423
	/**
424
	 * Wipe clean the music database of the given user, or all users
425
	 * @param string $userId
426
	 * @param boolean $allUsers
427
	 */
428
	public function resetDb($userId, $allUsers = false) {
429
		if ($userId && $allUsers) {
430
			throw new InvalidArgumentException('userId should be null if allUsers targeted');
431
		}
432
433
		$sqls = array(
434
				'DELETE FROM `*PREFIX*music_tracks`',
435
				'DELETE FROM `*PREFIX*music_albums`',
436
				'DELETE FROM `*PREFIX*music_artists`',
437
				'UPDATE *PREFIX*music_playlists SET track_ids=NULL',
438
				'DELETE FROM `*PREFIX*music_cache`'
439
		);
440
441
		foreach ($sqls as $sql) {
442
			$params = [];
443
			if (!$allUsers) {
444
				$sql .=  ' WHERE `user_id` = ?';
445
				$params[] = $userId;
446
			}
447
			$this->db->executeUpdate($sql, $params);
448
		}
449
450
		if ($allUsers) {
451
			$this->logger->log("Erased music databases of all users", 'info');
452
		} else {
453
			$this->logger->log("Erased music database of user $userId", 'info');
454
		}
455
	}
456
457
	/**
458
	 * Update music path
459
	 */
460
	public function updatePath($path, $userId) {
461
		// TODO currently this function is quite dumb
462
		// it just drops all entries of an user from the tables
463
		$this->logger->log("Changing music collection path of user $userId to $path", 'info');
464
		$this->resetDb($userId);
465
	}
466
467
	public function findCovers() {
468
		$affectedUsers = $this->albumBusinessLayer->findCovers();
469
		// scratch the cache for those users whose music collection was touched
470
		foreach ($affectedUsers as $user) {
471
			$this->cache->remove($user);
472
			$this->logger->log('album cover(s) were found for user '. $user , 'debug');
473
		}
474
		return !empty($affectedUsers);
475
	}
476
477
	public function resolveUserFolder($userId) {
478
		$dir = '/' . $userId;
479
		$root = $this->rootFolder;
480
481
		// copy of getUserServer of server container
482
		$folder = null;
483
484 View Code Duplication
		if (!$root->nodeExists($dir)) {
485
			$folder = $root->newFolder($dir);
486
		} else {
487
			$folder = $root->get($dir);
488
		}
489
490
		$dir = '/files';
491 View Code Duplication
		if (!$folder->nodeExists($dir)) {
492
			$folder = $folder->newFolder($dir);
493
		} else {
494
			$folder = $folder->get($dir);
495
		}
496
	
497
		return $folder;
498
	}
499
500
	private static function idsFromArray(array $arr) {
501
		return array_map(function($i) { return $i->getId(); }, $arr);
502
	}
503
504
	private static function startsWith($string, $potentialStart) {
505
		return substr($string, 0, strlen($potentialStart)) === $potentialStart;
506
	}
507
508
	private static function isNullOrEmpty($string) {
509
		return $string === null || $string === '';
510
	}
511
512
	private static function getId3Tag($fileInfo, $tag) {
513
		if(array_key_exists('comments', $fileInfo)) {
514
			$comments = $fileInfo['comments'];
515
			if(array_key_exists($tag, $comments)) {
516
				return $comments[$tag][0];
517
			}
518
		}
519
		return null;
520
	}
521
522
	private static function getFirstOfId3Tags($fileInfo, array $tags, $defaultValue = null) {
523
		foreach ($tags as $tag) {
524
			$value = self::getId3Tag($fileInfo, $tag);
525
			if (!self::isNullOrEmpty($value)) {
526
				return $value;
527
			}
528
		}
529
		return $defaultValue;
530
	}
531
532
	private static function normalizeOrdinal($ordinal) {
533
		// convert format '1/10' to '1'
534
		$tmp = explode('/', $ordinal);
535
		$ordinal = $tmp[0];
536
537
		// check for numeric values - cast them to int and verify it's a natural number above 0
538
		if(is_numeric($ordinal) && ((int)$ordinal) > 0) {
539
			$ordinal = (int)$ordinal;
540
		} else {
541
			$ordinal = null;
542
		}
543
544
		return $ordinal;
545
	}
546
547
	private static function parseFileName($fileName) {
548
		// If the file name starts e.g like "12 something" or "12. something" or "12 - something",
549
		// the preceeding number is extracted as track number. Everything after the optional track
550
		// number + delimiters part but before the file extension is extracted as title.
551
		// The file extension consists of a '.' followed by 1-4 "word characters".
552
		if(preg_match('/^((\d+)\s*[\s.-]\s*)?(.+)\.(\w{1,4})$/', $fileName, $matches) === 1) {
553
			return ['track_number' => $matches[2], 'title' => $matches[3]];
554
		} else {
555
			return ['track_number' => null, 'title' => $fileName];
556
		}
557
	}
558
559
	private static function normalizeYear($date) {
560
		if(ctype_digit($date)) {
561
			return $date; // the date is a valid year as-is
562
		} else if(preg_match('/^(\d\d\d\d)-\d\d-\d\d.*/', $date, $matches) === 1) {
563
			return $matches[1]; // year from ISO-formatted date yyyy-mm-dd
564
		} else {
565
			return null;
566
		}
567
	}
568
569
	/**
570
	 * Loop through the tracks of an album and set the first track containing embedded cover art
571
	 * as cover file for the album
572
	 * @param int $albumId
573
	 * @param string|null $userId, deducted from $albumId if omitted
0 ignored issues
show
Documentation introduced by
There is no parameter named $userId,. Did you maybe mean $userId?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
574
	 * @param Folder|null $userFolder, deducted from $userId if omitted
0 ignored issues
show
Documentation introduced by
There is no parameter named $userFolder,. Did you maybe mean $userFolder?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
575
	 */
576
	private function findEmbeddedCoverForAlbum($albumId, $userId=null, $userFolder=null) {
577
		if (!$userId) {
578
			$userId = $this->albumBusinessLayer->findAlbumOwner($albumId);
579
		}
580
		if (!$userFolder) {
581
			$userFolder = $this->resolveUserFolder($userId);
582
		}
583
584
		$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId);
585
		foreach ($tracks as $track) {
586
			$nodes = $userFolder->getById($track->getFileId());
587
			if(count($nodes) > 0) {
588
				// parse the first valid node and check if it contains embedded cover art
589
				$image = $this->parseEmbeddedCoverArt($nodes[0]);
590
				if ($image != null) {
591
					$this->albumBusinessLayer->setCover($track->getFileId(), $albumId);
592
					break;
593
				}
594
			}
595
		}
596
	}
597
}
598