Completed
Push — feature/464_small_player_for_f... ( 144ef9...383208 )
by Pauli
11:38
created

Scanner::getFileInfo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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