Completed
Push — scalability_improvements ( 1f54dd...dcfdac )
by Pauli
19:38 queued 05:25
created

Scanner::__construct()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 31
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 27
nc 2
nop 12

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
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 $userId;
43
	private $configManager;
44
	private $appName;
45
	private $userFolder;
46
47
	public function __construct(Extractor $extractor,
48
								ArtistBusinessLayer $artistBusinessLayer,
49
								AlbumBusinessLayer $albumBusinessLayer,
50
								TrackBusinessLayer $trackBusinessLayer,
51
								PlaylistBusinessLayer $playlistBusinessLayer,
52
								Cache $cache,
53
								Logger $logger,
54
								IDBConnection $db,
55
								$userId,
56
								IConfig $configManager,
57
								$appName,
58
								$userFolder = null){
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->logger = $logger;
66
		$this->db = $db;
67
		$this->userId = $userId;
68
		$this->configManager = $configManager;
69
		$this->appName = $appName;
70
		$this->userFolder = $userFolder;
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
	public function updateById($fileId, $userId) {
80
		// TODO properly initialize the user folder for external events (upload to public share)
81
		if ($this->userFolder === null) {
82
			return;
83
		}
84
85
		try {
86
			$files = $this->userFolder->getById($fileId);
87
			if(count($files) > 0) {
88
				// use first result
89
				$this->update($files[0], $userId, $this->userFolder);
90
			}
91
		} catch (\OCP\Files\NotFoundException $e) {
92
			// just ignore the error
93
			$this->logger->log('updateById - file not found - '. $fileId , 'debug');
94
		}
95
	}
96
97
	/**
98
	 * Gets called by 'post_write' hook (file creation, file update)
99
	 * @param \OCP\Files\Node $file the file
100
	 */
101
	public function update($file, $userId, $userHome){
102
		// debug logging
103
		$this->logger->log('update - '. $file->getPath() , 'debug');
104
105
		if(!($file instanceof \OCP\Files\File)) {
106
			return;
107
		}
108
109
		// TODO find a way to get this for a sharee
110
		$isSharee = $userId && $this->userId !== $userId;
111
112
		if(!$userId) {
113
			$userId = $this->userId;
114
		}
115
116
		if(!$userHome) {
117
			$userHome = $this->userFolder;
118
		}
119
120
		$musicPath = $this->configManager->getUserValue($userId, $this->appName, 'path');
121
		if($musicPath !== null || $musicPath !== '/' || $musicPath !== '') {
122
			// TODO verify
123
			$musicPath = $userHome->get($musicPath)->getPath();
124
			// skip files that aren't inside the user specified path (and also for sharees - TODO remove this)
125
			if(!$isSharee && !self::startsWith($file->getPath(), $musicPath)) {
126
				$this->logger->log('skipped - outside of specified path' , 'debug');
127
				return;
128
			}
129
		}
130
131
		$mimetype = $file->getMimeType();
132
133
		// debug logging
134
		$this->logger->log('update - mimetype '. $mimetype , 'debug');
135
		$this->emit('\OCA\Music\Utility\Scanner', 'update', array($file->getPath()));
136
137
		if(self::startsWith($mimetype, 'image')) {
138
			$coverFileId = $file->getId();
139
			$parentFolderId = $file->getParent()->getId();
140
			if ($this->albumBusinessLayer->updateFolderCover($coverFileId, $parentFolderId)) {
141
				$this->logger->log('update - the image was set as cover for some album(s)', 'debug');
142
				$this->cache->remove($userId);
143
			}
144
			return;
145
		}
146
147
		if(!self::startsWith($mimetype, 'audio') && !self::startsWith($mimetype, 'application/ogg')) {
148
			return;
149
		}
150
151
		if(ini_get('allow_url_fopen')) {
152
153
			$fieldsFromFileName = self::parseFileName($file->getName());
154
			$fileInfo = $this->extractor->extract($file);
155
156
			// Track artist and album artist
157
			$artist = self::getId3Tag($fileInfo, 'artist');
158
			$albumArtist = self::getFirstOfId3Tags($fileInfo, ['band', 'albumartist', 'album artist', 'album_artist']);
159
160
			// use artist and albumArtist as fallbacks for each other
161
			if(self::isNullOrEmpty($albumArtist)){
162
				$albumArtist = $artist;
163
			}
164
165
			if(self::isNullOrEmpty($artist)){
166
				$artist = $albumArtist;
167
			}
168
169
			// set 'Unknown Artist' in case neither artist nor albumArtist was found
170
			if(self::isNullOrEmpty($artist)){
171
				$artist = null;
172
				$albumArtist = null;
173
			}
174
175
			// title
176
			$title = self::getId3Tag($fileInfo, 'title');
177
			if(self::isNullOrEmpty($title)){
178
				$title = $fieldsFromFileName['title'];
179
			}
180
181
			// album
182
			$album = self::getId3Tag($fileInfo, 'album');
183
			if(self::isNullOrEmpty($album)){
184
				// album name not set in fileinfo, use parent folder name as album name unless it is the root folder
185
				if ( $userHome->getId() === $file->getParent()->getId() ) {
186
					$album = null;
187
				} else {
188
					$album = $file->getParent()->getName();
189
				}
190
			}
191
192
			// track number
193
			$trackNumber = self::getFirstOfId3Tags($fileInfo, ['track_number', 'tracknumber', 'track'], 
194
					$fieldsFromFileName['track_number']);
195
			$trackNumber = self::normalizeOrdinal($trackNumber);
196
197
			// disc number
198
			$discNumber = self::getFirstOfId3Tags($fileInfo, ['discnumber', 'part_of_a_set'], '1');
199
			$discNumber = self::normalizeOrdinal($discNumber);
200
201
			// year
202
			$year = self::getFirstOfId3Tags($fileInfo, ['year', 'date']);
203
			$year = self::normalizeYear($year);
204
205
			$fileId = $file->getId();
206
207
			$length = null;
208
			if (array_key_exists('playtime_seconds', $fileInfo)) {
209
				$length = ceil($fileInfo['playtime_seconds']);
210
			}
211
212
			$bitrate = null;
213
			if (array_key_exists('audio', $fileInfo) && array_key_exists('bitrate', $fileInfo['audio'])) {
214
				$bitrate = $fileInfo['audio']['bitrate'];
215
			}
216
217
			// debug logging
218
			$this->logger->log('extracted metadata - ' .
219
				sprintf('artist: %s, albumArtist: %s, album: %s, title: %s, track#: %s, disc#: %s, year: %s, mimetype: %s, length: %s, bitrate: %s, fileId: %i, this->userId: %s, userId: %s',
220
					$artist, $albumArtist, $album, $title, $trackNumber, $discNumber, $year, $mimetype, $length, $bitrate, $fileId, $this->userId, $userId), 'debug');
221
222
			// add artist and get artist entity
223
			$artist = $this->artistBusinessLayer->addArtistIfNotExist($artist, $userId);
224
			$artistId = $artist->getId();
225
226
			// add albumArtist and get artist entity
227
			$albumArtist = $this->artistBusinessLayer->addArtistIfNotExist($albumArtist, $userId);
228
			$albumArtistId = $albumArtist->getId();
229
230
			// add album and get album entity
231
			$album = $this->albumBusinessLayer->addAlbumIfNotExist($album, $year, $discNumber, $albumArtistId, $userId);
232
			$albumId = $album->getId();
233
234
			// add track and get track entity; the track gets updated if it already exists
235
			$track = $this->trackBusinessLayer->addTrackIfNotExist($title, $trackNumber, $artistId,
236
				$albumId, $fileId, $mimetype, $userId, $length, $bitrate);
237
238
			// if present, use the embedded album art as cover for the respective album
239
			if(self::getId3Tag($fileInfo, 'picture') != null) {
240
				$this->albumBusinessLayer->setCover($fileId, $albumId);
241
			}
242
			// if this file is an existing file which previously was used as cover for an album but now
243
			// the file no longer contains any embedded album art
244
			else if($this->fileIsCoverForAlbum($fileId, $albumId, $userId)) {
245
				$this->albumBusinessLayer->removeCover($fileId);
246
				$this->findEmbeddedCoverForAlbum($albumId, $userId);
247
			}
248
249
			// invalidate the cache as the music collection was changed
250
			$this->cache->remove($userId);
251
252
			// debug logging
253
			$this->logger->log('imported entities - ' .
254
				sprintf('artist: %d, albumArtist: %d, album: %d, track: %d', $artistId, $albumArtistId, $albumId, $track->getId()),
255
				'debug');
256
		}
257
258
	}
259
260
	/**
261
	 * @param \OCP\Files\Node $musicFile
262
	 * @return Array with image MIME and content or null
263
	 */
264
	public function parseEmbeddedCoverArt($musicFile){
265
		$fileInfo = $this->extractor->extract($musicFile);
266
		return self::getId3Tag($fileInfo, 'picture');
267
	}
268
269
	/**
270
	 * Get called by 'unshare' hook and 'delete' hook
271
	 * @param int $fileId the id of the deleted file
272
	 * @param string $userId the user id of the user to delete the track from
273
	 */
274
	public function delete($fileId, $userId = null){
275
		// debug logging
276
		$this->logger->log('delete - '. $fileId , 'debug');
277
		$this->emit('\OCA\Music\Utility\Scanner', 'delete', array($fileId, $userId));
278
279
		if ($userId === null) {
280
			$userId = $this->userId;
281
		}
282
283
		$result = $this->trackBusinessLayer->deleteTrack($fileId, $userId);
284
285
		if ($result) { // this was a track file
286
			// remove obsolete artists and albums, and track references in playlists
287
			$this->albumBusinessLayer->deleteById($result['obsoleteAlbums']);
288
			$this->artistBusinessLayer->deleteById($result['obsoleteArtists']);
289
			$this->playlistBusinessLayer->removeTracksFromAllLists($result['deletedTracks']);
290
291
			// check if the removed track was used as embedded cover art file for a remaining album
292
			foreach ($result['remainingAlbums'] as $albumId) {
293
				if ($this->fileIsCoverForAlbum($fileId, $albumId, $userId)) {
294
					$this->albumBusinessLayer->removeCover($fileId);
295
					$this->findEmbeddedCoverForAlbum($albumId, $userId);
296
				}
297
			}
298
299
			// invalidate the cache as the music collection was changed
300
			$this->cache->remove($userId);
301
302
			// debug logging
303
			$this->logger->log('removed entities - ' . json_encode($result), 'debug');
304
		}
305
		// maybe this was an image file
306
		else if ($this->albumBusinessLayer->removeCover($fileId)) {
307
			$this->cache->remove($userId);
308
		}
309
	}
310
311
	/**
312
	 * search for files by mimetype inside an optional user specified path
313
	 *
314
	 * @return \OCP\Files\Node[]
315
	 */
316
	public function getMusicFiles($userId, $folder) {
317
		$musicPath = $this->configManager->getUserValue($userId, $this->appName, 'path');
318
319
		if($musicPath !== null && $musicPath !== '/' && $musicPath !== '') {
320
			try {
321
				$folder = $folder->get($musicPath);
322
			} catch (\OCP\Files\NotFoundException $e) {
323
				return array();
324
			}
325
		}
326
327
		$audio = $folder->searchByMime('audio');
328
		$ogg = $folder->searchByMime('application/ogg');
329
330
		return array_merge($audio, $ogg);
331
	}
332
333
	public function getScannedFiles($userId) {
334
		return $this->trackBusinessLayer->findAllFileIds($userId);
335
	}
336
337
	public function getUnscannedMusicFileIds($userId, $userHome) {
338
		$scannedIds = $this->getScannedFiles($userId);
339
		$musicFiles = $this->getMusicFiles($userId, $userHome);
340
		$allIds = array_map(function($f) { return $f->getId(); }, $musicFiles);
341
		$unscannedIds = array_values(array_diff($allIds, $scannedIds));
342
343
		$count = count($unscannedIds);
344
		if ($count) {
345
			$this->logger->log("Found $count unscanned music files for user $userId", 'info');
346
		} else {
347
			$this->logger->log("No unscanned music files for user $userId", 'debug');
348
		}
349
350
		return $unscannedIds;
351
	}
352
353
	public function scanFiles($userId, $userHome, $fileIds, OutputInterface $debugOutput = null) {
354
		$count = count($fileIds);
355
		$this->logger->log("Scanning $count files of user $userId", 'debug');
356
357
		// back up the execution time limit
358
		$executionTime = intval(ini_get('max_execution_time'));
359
		// set execution time limit to unlimited
360
		set_time_limit(0);
361
362
		$count = 0;
363
		foreach ($fileIds as $fileId) {
364
			$fileNodes = $userHome->getById($fileId);
365
			if (count($fileNodes) > 0) {
366
				$file = $fileNodes[0];
367
				if($debugOutput) {
368
					$before = memory_get_usage(true);
369
				}
370
				$this->update($file, $userId, $userHome);
371
				if($debugOutput) {
372
					$after = memory_get_usage(true);
373
					$diff = $after - $before;
374
					$afterFileSize = new FileSize($after);
375
					$diffFileSize = new FileSize($diff);
376
					$humanFilesizeAfter = $afterFileSize->getHumanReadable();
377
					$humanFilesizeDiff = $diffFileSize->getHumanReadable();
378
					$path = $file->getPath();
379
					$debugOutput->writeln("\e[1m $count \e[0m $humanFilesizeAfter \e[1m $diff \e[0m ($humanFilesizeDiff) $path");
380
				}
381
				$count++;
382
			}
383
			else {
384
				$this->logger->log("File with id $fileId not found for user $userId", 'warn');
385
			}
386
		}
387
388
		// reset execution time limit
389
		set_time_limit($executionTime);
390
391
		return $count;
392
	}
393
394
	/**
395
	 * Update music path
396
	 */
397
	public function updatePath($path, $userId = null) {
398
		// TODO currently this function is quite dumb
399
		// it just drops all entries of an user from the tables
400
		if ($userId === null) {
401
			$userId = $this->userId;
402
		}
403
404
		$sqls = array(
405
			'DELETE FROM `*PREFIX*music_tracks` WHERE `user_id` = ?;',
406
			'DELETE FROM `*PREFIX*music_albums` WHERE `user_id` = ?;',
407
			'DELETE FROM `*PREFIX*music_artists` WHERE `user_id` = ?;',
408
			'UPDATE *PREFIX*music_playlists SET track_ids=NULL WHERE `user_id` = ?;'
409
		);
410
411
		foreach ($sqls as $sql) {
412
			$this->db->executeUpdate($sql, array($userId));
413
		}
414
415
		$this->cache->remove($userId);
416
	}
417
418
	public function findCovers() {
419
		$affectedUsers = $this->albumBusinessLayer->findCovers();
420
		// scratch the cache for those users whose music collection was touched
421
		foreach ($affectedUsers as $user) {
422
			$this->cache->remove($user);
423
			$this->logger->log('album cover(s) were found for user '. $user , 'debug');
424
		}
425
		return !empty($affectedUsers);
426
	}
427
428
	private static function startsWith($string, $potentialStart) {
429
		return substr($string, 0, strlen($potentialStart)) === $potentialStart;
430
	}
431
432
	private static function isNullOrEmpty($string) {
433
		return $string === null || $string === '';
434
	}
435
436
	private static function getId3Tag($fileInfo, $tag) {
437
		if(array_key_exists('comments', $fileInfo)) {
438
			$comments = $fileInfo['comments'];
439
			if(array_key_exists($tag, $comments)) {
440
				return $comments[$tag][0];
441
			}
442
		}
443
		return null;
444
	}
445
446
	private static function getFirstOfId3Tags($fileInfo, array $tags, $defaultValue = null) {
447
		foreach ($tags as $tag) {
448
			$value = self::getId3Tag($fileInfo, $tag);
449
			if (!self::isNullOrEmpty($value)) {
450
				return $value;
451
			}
452
		}
453
		return $defaultValue;
454
	}
455
456
	private static function normalizeOrdinal($ordinal) {
457
		// convert format '1/10' to '1'
458
		$tmp = explode('/', $ordinal);
459
		$ordinal = $tmp[0];
460
461
		// check for numeric values - cast them to int and verify it's a natural number above 0
462
		if(is_numeric($ordinal) && ((int)$ordinal) > 0) {
463
			$ordinal = (int)$ordinal;
464
		} else {
465
			$ordinal = null;
466
		}
467
468
		return $ordinal;
469
	}
470
471
	private static function parseFileName($fileName) {
472
		// If the file name starts e.g like "12 something" or "12. something" or "12 - something",
473
		// the preceeding number is extracted as track number. Everything after the optional track
474
		// number + delimiters part but before the file extension is extracted as title.
475
		// The file extension consists of a '.' followed by 1-4 "word characters".
476
		if(preg_match('/^((\d+)\s*[\s.-]\s*)?(.+)\.(\w{1,4})$/', $fileName, $matches) === 1) {
477
			return ['track_number' => $matches[2], 'title' => $matches[3]];
478
		} else {
479
			return ['track_number' => null, 'title' => $fileName];
480
		}
481
	}
482
483
	private static function normalizeYear($date) {
484
		if(ctype_digit($date)) {
485
			return $date; // the date is a valid year as-is
486
		} else if(preg_match('/^(\d\d\d\d)-\d\d-\d\d.*/', $date, $matches) === 1) {
487
			return $matches[1]; // year from ISO-formatted date yyyy-mm-dd
488
		} else {
489
			return null;
490
		}
491
	}
492
493
	private function fileIsCoverForAlbum($fileId, $albumId, $userId) {
494
		$album = $this->albumBusinessLayer->find($albumId, $userId);
495
		return ($album != null && $album->getCoverFileId() == $fileId);
496
	}
497
498
	/**
499
	 * Loop through the tracks of an album and set the first track containing embedded cover art
500
	 * as cover file for the album
501
	 * @param int $albumId
502
	 * @param int $userId
503
	 */
504
	private function findEmbeddedCoverForAlbum($albumId, $userId) {
505
		if ($this->userFolder != null) {
506
			$tracks = $this->trackBusinessLayer->findAllByAlbum($albumId, $userId);
507
			foreach ($tracks as $track) {
508
				$nodes = $this->userFolder->getById($track->getFileId());
509
				if(count($nodes) > 0) {
510
					// parse the first valid node and check if it contains embedded cover art
511
					$image = $this->parseEmbeddedCoverArt($nodes[0]);
512
					if ($image != null) {
513
						$this->albumBusinessLayer->setCover($track->getFileId(), $albumId);
514
						break;
515
					}
516
				}
517
			}
518
		}
519
	}
520
}
521