Passed
Push — master ( 07fdea...6c7f77 )
by Marcel
02:24
created

lib/Controller/ScannerController.php (3 issues)

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