ScannerController::truncateStrings()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

282
            $output->/** @scrutinizer ignore-call */ 
283
                     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...
283
            return 'error';
284
        }
285
286
        $album = $this->getID3Value(array('album'));
287
        $genre = $this->getID3Value(array('genre'));
288
        $artist = $this->getID3Value(array('artist'));
289
        $name = $this->getID3Value(array('title'), $audio->getName());
290
        $trackNr = $this->getID3Value(array('track_number'), '');
291
        $composer = $this->getID3Value(array('composer'), '');
292
        $year = $this->getID3Value(array('year', 'creation_date', 'date'), 0);
293
        $subtitle = $this->getID3Value(array('subtitle', 'version'), '');
294
        $disc = $this->getID3Value(array('part_of_a_set', 'discnumber', 'partofset', 'disc_number'), 1);
295
        $isrc = $this->getID3Value(array('isrc'), '');
296
        $copyright = $this->getID3Value(array('copyright_message', 'copyright'), '');
297
298
        $iGenreId = $this->DBController->writeGenreToDB($this->userId, $genre);
299
        $iArtistId = $this->DBController->writeArtistToDB($this->userId, $artist);
300
301
        # write albumartist if available
302
        # if no albumartist, NO artist is stored on album level
303
        # in DBController loadArtistsToAlbum() takes over deriving the artists from the album tracks
304
        # MP3, FLAC & MP4 have different tags for albumartist
305
        $iAlbumArtistId = NULL;
306
        $album_artist = $this->getID3Value(array('band', 'album_artist', 'albumartist', 'album artist'), '0');
307
308
        if ($album_artist !== '0') {
309
            $iAlbumArtistId = $this->DBController->writeArtistToDB($this->userId, $album_artist);
310
        }
311
312
        $parentId = $audio->getParent()->getId();
313
        $return = $this->DBController->writeAlbumToDB($this->userId, $album, (int)$year, $iAlbumArtistId, $parentId);
314
        $iAlbumId = $return['id'];
315
        $this->iAlbumCount = $this->iAlbumCount + $return['albumcount'];
316
317
        $bitrate = 0;
318
        if (isset($this->ID3Tags['bitrate'])) {
319
            $bitrate = $this->ID3Tags['bitrate'];
320
        }
321
322
        $playTimeString = '';
323
        if (isset($this->ID3Tags['playtime_string'])) {
324
            $playTimeString = $this->ID3Tags['playtime_string'];
325
        }
326
327
        $this->getAlbumArt($audio, $iAlbumId, $parentId, $output);
328
329
        $aTrack = [
330
            'title' => $this->truncateStrings($name, '256'),
331
            'number' => $this->normalizeInteger($trackNr),
332
            'artist_id' => (int)$iArtistId,
333
            'album_id' => (int)$iAlbumId,
334
            'length' => $playTimeString,
335
            'file_id' => (int)$audio->getId(),
336
            'bitrate' => (int)$bitrate,
337
            'mimetype' => $audio->getMimetype(),
338
            'genre' => (int)$iGenreId,
339
            'year' => $this->truncateStrings($this->normalizeInteger($year), 4, ''),
340
            'disc' => $this->normalizeInteger($disc),
341
            'subtitle' => $this->truncateStrings($subtitle, '256'),
342
            'composer' => $this->truncateStrings($composer, '256'),
343
            'folder_id' => $parentId,
344
            'isrc' => $this->truncateStrings($isrc, '12'),
345
            'copyright' => $this->truncateStrings($copyright, '256'),
346
        ];
347
348
        $return = $this->DBController->writeTrackToDB($this->userId, $aTrack);
349
        if ($return['dublicate'] === 1) {
350
            $this->logger->debug('Duplicate file: ' . $audio->getPath(), array('app' => 'audioplayer'));
351
            $output->writeln("       This title is a duplicate and already existing", OutputInterface::VERBOSITY_VERBOSE);
352
            return 'duplicate';
353
        }
354
        return 'success';
355
    }
356
357
    /**
358
     * Process stream and insert it into DB
359
     * @param object $stream stream object to scan
360
     * @param OutputInterface|null $output
361
     * @return string
362
     */
363
    private function scanStream($stream, $output) {
364
        $title = $this->truncateStrings($stream->getName(), '256');
365
        $aStream = [
366
            'title' => substr($title, 0, strrpos($title, ".")),
367
            'artist_id' => 0,
368
            'album_id' => 0,
369
            'file_id' => (int)$stream->getId(),
370
            'bitrate' => 0,
371
            'mimetype' => $stream->getMimetype(),
372
        ];
373
        $return = $this->DBController->writeStreamToDB($this->userId, $aStream);
374
        if ($return['dublicate'] === 1) {
375
            $this->logger->debug('Duplicate file: ' . $stream->getPath(), array('app' => 'audioplayer'));
376
            $output->writeln("       This title is a duplicate and already existing", OutputInterface::VERBOSITY_VERBOSE);
377
            return 'duplicate';
378
        }
379
        return 'success';
380
    }
381
382
    /**
383
     * Summarize scan results in a message
384
     * @param $counter number of tracks
385
     * @param integer $error_count number of invalid files
386
     * @param string $duplicate_tracks list of invalid files
387
     * @param $error_file
388
     * @return string
389
     */
390
    private function composeResponseMessage($counter,
391
                                            $error_count,
392
                                            $duplicate_tracks,
393
                                            $error_file) {
394
        $message = (string)$this->l10n->t('Scanning finished!') . '<br />';
395
        $message .= (string)$this->l10n->t('Audios found:') . ' ' . $counter . '<br />';
396
        $message .= (string)$this->l10n->t('Written to library:') . ' ' . ($counter - $this->iDublicate - $error_count) . '<br />';
397
        $message .= (string)$this->l10n->t('Albums found:') . ' ' . $this->iAlbumCount . '<br />';
398
        if ($error_count > 0) {
399
            $message .= '<br /><b>' . (string)$this->l10n->t('Errors:') . ' ' . $error_count . '<br />';
400
            $message .= (string)$this->l10n->t('If rescan does not solve this problem the files are broken') . '</b>';
401
            $message .= '<br />' . $error_file . '<br />';
402
        }
403
        if ($this->iDublicate > 0) {
404
            $message .= '<br /><b>' . (string)$this->l10n->t('Duplicates found:') . ' ' . ($this->iDublicate) . '</b>';
405
            $message .= '<br />' . $duplicate_tracks . '<br />';
406
        }
407
        return $message;
408
    }
409
410
    /**
411
     * Give feedback to user via appropriate output
412
     * @param integer $filesProcessed
413
     * @param string $currentFile
414
     * @param OutputInterface|null $output
415
     */
416
    private function updateProgress($filesProcessed, $currentFile, OutputInterface $output = null)
417
    {
418
        if (!$this->occJob) {
419
            $response = [
420
                'filesProcessed' => $filesProcessed,
421
                'filesTotal' => $this->numOfSongs,
422
                'currentFile' => $currentFile
423
            ];
424
            $response = json_encode($response);
425
            $this->eventSource->send('progress', $response);
426
        } else {
427
            $output->writeln("   " . $currentFile . "</info>", OutputInterface::VERBOSITY_VERY_VERBOSE);
428
        }
429
    }
430
431
    /**
432
     * Prevent flood over the wire
433
     * @return bool
434
     */
435
    private function timeForUpdate()
436
    {
437
        if ($this->occJob) {
438
            return true;
439
        }
440
        $now = time();
441
        if ($now - $this->lastUpdated >= 1) {
442
            $this->lastUpdated = $now;
443
            return true;
444
        }
445
        return false;
446
    }
447
448
    /**
449
     * if the scanner is started on an empty library, the current app version is stored
450
     *
451
     */
452
    private function setScannerVersion()
453
    {
454
        $stmt = $this->db->prepare('SELECT COUNT(`id`) AS `TRACKCOUNT`  FROM `*PREFIX*audioplayer_tracks` WHERE `user_id` = ? ');
455
        $stmt->execute(array($this->userId));
456
        $row = $stmt->fetch();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\DB\IPreparedStatement::fetch() has been deprecated: 21.0.0 use \OCP\DB\IResult::fetch on the \OCP\DB\IResult returned by \OCP\IDBConnection::prepare ( Ignorable by Annotation )

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

456
        $row = /** @scrutinizer ignore-deprecated */ $stmt->fetch();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
457
        if ((int)$row['TRACKCOUNT'] === 0) {
458
            $app_version = $this->configManager->getAppValue($this->appName, 'installed_version', '0.0.0');
459
            $this->configManager->setUserValue($this->userId, $this->appName, 'scanner_version', $app_version);
460
        }
461
    }
462
463
    /**
464
     * Add track to db if not exist
465
     *
466
     * @param OutputInterface $output
467
     * @return array
468
     * @throws NotFoundException
469
     * @throws \OCP\Files\InvalidPathException
470
     */
471
    private function getAudioObjects(OutputInterface $output = null)
472
    {
473
        $audioPath = $this->configManager->getUserValue($this->userId, $this->appName, 'path');
474
        $userView = $this->rootFolder->getUserFolder($this->userId);
475
476
        if ($audioPath !== null && $audioPath !== '/' && $audioPath !== '') {
477
            try {
478
                $userView = $userView->get($audioPath);
479
            } catch (InvalidPathException $e) {
480
                $output->writeln("!Error: Selected scan folder is not existing");
481
                return;
482
            } catch (NotFoundException $e) {
483
                $output->writeln("!Error: Selected scan folder is not existing");
484
                return;
485
            }
486
        }
487
488
        $audios_mp3 = $userView->searchByMime('audio/mpeg');
489
        $audios_m4a = $userView->searchByMime('audio/mp4');
490
        $audios_ogg = $userView->searchByMime('audio/ogg');
491
        $audios_wav = $userView->searchByMime('audio/wav');
492
        $audios_flac = $userView->searchByMime('audio/flac');
493
        $audios_aif = $userView->searchByMime('audio/x-aiff');
494
        $audios_aac = $userView->searchByMime('audio/aac');
495
        $audios = array_merge($audios_mp3, $audios_m4a, $audios_ogg, $audios_wav, $audios_flac, $audios_aif, $audios_aac);
496
497
        $output->writeln("Scanned Folder: " . $userView->getPath(), OutputInterface::VERBOSITY_VERBOSE);
498
        $output->writeln("<info>Total audio files:</info> " . count($audios), OutputInterface::VERBOSITY_VERBOSE);
499
        $output->writeln("Checking audio files to be skipped", OutputInterface::VERBOSITY_VERBOSE);
500
501
        // get all fileids which are in an excluded folder
502
        $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)');
503
        $stmt->execute(array('.noAudio', '.noaudio'));
504
        $results = $stmt->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\DB\IPreparedStatement::fetchAll() has been deprecated: 21.0.0 use \OCP\DB\IResult::fetchAll on the \OCP\DB\IResult returned by \OCP\IDBConnection::prepare ( Ignorable by Annotation )

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

504
        $results = /** @scrutinizer ignore-deprecated */ $stmt->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
505
        $resultExclude = array_column($results, 'fileid');
506
507
        // get all fileids which are already in the Audio Player Database
508
        $stmt = $this->db->prepare('SELECT `file_id` FROM `*PREFIX*audioplayer_tracks` WHERE `user_id` = ? ');
509
        $stmt->execute(array($this->userId));
510
        $results = $stmt->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\DB\IPreparedStatement::fetchAll() has been deprecated: 21.0.0 use \OCP\DB\IResult::fetchAll on the \OCP\DB\IResult returned by \OCP\IDBConnection::prepare ( Ignorable by Annotation )

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

510
        $results = /** @scrutinizer ignore-deprecated */ $stmt->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
511
        $resultExisting = array_column($results, 'file_id');
512
513
        foreach ($audios as $key => &$audio) {
514
            $current_id = $audio->getID();
515
            if (in_array($current_id, $resultExclude)) {
516
                $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => excluded", OutputInterface::VERBOSITY_VERY_VERBOSE);
517
                unset($audios[$key]);
518
            } elseif (in_array($current_id, $resultExisting)) {
519
                if ($this->checkFileChanged($audio)) {
520
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => indexed title changed => reindex", OutputInterface::VERBOSITY_VERY_VERBOSE);
521
                } else {
522
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => already indexed", OutputInterface::VERBOSITY_VERY_VERBOSE);
523
                    unset($audios[$key]);
524
                }
525
            }
526
        }
527
        $this->numOfSongs = count($audios);
528
        $output->writeln("Final audio files to be processed: " . $this->numOfSongs, OutputInterface::VERBOSITY_VERBOSE);
529
        return $audios;
530
    }
531
532
    /**
533
     * check changed timestamps
534
     *
535
     * @param object $audio
536
     * @return bool
537
     */
538
    private function checkFileChanged($audio)
539
    {
540
        $modTime = $audio->getMTime();
541
        $scannerTime = $this->getScannerTimestamp();
542
        if ($modTime >= $scannerTime - 300) {
543
            return true;
544
        } else {
545
            return false;
546
        }
547
    }
548
549
    /**
550
     * check the timestamp of the last scan to derive changed files
551
     *
552
     */
553
    private function getScannerTimestamp()
554
    {
555
        return $this->configManager->getUserValue($this->userId, $this->appName, 'scanner_timestamp', 300);
556
    }
557
558
    /**
559
     * Add track to db if not exist
560
     *
561
     * @param OutputInterface $output
562
     * @return array
563
     * @throws NotFoundException
564
     */
565
    private function getStreamObjects(OutputInterface $output = null)
566
    {
567
        $audios_clean = array();
568
        $audioPath = $this->configManager->getUserValue($this->userId, $this->appName, 'path');
569
        $userView = $this->rootFolder->getUserFolder($this->userId);
570
571
        if ($audioPath !== null && $audioPath !== '/' && $audioPath !== '') {
572
            try {
573
                $userView = $userView->get($audioPath);
574
            } catch (InvalidPathException $e) {
575
                $output->writeln("!Error: Selected scan folder is not existing");
576
                return;
577
            } catch (NotFoundException $e) {
578
                $output->writeln("!Error: Selected scan folder is not existing");
579
                return;
580
            }
581
        }
582
583
        $audios_mpegurl = $userView->searchByMime('audio/mpegurl');
584
        $audios_scpls = $userView->searchByMime('audio/x-scpls');
585
        $audios_xspf = $userView->searchByMime('application/xspf+xml');
586
        $audios = array_merge($audios_mpegurl, $audios_scpls, $audios_xspf);
587
        $output->writeln("<info>Total stream files:</info> " . count($audios), OutputInterface::VERBOSITY_VERBOSE);
588
        $output->writeln("Checking stream files to be skipped", OutputInterface::VERBOSITY_VERBOSE);
589
590
        // get all fileids which are in an excluded folder
591
        $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)');
592
        $stmt->execute(array('.noAudio', '.noaudio'));
593
        $results = $stmt->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\DB\IPreparedStatement::fetchAll() has been deprecated: 21.0.0 use \OCP\DB\IResult::fetchAll on the \OCP\DB\IResult returned by \OCP\IDBConnection::prepare ( Ignorable by Annotation )

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

593
        $results = /** @scrutinizer ignore-deprecated */ $stmt->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
594
        $resultExclude = array_column($results, 'fileid');
595
596
        // get all fileids which are already in the Audio Player Database
597
        $stmt = $this->db->prepare('SELECT `file_id` FROM `*PREFIX*audioplayer_streams` WHERE `user_id` = ? ');
598
        $stmt->execute(array($this->userId));
599
        $results = $stmt->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function OCP\DB\IPreparedStatement::fetchAll() has been deprecated: 21.0.0 use \OCP\DB\IResult::fetchAll on the \OCP\DB\IResult returned by \OCP\IDBConnection::prepare ( Ignorable by Annotation )

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

599
        $results = /** @scrutinizer ignore-deprecated */ $stmt->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
600
        $resultExisting = array_column($results, 'file_id');
601
602
        foreach ($audios as $key => &$audio) {
603
            $current_id = $audio->getID();
604
            if (in_array($current_id, $resultExclude)) {
605
                $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => excluded", OutputInterface::VERBOSITY_VERY_VERBOSE);
606
                unset($audios[$key]);
607
            } elseif (in_array($current_id, $resultExisting)) {
608
                if ($this->checkFileChanged($audio)) {
609
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => indexed file changed => reindex", OutputInterface::VERBOSITY_VERY_VERBOSE);
610
                } else {
611
                    $output->writeln("   " . $current_id . " - " . $audio->getPath() . "  => already indexed", OutputInterface::VERBOSITY_VERY_VERBOSE);
612
                    unset($audios[$key]);
613
                }
614
            }
615
        }
616
        $this->numOfSongs = $this->numOfSongs + count($audios);
617
        $output->writeln("Final stream files to be processed: " . count($audios_clean), OutputInterface::VERBOSITY_VERBOSE);
618
        return $audios;
619
    }
620
621
    /**
622
     * Analyze ID3 Tags
623
     * if fseek is not possible, libsmbclient-php is not installed or an external storage is used which does not support this.
624
     * then fallback to slow extraction via tmpfile
625
     *
626
     * @param $audio object
627
     * @param $getID3 object
628
     * @param OutputInterface $output
629
     */
630
    private function analyze($audio, $getID3, OutputInterface $output = null)
631
    {
632
        $this->ID3Tags = array();
633
        $ThisFileInfo = array();
634
        if ($audio->getMimetype() === 'audio/mpegurl' or $audio->getMimetype() === 'audio/x-scpls' or $audio->getMimetype() === 'application/xspf+xml') {
635
            $ThisFileInfo['comments']['genre'][0] = 'Stream';
636
            $ThisFileInfo['comments']['artist'][0] = 'Stream';
637
            $ThisFileInfo['comments']['album'][0] = 'Stream';
638
            $ThisFileInfo['bitrate'] = 0;
639
            $ThisFileInfo['playtime_string'] = 0;
640
        } else {
641
642
            $availability =  $audio->getStorage()->getAvailability();
643
            if (!$availability['available']) {
644
                $output->writeln("Some external storage is not available", OutputInterface::VERBOSITY_VERBOSE);
645
                $this->logger->debug('Some external storage is not available', array('app' => 'audioplayer'));
646
            } else {
647
                $handle = $audio->fopen('rb');
648
                if (is_resource($handle) && @fseek($handle, -24, SEEK_END) === 0) {
649
                    $ThisFileInfo = $getID3->analyze($audio->getPath(), $audio->getSize(), '', $handle);
650
                } else {
651
                    if (!$this->noFseek) {
652
                        $output->writeln("Attention: Only slow indexing due to server config. See Audio Player wiki on GitHub for details.", OutputInterface::VERBOSITY_VERBOSE);
653
                        $this->logger->debug('Attention: Only slow indexing due to server config. See Audio Player wiki on GitHub for details.', array('app' => 'audioplayer'));
654
                        $this->noFseek = true;
655
                    }
656
                    $fileName = $audio->getStorage()->getLocalFile($audio->getInternalPath());
657
                    $ThisFileInfo = $getID3->analyze($fileName);
658
659
                    if (!$audio->getStorage()->isLocal($audio->getInternalPath())) {
660
                        unlink($fileName);
661
                    }
662
                }
663
                if ($this->cyrillic === 'checked') $ThisFileInfo = $this->convertCyrillic($ThisFileInfo);
664
                getid3_lib::CopyTagsToComments($ThisFileInfo);
665
            }
666
        }
667
        $this->ID3Tags = $ThisFileInfo;
668
    }
669
670
    /**
671
     * Concert cyrillic characters
672
     *
673
     * @param array $ThisFileInfo
674
     * @return array
675
     */
676
    private function convertCyrillic($ThisFileInfo)
677
    {
678
        //$this->logger->debug('cyrillic handling activated', array('app' => 'audioplayer'));
679
        // Check, if this tag was win1251 before the incorrect "8859->utf" convertion by the getid3 lib
680
        foreach (array('id3v1', 'id3v2') as $ttype) {
681
            $ruTag = 0;
682
            if (isset($ThisFileInfo['tags'][$ttype])) {
683
                // Check, if this tag was win1251 before the incorrect "8859->utf" convertion by the getid3 lib
684
                foreach (array('album', 'artist', 'title', 'band', 'genre') as $tkey) {
685
                    if (isset($ThisFileInfo['tags'][$ttype][$tkey])) {
686
                        if (preg_match('#[\\xA8\\B8\\x80-\\xFF]{4,}#', iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $ThisFileInfo['tags'][$ttype][$tkey][0]))) {
687
                            $ruTag = 1;
688
                            break;
689
                        }
690
                    }
691
                }
692
                // Now make a correct conversion
693
                if ($ruTag === 1) {
694
                    foreach (array('album', 'artist', 'title', 'band', 'genre') as $tkey) {
695
                        if (isset($ThisFileInfo['tags'][$ttype][$tkey])) {
696
                            $ThisFileInfo['tags'][$ttype][$tkey][0] = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $ThisFileInfo['tags'][$ttype][$tkey][0]);
697
                            $ThisFileInfo['tags'][$ttype][$tkey][0] = iconv('Windows-1251', 'UTF-8', $ThisFileInfo['tags'][$ttype][$tkey][0]);
698
                        }
699
                    }
700
                }
701
            }
702
        }
703
        return $ThisFileInfo;
704
    }
705
706
    /**
707
     * Get specific ID3 tags from array
708
     *
709
     * @param string[] $ID3Value
710
     * @param string $defaultValue
711
     * @return string
712
     */
713
    private function getID3Value($ID3Value, $defaultValue = null)
714
    {
715
        $c = count($ID3Value);
716
        //	\OCP\Util::writeLog('audioplayer', 'album: '.$this->ID3Tags['comments']['album'][0], \OCP\Util::DEBUG);
717
        for ($i = 0; $i < $c; $i++) {
718
            if (isset($this->ID3Tags['comments'][$ID3Value[$i]][0]) and rawurlencode($this->ID3Tags['comments'][$ID3Value[$i]][0]) !== '%FF%FE') {
719
                return preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $this->ID3Tags['comments'][$ID3Value[$i]][0]);
720
            } elseif ($i === $c - 1 AND $defaultValue !== null) {
721
                return $defaultValue;
722
            } elseif ($i === $c - 1) {
723
                return (string)$this->l10n->t('Unknown');
724
            }
725
        }
726
    }
727
728
    /**
729
     * extract cover art from folder or from audio file
730
     * folder/cover.jpg/png
731
     *
732
     * @param object $audio
733
     * @param integer $iAlbumId
734
     * @param integer $parentId
735
     * @param OutputInterface|null $output
736
     * @return boolean|null
737
     */
738
    private function getAlbumArt($audio, $iAlbumId, $parentId, OutputInterface $output = null)
739
    {
740
        if ($parentId === $this->parentIdPrevious) {
741
            if ($this->folderPicture) {
742
                $output->writeln("     Reusing previous folder image", OutputInterface::VERBOSITY_VERY_VERBOSE);
743
                $this->processImageString($iAlbumId, $this->folderPicture->getContent());
744
            } elseif (isset($this->ID3Tags['comments']['picture'][0]['data'])) {
745
                $data = $this->ID3Tags['comments']['picture'][0]['data'];
746
                $this->processImageString($iAlbumId, $data);
747
            }
748
        } else {
749
            $this->folderPicture = false;
750
            if ($audio->getParent()->nodeExists('cover.jpg')) {
751
                $this->folderPicture = $audio->getParent()->get('cover.jpg');
752
            } elseif ($audio->getParent()->nodeExists('Cover.jpg')) {
753
                $this->folderPicture = $audio->getParent()->get('Cover.jpg');
754
            } elseif ($audio->getParent()->nodeExists('cover.jpeg')) {
755
                $this->folderPicture = $audio->getParent()->get('cover.jpeg');
756
            } elseif ($audio->getParent()->nodeExists('Cover.jpeg')) {
757
                $this->folderPicture = $audio->getParent()->get('Cover.jpeg');
758
            } elseif ($audio->getParent()->nodeExists('cover.png')) {
759
                $this->folderPicture = $audio->getParent()->get('cover.png');
760
            } elseif ($audio->getParent()->nodeExists('Cover.png')) {
761
                $this->folderPicture = $audio->getParent()->get('Cover.png');
762
            } elseif ($audio->getParent()->nodeExists('folder.jpg')) {
763
                $this->folderPicture = $audio->getParent()->get('folder.jpg');
764
            } elseif ($audio->getParent()->nodeExists('Folder.jpg')) {
765
                $this->folderPicture = $audio->getParent()->get('Folder.jpg');
766
            } elseif ($audio->getParent()->nodeExists('folder.jpeg')) {
767
                $this->folderPicture = $audio->getParent()->get('folder.jpeg');
768
            } elseif ($audio->getParent()->nodeExists('Folder.jpeg')) {
769
                $this->folderPicture = $audio->getParent()->get('Folder.jpeg');
770
            } elseif ($audio->getParent()->nodeExists('folder.png')) {
771
                $this->folderPicture = $audio->getParent()->get('folder.png');
772
            } elseif ($audio->getParent()->nodeExists('Folder.png')) {
773
                $this->folderPicture = $audio->getParent()->get('Folder.png');
774
            } elseif ($audio->getParent()->nodeExists('front.jpg')) {
775
                $this->folderPicture = $audio->getParent()->get('front.jpg');
776
            } elseif ($audio->getParent()->nodeExists('Front.jpg')) {
777
                $this->folderPicture = $audio->getParent()->get('Front.jpg');
778
            } elseif ($audio->getParent()->nodeExists('front.jpeg')) {
779
                $this->folderPicture = $audio->getParent()->get('front.jpeg');
780
            } elseif ($audio->getParent()->nodeExists('Front.jpeg')) {
781
                $this->folderPicture = $audio->getParent()->get('Front.jpeg');
782
            } elseif ($audio->getParent()->nodeExists('front.png')) {
783
                $this->folderPicture = $audio->getParent()->get('front.png');
784
            } elseif ($audio->getParent()->nodeExists('Front.png')) {
785
                $this->folderPicture = $audio->getParent()->get('Front.png');
786
            }
787
788
            if ($this->folderPicture) {
789
                $output->writeln("     Alternative album art: " . $this->folderPicture->getInternalPath(), OutputInterface::VERBOSITY_VERY_VERBOSE);
790
                $this->processImageString($iAlbumId, $this->folderPicture->getContent());
791
            } elseif (isset($this->ID3Tags['comments']['picture'])) {
792
                $data = $this->ID3Tags['comments']['picture'][0]['data'];
793
                $this->processImageString($iAlbumId, $data);
794
            }
795
            $this->parentIdPrevious = $parentId;
796
        }
797
        return true;
798
    }
799
800
    /**
801
     * create image string from rawdata and store as album cover
802
     *
803
     * @param integer $iAlbumId
804
     * @param $data
805
     * @return boolean
806
     */
807
    private function processImageString($iAlbumId, $data)
808
    {
809
        $image = new Image();
810
        if ($image->loadFromdata($data)) {
811
            if (($image->width() <= 250 && $image->height() <= 250) || $image->centerCrop(250)) {
812
                $imgString = $image->__toString();
813
                $this->DBController->writeCoverToAlbum($this->userId, $iAlbumId, $imgString);
814
            }
815
        }
816
        return true;
817
    }
818
819
    /**
820
     * truncates fiels do DB-field size
821
     *
822
     * @param $string
823
     * @param $length
824
     * @param $dots
825
     * @return string
826
     */
827
    private function truncateStrings($string, $length, $dots = "...")
828
    {
829
        return (strlen($string) > $length) ? mb_strcut($string, 0, $length - strlen($dots)) . $dots : $string;
830
    }
831
832
    /**
833
     * validate unsigned int values
834
     *
835
     * @param string $value
836
     * @return int value
837
     */
838
    private function normalizeInteger($value)
839
    {
840
        // convert format '1/10' to '1' and '-1' to null
841
        $tmp = explode('/', $value);
842
        $tmp = explode('-', $tmp[0]);
843
        $value = $tmp[0];
844
        if (is_numeric($value) && ((int)$value) > 0) {
845
            $value = (int)$value;
846
        } else {
847
            $value = 0;
848
        }
849
        return $value;
850
    }
851
852
    /**
853
     * set the timestamp of the last scan to derive changed files
854
     *
855
     */
856
    private function setScannerTimestamp()
857
    {
858
        $this->configManager->setUserValue($this->userId, $this->appName, 'scanner_timestamp', time());
859
    }
860
861
    /**
862
     * @NoAdminRequired
863
     *
864
     * @throws NotFoundException
865
     */
866
    public function checkNewTracks()
867
    {
868
        // get only the relevant audio files
869
        $output = new NullOutput();
870
        $this->getAudioObjects($output);
871
        $this->getStreamObjects($output);
872
        return ($this->numOfSongs !== 0);
873
    }
874
}
875