Passed
Push — master ( b92b41...c5601d )
by Marcel
18:04 queued 13:04
created

ScannerController::convertCyrillic()   B

Complexity

Conditions 9
Paths 18

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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