ScannerController   F
last analyzed

Complexity

Total Complexity 116

Size/Duplication

Total Lines 785
Duplicated Lines 0 %

Importance

Changes 35
Bugs 7 Features 0
Metric Value
eloc 420
c 35
b 7
f 0
dl 0
loc 785
rs 2
wmc 116

23 Methods

Rating   Name   Duplication   Size   Complexity  
A checkNewTracks() 0 7 1
A processImageString() 0 10 5
A composeResponseMessage() 0 18 3
B analyze() 0 31 8
A normalizeInteger() 0 12 3
A checkFileChanged() 0 8 2
B getStreamObjects() 0 46 8
B getID3Value() 0 11 7
A getScannerTimestamp() 0 3 1
F scanForAudios() 0 123 19
A getImportTpl() 0 4 1
A truncateStrings() 0 3 2
A updateProgress() 0 12 2
A __construct() 0 29 1
B scanAudio() 0 83 8
A scanStream() 0 17 2
D getAlbumArt() 0 48 18
B getAudioObjects() 0 49 8
A timeForUpdate() 0 11 3
A setScannerVersion() 0 8 2
A scanCancelled() 0 5 2
A setScannerTimestamp() 0 3 1
B convertCyrillic() 0 28 9

How to fix   Complexity   

Complex Class

Complex classes like ScannerController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ScannerController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Audio Player
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the LICENSE.md file.
7
 *
8
 * @author Marcel Scherello <[email protected]>
9
 * @author Sebastian Doell <[email protected]>
10
 * @copyright 2016-2019 Marcel Scherello
11
 * @copyright 2015 Sebastian Doell
12
 */
13
14
namespace OCA\audioplayer\Controller;
15
16
use Doctrine\DBAL\DBALException;
0 ignored issues
show
Bug introduced by Rello
The type Doctrine\DBAL\DBALException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Exception;
18
use getID3;
19
use getid3_exception;
0 ignored issues
show
Bug introduced by Rello
The type getid3_exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use getid3_lib;
0 ignored issues
show
Bug introduced by Rello
The type getid3_lib was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use OC;
22
use OCP\AppFramework\Controller;
23
use OCP\AppFramework\Http\JSONResponse;
24
use OCP\AppFramework\Http\TemplateResponse;
25
use OCP\Files\NotFoundException;
26
use OCP\Image;
27
use OCP\PreconditionNotMetException;
28
use Symfony\Component\Console\Output\NullOutput;
29
use Symfony\Component\Console\Output\OutputInterface;
30
use OCP\IRequest;
31
use OCP\IConfig;
32
use OCP\IL10N;
33
use OCP\L10N\IFactory;
34
use OCP\IDbConnection;
35
use OCP\Files\IRootFolder;
36
use OCP\ILogger;
37
use OCP\IDateTimeZone;
38
use OCP\IEventSource;
39
40
/**
41
 * Controller class for main page.
42
 */
43
class ScannerController extends Controller
44
{
45
46
    private $userId;
47
    private $l10n;
48
    private $iDublicate = 0;
49
    private $iAlbumCount = 0;
50
    private $numOfSongs;
51
    private $db;
52
    private $configManager;
53
    private $occJob = false;
54
    private $noFseek = false;
55
    private $languageFactory;
56
    private $rootFolder;
57
    private $ID3Tags;
58
    private $cyrillic;
59
    private $logger;
60
    private $parentIdPrevious = 0;
61
    private $folderPicture = false;
62
    private $DBController;
63
    private $IDateTimeZone;
64
    private $SettingController;
65
    private $eventSource;
66
    private $lastUpdated;
67
68
    public function __construct(
69
        $appName,
70
        IRequest $request,
71
        $userId,
72
        IL10N $l10n,
73
        IDbConnection $db,
74
        IConfig $configManager,
75
        IFactory $languageFactory,
76
        IRootFolder $rootFolder,
77
        ILogger $logger,
78
        DbController $DBController,
79
        SettingController $SettingController,
80
        IDateTimeZone $IDateTimeZone
81
    )
82
    {
83
        parent::__construct($appName, $request);
84
        $this->appName = $appName;
85
        $this->userId = $userId;
86
        $this->l10n = $l10n;
87
        $this->db = $db;
88
        $this->configManager = $configManager;
89
        $this->languageFactory = $languageFactory;
90
        $this->rootFolder = $rootFolder;
91
        $this->logger = $logger;
92
        $this->DBController = $DBController;
93
        $this->SettingController = $SettingController;
94
        $this->IDateTimeZone = $IDateTimeZone;
95
        $this->eventSource = OC::$server->createEventSource();
96
        $this->lastUpdated = time();
97
    }
98
99
    /**
100
     * @NoAdminRequired
101
     *
102
     */
103
    public function getImportTpl()
104
    {
105
        $params = [];
106
        return new TemplateResponse('audioplayer', 'part.import', $params, '');
107
    }
108
109
    /**
110
     * @NoAdminRequired
111
     *
112
     * @param $userId
113
     * @param $output
114
     * @param $scanstop
115
     * @return bool|JSONResponse
116
     * @throws NotFoundException
117
     * @throws getid3_exception
118
     */
119
    public function scanForAudios($userId = null, $output = null, $scanstop = null)
120
    {
121
        set_time_limit(0);
122
        if (isset($scanstop)) {
123
            $this->DBController->setSessionValue('scanner_running', 'stopped', $this->userId);
124
            $params = ['status' => 'stopped'];
125
            return new JSONResponse($params);
126
        }
127
128
        // check if scanner is started from web or occ
129
        if ($userId !== null) {
130
            $this->occJob = true;
131
            $this->userId = $userId;
132
            $languageCode = $this->configManager->getUserValue($userId, 'core', 'lang');
133
            $this->l10n = $this->languageFactory->get('audioplayer', $languageCode);
134
        } else {
135
            $output = new NullOutput();
136
        }
137
138
        $output->writeln("Start processing of <info>audio files</info>");
139
140
        $counter = 0;
141
        $error_count = 0;
142
        $duplicate_tracks = '';
143
        $error_file = '';
144
        $this->cyrillic = $this->configManager->getUserValue($this->userId, $this->appName, 'cyrillic');
145
        $this->DBController->setSessionValue('scanner_running', 'active', $this->userId);
146
147
        $this->setScannerVersion();
148
149
        if (!class_exists('getid3_exception')) {
150
            require_once __DIR__ . '/../../3rdparty/getid3/getid3.php';
151
        }
152
        $getID3 = new getID3;
153
        $getID3->setOption(['encoding' => 'UTF-8',
154
            'option_tag_id3v1' => false,
155
            'option_tag_id3v2' => true,
156
            'option_tag_lyrics3' => false,
157
            'option_tag_apetag' => false,
158
            'option_tags_process' => true,
159
            'option_tags_html' => false
160
        ]);
161
162
        $audios = $this->getAudioObjects($output);
163
        $streams = $this->getStreamObjects($output);
164
165
        if ($this->cyrillic === 'checked') $output->writeln("Cyrillic processing activated", OutputInterface::VERBOSITY_VERBOSE);
166
        $output->writeln("Start processing of <info>audio files</info>", OutputInterface::VERBOSITY_VERBOSE);
167
168
        $commitThreshold = max(200, intdiv(count($audios), 10));
169
        $this->DBController->beginTransaction();
170
        try {
171
            foreach ($audios as &$audio) {
172
                if ($this->scanCancelled()) { break; }
173
174
                $counter++;
175
                try {
176
                    $scanResult = $this->scanAudio($audio, $getID3, $output);
177
                    if ($scanResult === 'error') {
178
                        $error_file .= $audio->getPath() . '<br />';
179
                        $error_count++;
180
                    } else if ($scanResult === 'duplicate') {
181
                        $duplicate_tracks .= $audio->getPath() . '<br />';
182
                        $this->iDublicate++;
183
                    }
184
                } catch (getid3_exception $e) {
185
                    $this->logger->error('getID3 error while building library: '. $e);
186
                    continue;
187
                }
188
189
                if ($this->timeForUpdate()) {
190
                    $this->updateProgress($counter, $audio->getPath(), $output);
191
                }
192
                if ($counter % $commitThreshold == 0) {
193
                    $this->DBController->commit();
194
                    $output->writeln("Status committed to database", OutputInterface::VERBOSITY_VERBOSE);
195
                    $this->DBController->beginTransaction();
196
                }
197
            }
198
199
            $output->writeln("Start processing of <info>stream files</info>", OutputInterface::VERBOSITY_VERBOSE);
200
            foreach ($streams as &$stream) {
201
                if ($this->scanCancelled()) { break; }
202
203
                $counter++;
204
                $scanResult = $this->scanStream($stream, $output);
205
                if ($scanResult === 'duplicate') {
206
                    $duplicate_tracks .= $stream->getPath() . '<br />';
207
                    $this->iDublicate++;
208
                }
209
210
                if ($this->timeForUpdate()) {
211
                    $this->updateProgress($counter, $stream->getPath(), $output);
212
                }
213
            }
214
            $this->setScannerTimestamp();
215
            $this->DBController->commit();
216
        } catch (DBALException $e) {
217
            $this->logger->error('DB error while building library: '. $e);
218
            $this->DBController->rollBack();
219
        } catch (Exception $e) {
220
            $this->logger->error('Error while building library: '. $e);
221
            $this->DBController->commit();
222
        }
223
224
        // different outputs when web or occ
225
        if (!$this->occJob) {
226
            $message = $this->composeResponseMessage($counter, $error_count, $duplicate_tracks, $error_file);
227
            $this->DBController->setSessionValue('scanner_running', '', $this->userId);
228
            $response = [
229
                'message' => $message
230
            ];
231
            $response = json_encode($response);
232
            $this->eventSource->send('done', $response);
233
            $this->eventSource->close();
234
            return new JSONResponse();
235
        } else {
236
            $output->writeln("Audios found: " . ($counter) . "");
237
            $output->writeln("Duplicates found: " . ($this->iDublicate) . "");
238
            $output->writeln("Written to library: " . ($counter - $this->iDublicate - $error_count) . "");
239
            $output->writeln("Albums found: " . ($this->iAlbumCount) . "");
240
            $output->writeln("Errors: " . ($error_count) . "");
241
            return true;
242
        }
243
    }
244
245
    /**
246
     * Check whether scan got cancelled by user
247
     * @return bool
248
     */
249
    private function scanCancelled() {
250
        //check if scan is still supposed to run, or if dialog was closed in web already
251
        if (!$this->occJob) {
252
            $scan_running = $this->DBController->getSessionValue('scanner_running');
253
            return ($scan_running !== 'active');
254
        }
255
    }
256
257
    /**
258
     * Process audio track and insert it into DB
259
     * @param object $audio audio object to scan
260
     * @param object $getID3 ID3 tag helper from getid3 library
261
     * @param OutputInterface|null $output
262
     * @return string
263
     */
264
    private function scanAudio($audio, $getID3, $output) {
265
        if ($this->checkFileChanged($audio)) {
266
            $this->DBController->deleteFromDB($audio->getId(), $this->userId);
267
        }
268
269
        $this->analyze($audio, $getID3, $output);
270
271
        # catch issue when getID3 does not bring a result in case of corrupt file or fpm-timeout
272
        if (!isset($this->ID3Tags['bitrate']) AND !isset($this->ID3Tags['playtime_string'])) {
273
            $this->logger->debug('Error with getID3. Does not seem to be a valid audio file: ' . $audio->getPath(), array('app' => 'audioplayer'));
274
            $output->writeln("       Error with getID3. Does not seem to be a valid audio file", OutputInterface::VERBOSITY_VERBOSE);
0 ignored issues
show
Bug introduced by Martin Matous
The method writeln() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

274
            $output->/** @scrutinizer ignore-call */ 
275
                     writeln("       Error with getID3. Does not seem to be a valid audio file", OutputInterface::VERBOSITY_VERBOSE);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
275
            return 'error';
276
        }
277
278
        $album = $this->getID3Value(array('album'));
279
        $genre = $this->getID3Value(array('genre'));
280
        $artist = $this->getID3Value(array('artist'));
281
        $name = $this->getID3Value(array('title'), $audio->getName());
282
        $trackNr = $this->getID3Value(array('track_number'), '');
283
        $composer = $this->getID3Value(array('composer'), '');
284
        $year = $this->getID3Value(array('year', 'creation_date', 'date'), 0);
285
        $subtitle = $this->getID3Value(array('subtitle', 'version'), '');
286
        $disc = $this->getID3Value(array('part_of_a_set', 'discnumber', 'partofset', 'disc_number'), 1);
287
        $isrc = $this->getID3Value(array('isrc'), '');
288
        $copyright = $this->getID3Value(array('copyright_message', 'copyright'), '');
289
290
        $iGenreId = $this->DBController->writeGenreToDB($this->userId, $genre);
291
        $iArtistId = $this->DBController->writeArtistToDB($this->userId, $artist);
292
293
        # write albumartist if available
294
        # if no albumartist, NO artist is stored on album level
295
        # in DBController loadArtistsToAlbum() takes over deriving the artists from the album tracks
296
        # MP3, FLAC & MP4 have different tags for albumartist
297
        $iAlbumArtistId = NULL;
298
        $album_artist = $this->getID3Value(array('band', 'album_artist', 'albumartist', 'album artist'), '0');
299
300
        if ($album_artist !== '0') {
301
            $iAlbumArtistId = $this->DBController->writeArtistToDB($this->userId, $album_artist);
302
        }
303
304
        $parentId = $audio->getParent()->getId();
305
        $return = $this->DBController->writeAlbumToDB($this->userId, $album, (int)$year, $iAlbumArtistId, $parentId);
306
        $iAlbumId = $return['id'];
307
        $this->iAlbumCount = $this->iAlbumCount + $return['albumcount'];
308
309
        $bitrate = 0;
310
        if (isset($this->ID3Tags['bitrate'])) {
311
            $bitrate = $this->ID3Tags['bitrate'];
312
        }
313
314
        $playTimeString = '';
315
        if (isset($this->ID3Tags['playtime_string'])) {
316
            $playTimeString = $this->ID3Tags['playtime_string'];
317
        }
318
319
        $this->getAlbumArt($audio, $iAlbumId, $parentId, $output);
320
321
        $aTrack = [
322
            'title' => $this->truncateStrings($name, '256'),
323
            'number' => $this->normalizeInteger($trackNr),
324
            'artist_id' => (int)$iArtistId,
325
            'album_id' => (int)$iAlbumId,
326
            'length' => $playTimeString,
327
            'file_id' => (int)$audio->getId(),
328
            'bitrate' => (int)$bitrate,
329
            'mimetype' => $audio->getMimetype(),
330
            'genre' => (int)$iGenreId,
331
            'year' => $this->truncateStrings($this->normalizeInteger($year), 4, ''),
332
            'disc' => $this->normalizeInteger($disc),
333
            'subtitle' => $this->truncateStrings($subtitle, '256'),
334
            'composer' => $this->truncateStrings($composer, '256'),
335
            'folder_id' => $parentId,
336
            'isrc' => $this->truncateStrings($isrc, '12'),
337
            'copyright' => $this->truncateStrings($copyright, '256'),
338
        ];
339
340
        $return = $this->DBController->writeTrackToDB($this->userId, $aTrack);
341
        if ($return['dublicate'] === 1) {
342
            $this->logger->debug('Duplicate file: ' . $audio->getPath(), array('app' => 'audioplayer'));
343
            $output->writeln("       This title is a duplicate and already existing", OutputInterface::VERBOSITY_VERBOSE);
344
            return 'duplicate';
345
        }
346
        return 'success';
347
    }
348
349
    /**
350
     * Process stream and insert it into DB
351
     * @param object $stream stream object to scan
352
     * @param object $getID3 ID3 tag helper from getid3 library
353
     * @param OutputInterface|null $output
354
     * @return string
355
     */
356
    private function scanStream($stream, $output) {
357
        $title = $this->truncateStrings($stream->getName(), '256');
358
        $aStream = [
359
            'title' => substr($title, 0, strrpos($title, ".")),
360
            'artist_id' => 0,
361
            'album_id' => 0,
362
            'file_id' => (int)$stream->getId(),
363
            'bitrate' => 0,
364
            'mimetype' => $stream->getMimetype(),
365
        ];
366
        $return = $this->DBController->writeStreamToDB($this->userId, $aStream);
367
        if ($return['dublicate'] === 1) {
368
            $this->logger->debug('Duplicate file: ' . $stream->getPath(), array('app' => 'audioplayer'));
369
            $output->writeln("       This title is a duplicate and already existing", OutputInterface::VERBOSITY_VERBOSE);
370
            return 'duplicate';
371
        }
372
        return 'success';
373
    }
374
375
    /**
376
     * Summarize scan results in a message
377
     * @param integer $stream number of processed files
378
     * @param integer $error_count number of invalid files
379
     * @param string $duplicate_tracks list of duplicates
380
     * @param string $duplicate_tracks list of invalid files
381
     * @return string
382
     */
383
    private function composeResponseMessage($counter,
384
                                            $error_count,
385
                                            $duplicate_tracks,
386
                                            $error_file) {
387
        $message = (string)$this->l10n->t('Scanning finished!') . '<br />';
388
        $message .= (string)$this->l10n->t('Audios found: ') . $counter . '<br />';
389
        $message .= (string)$this->l10n->t('Written to library: ') . ($counter - $this->iDublicate - $error_count) . '<br />';
390
        $message .= (string)$this->l10n->t('Albums found: ') . $this->iAlbumCount . '<br />';
391
        if ($error_count > 0) {
392
            $message .= '<br /><b>' . (string)$this->l10n->t('Errors: ') . $error_count . '<br />';
393
            $message .= (string)$this->l10n->t('If rescan does not solve this problem the files are broken') . '</b>';
394
            $message .= '<br />' . $error_file . '<br />';
395
        }
396
        if ($this->iDublicate > 0) {
397
            $message .= '<br /><b>' . (string)$this->l10n->t('Duplicates found: ') . ($this->iDublicate) . '</b>';
398
            $message .= '<br />' . $duplicate_tracks . '<br />';
399
        }
400
        return $message;
401
    }
402
403
    /**
404
     * Give feedback to user via appropriate output
405
     * @param integer $filesProcessed
406
     * @param string $currentFile
407
     * @param OutputInterface|null $output
408
     */
409
    private function updateProgress($filesProcessed, $currentFile, OutputInterface $output = null)
410
    {
411
        if (!$this->occJob) {
412
            $response = [
413
                'filesProcessed' => $filesProcessed,
414
                'filesTotal' => $this->numOfSongs,
415
                'currentFile' => $currentFile
416
            ];
417
            $response = json_encode($response);
418
            $this->eventSource->send('progress', $response);
419
        } else {
420
            $output->writeln("   " . $currentFile . "</info>", OutputInterface::VERBOSITY_VERY_VERBOSE);
421
        }
422
    }
423
424
    /**
425
     * Prevent flood over the wire
426
     * @return bool
427
     */
428
    private function timeForUpdate()
429
    {
430
        if ($this->occJob) {
431
            return true;
432
        }
433
        $now = time();
434
        if ($now - $this->lastUpdated >= 1) {
435
            $this->lastUpdated = $now;
436
            return true;
437
        }
438
        return false;
439
    }
440
441
    /**
442
     * if the scanner is started on an empty library, the current app version is stored
443
     *
444
     */
445
    private function setScannerVersion()
446
    {
447
        $stmt = $this->db->prepare('SELECT COUNT(`id`) AS `TRACKCOUNT`  FROM `*PREFIX*audioplayer_tracks` WHERE `user_id` = ? ');
448
        $stmt->execute(array($this->userId));
449
        $row = $stmt->fetch();
450
        if ((int)$row['TRACKCOUNT'] === 0) {
451
            $app_version = $this->configManager->getAppValue($this->appName, 'installed_version', '0.0.0');
452
            $this->configManager->setUserValue($this->userId, $this->appName, 'scanner_version', $app_version);
453
        }
454
    }
455
456
    /**
457
     * Add track to db if not exist
458
     *
459
     * @param OutputInterface $output
460
     * @return array
461
     * @throws NotFoundException
462
     */
463
    private function getAudioObjects(OutputInterface $output = null)
464
    {
465
        $audioPath = $this->configManager->getUserValue($this->userId, $this->appName, 'path');
466
        $userView = $this->rootFolder->getUserFolder($this->userId);
467
468
        if ($audioPath !== null && $audioPath !== '/' && $audioPath !== '') {
469
            $userView = $userView->get($audioPath);
470
        }
471
472
        $audios_mp3 = $userView->searchByMime('audio/mpeg');
473
        $audios_m4a = $userView->searchByMime('audio/mp4');
474
        $audios_ogg = $userView->searchByMime('audio/ogg');
475
        $audios_wav = $userView->searchByMime('audio/wav');
476
        $audios_flac = $userView->searchByMime('audio/flac');
477
        $audios = array_merge($audios_mp3, $audios_m4a, $audios_ogg, $audios_wav, $audios_flac);
478
479
        $output->writeln("Scanned Folder: " . $userView->getPath(), OutputInterface::VERBOSITY_VERBOSE);
480
        $output->writeln("<info>Total audio files:</info> " . count($audios), OutputInterface::VERBOSITY_VERBOSE);
481
        $output->writeln("Checking audio files to be skipped", OutputInterface::VERBOSITY_VERBOSE);
482
483
        // get all fileids which are in an excluded folder
484
        $stmt = $this->db->prepare('SELECT `fileid` from `*PREFIX*filecache` WHERE `parent` IN (SELECT `parent` FROM `*PREFIX*filecache` WHERE `name` = ? OR `name` = ? ORDER BY `fileid` ASC)');
485
        $stmt->execute(array('.noAudio', '.noaudio'));
486
        $results = $stmt->fetchAll();
487
        $resultExclude = array_column($results, 'fileid');
488
489
        // get all fileids which are already in the Audio Player Database
490
        $stmt = $this->db->prepare('SELECT `file_id` FROM `*PREFIX*audioplayer_tracks` WHERE `user_id` = ? ');
491
        $stmt->execute(array($this->userId));
492
        $results = $stmt->fetchAll();
493
        $resultExisting = array_column($results, 'file_id');
494
495
        foreach ($audios as $key => &$audio) {
496
            $current_id = $audio->getID();
497
            if (in_array($current_id, $resultExclude)) {
498
                $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => excluded", OutputInterface::VERBOSITY_VERY_VERBOSE);
499
                unset($audios[$key]);
500
            } elseif (in_array($current_id, $resultExisting)) {
501
                if ($this->checkFileChanged($audio)) {
502
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => indexed title changed => reindex", OutputInterface::VERBOSITY_VERY_VERBOSE);
503
                } else {
504
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => already indexed", OutputInterface::VERBOSITY_VERY_VERBOSE);
505
                    unset($audios[$key]);
506
                }
507
            }
508
        }
509
        $this->numOfSongs = count($audios);
510
        $output->writeln("Final audio files to be processed: " . $this->numOfSongs, OutputInterface::VERBOSITY_VERBOSE);
511
        return $audios;
512
    }
513
514
    /**
515
     * check changed timestamps
516
     *
517
     * @param object $audio
518
     * @return bool
519
     */
520
    private function checkFileChanged($audio)
521
    {
522
        $modTime = $audio->getMTime();
523
        $scannerTime = $this->getScannerTimestamp();
524
        if ($modTime >= $scannerTime - 300) {
525
            return true;
526
        } else {
527
            return false;
528
        }
529
    }
530
531
    /**
532
     * check the timestamp of the last scan to derive changed files
533
     *
534
     */
535
    private function getScannerTimestamp()
536
    {
537
        return $this->configManager->getUserValue($this->userId, $this->appName, 'scanner_timestamp', 300);
538
    }
539
540
    /**
541
     * Add track to db if not exist
542
     *
543
     * @param OutputInterface $output
544
     * @return array
545
     * @throws NotFoundException
546
     */
547
    private function getStreamObjects(OutputInterface $output = null)
548
    {
549
        $audios_clean = array();
550
        $audioPath = $this->configManager->getUserValue($this->userId, $this->appName, 'path');
551
        $userView = $this->rootFolder->getUserFolder($this->userId);
552
553
        if ($audioPath !== null && $audioPath !== '/' && $audioPath !== '') {
554
            $userView = $userView->get($audioPath);
555
        }
556
557
        $audios_mpegurl = $userView->searchByMime('audio/mpegurl');
558
        $audios_scpls = $userView->searchByMime('audio/x-scpls');
559
        $audios_xspf = $userView->searchByMime('application/xspf+xml');
560
        $audios = array_merge($audios_mpegurl, $audios_scpls, $audios_xspf);
561
        $output->writeln("<info>Total stream files:</info> " . count($audios), OutputInterface::VERBOSITY_VERBOSE);
562
        $output->writeln("Checking stream files to be skipped", OutputInterface::VERBOSITY_VERBOSE);
563
564
        // get all fileids which are in an excluded folder
565
        $stmt = $this->db->prepare('SELECT `fileid` from `*PREFIX*filecache` WHERE `parent` IN (SELECT `parent` FROM `*PREFIX*filecache` WHERE `name` = ? OR `name` = ? ORDER BY `fileid` ASC)');
566
        $stmt->execute(array('.noAudio', '.noaudio'));
567
        $results = $stmt->fetchAll();
568
        $resultExclude = array_column($results, 'fileid');
569
570
        // get all fileids which are already in the Audio Player Database
571
        $stmt = $this->db->prepare('SELECT `file_id` FROM `*PREFIX*audioplayer_streams` WHERE `user_id` = ? ');
572
        $stmt->execute(array($this->userId));
573
        $results = $stmt->fetchAll();
574
        $resultExisting = array_column($results, 'file_id');
575
576
        foreach ($audios as $key => &$audio) {
577
            $current_id = $audio->getID();
578
            if (in_array($current_id, $resultExclude)) {
579
                $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => excluded", OutputInterface::VERBOSITY_VERY_VERBOSE);
580
                unset($audios[$key]);
581
            } elseif (in_array($current_id, $resultExisting)) {
582
                if ($this->checkFileChanged($audio)) {
583
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => indexed file changed => reindex", OutputInterface::VERBOSITY_VERY_VERBOSE);
584
                } else {
585
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => already indexed", OutputInterface::VERBOSITY_VERY_VERBOSE);
586
                    unset($audios[$key]);
587
                }
588
            }
589
        }
590
        $this->numOfSongs = $this->numOfSongs + count($audios);
591
        $output->writeln("Final stream files to be processed: " . count($audios_clean), OutputInterface::VERBOSITY_VERBOSE);
592
        return $audios;
593
    }
594
595
    /**
596
     * Analyze ID3 Tags
597
     * if fseek is not possible, libsmbclient-php is not installed or an external storage is used which does not support this.
598
     * then fallback to slow extraction via tmpfile
599
     *
600
     * @param $audio object
601
     * @param $getID3 object
602
     * @param OutputInterface $output
603
     */
604
    private function analyze($audio, $getID3, OutputInterface $output = null)
605
    {
606
        $this->ID3Tags = array();
607
        $ThisFileInfo = array();
608
        if ($audio->getMimetype() === 'audio/mpegurl' or $audio->getMimetype() === 'audio/x-scpls' or $audio->getMimetype() === 'application/xspf+xml') {
609
            $ThisFileInfo['comments']['genre'][0] = 'Stream';
610
            $ThisFileInfo['comments']['artist'][0] = 'Stream';
611
            $ThisFileInfo['comments']['album'][0] = 'Stream';
612
            $ThisFileInfo['bitrate'] = 0;
613
            $ThisFileInfo['playtime_string'] = 0;
614
        } else {
615
            $handle = $audio->fopen('rb');
616
            if (@fseek($handle, -24, SEEK_END) === 0) {
617
                $ThisFileInfo = $getID3->analyze($audio->getPath(), $audio->getSize(), '', $handle);
618
            } else {
619
                if (!$this->noFseek) {
620
                    $output->writeln("Attention: Only slow indexing due to server config. See Audio Player wiki on GitHub for details.", OutputInterface::VERBOSITY_VERBOSE);
621
                    $this->logger->debug('Attention: Only slow indexing due to server config. See Audio Player wiki on GitHub for details.', array('app' => 'audioplayer'));
622
                    $this->noFseek = true;
623
                }
624
                $fileName = $audio->getStorage()->getLocalFile($audio->getInternalPath());
625
                $ThisFileInfo = $getID3->analyze($fileName);
626
627
                if (!$audio->getStorage()->isLocal($audio->getInternalPath())) {
628
                    unlink($fileName);
629
                }
630
            }
631
            if ($this->cyrillic === 'checked') $ThisFileInfo = $this->convertCyrillic($ThisFileInfo);
632
            getid3_lib::CopyTagsToComments($ThisFileInfo);
633
        }
634
        $this->ID3Tags = $ThisFileInfo;
635
    }
636
637
    /**
638
     * Concert cyrillic characters
639
     *
640
     * @param array $ThisFileInfo
641
     * @return array
642
     */
643
    private function convertCyrillic($ThisFileInfo)
644
    {
645
        //$this->logger->debug('cyrillic handling activated', array('app' => 'audioplayer'));
646
        // Check, if this tag was win1251 before the incorrect "8859->utf" convertion by the getid3 lib
647
        foreach (array('id3v1', 'id3v2') as $ttype) {
648
            $ruTag = 0;
649
            if (isset($ThisFileInfo['tags'][$ttype])) {
650
                // Check, if this tag was win1251 before the incorrect "8859->utf" convertion by the getid3 lib
651
                foreach (array('album', 'artist', 'title', 'band', 'genre') as $tkey) {
652
                    if (isset($ThisFileInfo['tags'][$ttype][$tkey])) {
653
                        if (preg_match('#[\\xA8\\B8\\x80-\\xFF]{4,}#', iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $ThisFileInfo['tags'][$ttype][$tkey][0]))) {
654
                            $ruTag = 1;
655
                            break;
656
                        }
657
                    }
658
                }
659
                // Now make a correct conversion
660
                if ($ruTag === 1) {
661
                    foreach (array('album', 'artist', 'title', 'band', 'genre') as $tkey) {
662
                        if (isset($ThisFileInfo['tags'][$ttype][$tkey])) {
663
                            $ThisFileInfo['tags'][$ttype][$tkey][0] = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $ThisFileInfo['tags'][$ttype][$tkey][0]);
664
                            $ThisFileInfo['tags'][$ttype][$tkey][0] = iconv('Windows-1251', 'UTF-8', $ThisFileInfo['tags'][$ttype][$tkey][0]);
665
                        }
666
                    }
667
                }
668
            }
669
        }
670
        return $ThisFileInfo;
671
    }
672
673
    /**
674
     * Get specific ID3 tags from array
675
     *
676
     * @param string[] $ID3Value
677
     * @param string $defaultValue
678
     * @return string
679
     */
680
    private function getID3Value($ID3Value, $defaultValue = null)
681
    {
682
        $c = count($ID3Value);
683
        //	\OCP\Util::writeLog('audioplayer', 'album: '.$this->ID3Tags['comments']['album'][0], \OCP\Util::DEBUG);
684
        for ($i = 0; $i < $c; $i++) {
685
            if (isset($this->ID3Tags['comments'][$ID3Value[$i]][0]) and rawurlencode($this->ID3Tags['comments'][$ID3Value[$i]][0]) !== '%FF%FE') {
686
                return $this->ID3Tags['comments'][$ID3Value[$i]][0];
687
            } elseif ($i === $c - 1 AND $defaultValue !== null) {
688
                return $defaultValue;
689
            } elseif ($i === $c - 1) {
690
                return (string)$this->l10n->t('Unknown');
691
            }
692
        }
693
    }
694
695
    /**
696
     * extract cover art from folder or from audio file
697
     * folder/cover.jpg/png
698
     *
699
     * @param object $audio
700
     * @param integer $iAlbumId
701
     * @param integer $parentId
702
     * @param OutputInterface|null $output
703
     * @return boolean|null
704
     */
705
    private function getAlbumArt($audio, $iAlbumId, $parentId, OutputInterface $output = null)
706
    {
707
        if ($parentId === $this->parentIdPrevious) {
708
            if ($this->folderPicture) {
709
                $output->writeln("     Reusing previous folder image", OutputInterface::VERBOSITY_VERY_VERBOSE);
710
                $this->processImageString($iAlbumId, $this->folderPicture->getContent());
711
            } elseif (isset($this->ID3Tags['comments']['picture'][0]['data'])) {
712
                $data = $this->ID3Tags['comments']['picture'][0]['data'];
713
                $this->processImageString($iAlbumId, $data);
714
            }
715
        } else {
716
            $this->folderPicture = false;
717
            if ($audio->getParent()->nodeExists('cover.jpg')) {
718
                $this->folderPicture = $audio->getParent()->get('cover.jpg');
719
            } elseif ($audio->getParent()->nodeExists('Cover.jpg')) {
720
                $this->folderPicture = $audio->getParent()->get('Cover.jpg');
721
            } elseif ($audio->getParent()->nodeExists('cover.png')) {
722
                $this->folderPicture = $audio->getParent()->get('cover.png');
723
            } elseif ($audio->getParent()->nodeExists('Cover.png')) {
724
                $this->folderPicture = $audio->getParent()->get('Cover.png');
725
            } elseif ($audio->getParent()->nodeExists('folder.jpg')) {
726
                $this->folderPicture = $audio->getParent()->get('folder.jpg');
727
            } elseif ($audio->getParent()->nodeExists('Folder.jpg')) {
728
                $this->folderPicture = $audio->getParent()->get('Folder.jpg');
729
            } elseif ($audio->getParent()->nodeExists('folder.png')) {
730
                $this->folderPicture = $audio->getParent()->get('folder.png');
731
            } elseif ($audio->getParent()->nodeExists('Folder.png')) {
732
                $this->folderPicture = $audio->getParent()->get('Folder.png');
733
            } elseif ($audio->getParent()->nodeExists('front.jpg')) {
734
                $this->folderPicture = $audio->getParent()->get('front.jpg');
735
            } elseif ($audio->getParent()->nodeExists('Front.jpg')) {
736
                $this->folderPicture = $audio->getParent()->get('Front.jpg');
737
            } elseif ($audio->getParent()->nodeExists('front.png')) {
738
                $this->folderPicture = $audio->getParent()->get('front.png');
739
            } elseif ($audio->getParent()->nodeExists('Front.png')) {
740
                $this->folderPicture = $audio->getParent()->get('Front.png');
741
            }
742
743
            if ($this->folderPicture) {
744
                $output->writeln("     Alternative album art: " . $this->folderPicture->getInternalPath(), OutputInterface::VERBOSITY_VERY_VERBOSE);
745
                $this->processImageString($iAlbumId, $this->folderPicture->getContent());
746
            } elseif (isset($this->ID3Tags['comments']['picture'])) {
747
                $data = $this->ID3Tags['comments']['picture'][0]['data'];
748
                $this->processImageString($iAlbumId, $data);
749
            }
750
            $this->parentIdPrevious = $parentId;
751
        }
752
        return true;
753
    }
754
755
    /**
756
     * create image string from rawdata and store as album cover
757
     *
758
     * @param integer $iAlbumId
759
     * @param $data
760
     * @return boolean
761
     */
762
    private function processImageString($iAlbumId, $data)
763
    {
764
        $image = new Image();
765
        if ($image->loadFromdata($data)) {
766
            if (($image->width() <= 250 && $image->height() <= 250) || $image->centerCrop(250)) {
767
                $imgString = $image->__toString();
768
                $this->DBController->writeCoverToAlbum($this->userId, $iAlbumId, $imgString);
769
            }
770
        }
771
        return true;
772
    }
773
774
    /**
775
     * truncates fiels do DB-field size
776
     *
777
     * @param $string
778
     * @param $length
779
     * @param $dots
780
     * @return string
781
     */
782
    private function truncateStrings($string, $length, $dots = "...")
783
    {
784
        return (strlen($string) > $length) ? mb_strcut($string, 0, $length - strlen($dots)) . $dots : $string;
785
    }
786
787
    /**
788
     * validate unsigned int values
789
     *
790
     * @param string $value
791
     * @return int value
792
     */
793
    private function normalizeInteger($value)
794
    {
795
        // convert format '1/10' to '1' and '-1' to null
796
        $tmp = explode('/', $value);
797
        $tmp = explode('-', $tmp[0]);
798
        $value = $tmp[0];
799
        if (is_numeric($value) && ((int)$value) > 0) {
800
            $value = (int)$value;
801
        } else {
802
            $value = 0;
803
        }
804
        return $value;
805
    }
806
807
    /**
808
     * set the timestamp of the last scan to derive changed files
809
     *
810
     */
811
    private function setScannerTimestamp()
812
    {
813
        $this->configManager->setUserValue($this->userId, $this->appName, 'scanner_timestamp', time());
814
    }
815
816
    /**
817
     * @NoAdminRequired
818
     *
819
     * @throws NotFoundException
820
     */
821
    public function checkNewTracks()
822
    {
823
        // get only the relevant audio files
824
        $output = new NullOutput();
825
        $this->getAudioObjects($output);
826
        $this->getStreamObjects($output);
827
        return ($this->numOfSongs !== 0);
828
    }
829
}
830