Passed
Push — master ( d9d99b...742760 )
by Marcel
04:55 queued 02:45
created

ScannerController::scanAudio()   B

Complexity

Conditions 8
Paths 34

Size

Total Lines 83
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 58
c 1
b 0
f 0
nc 34
nop 3
dl 0
loc 83
rs 7.6719

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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