Failed Conditions
Push — master ( 65f0dd...191693 )
by Sébastien
02:09
created

VideoInfoReader::getCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @see       https://github.com/soluble-io/soluble-mediatools for the canonical repository
7
 *
8
 * @copyright Copyright (c) 2018-2019 Sébastien Vanvelthem. (https://github.com/belgattitude)
9
 * @license   https://github.com/soluble-io/soluble-mediatools/blob/master/LICENSE.md MIT
10
 */
11
12
namespace Soluble\MediaTools\Video;
13
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\LogLevel;
16
use Psr\Log\NullLogger;
17
use Psr\SimpleCache\CacheInterface;
18
use Soluble\MediaTools\Common\Assert\PathAssertionsTrait;
19
use Soluble\MediaTools\Common\Cache\NullCache;
20
use Soluble\MediaTools\Common\Exception\FileEmptyException;
21
use Soluble\MediaTools\Common\Exception\FileNotFoundException;
22
use Soluble\MediaTools\Common\Exception\FileNotReadableException;
23
use Soluble\MediaTools\Common\Exception\JsonParseException;
24
use Soluble\MediaTools\Common\Process\ProcessFactory;
25
use Soluble\MediaTools\Common\Process\ProcessParamsInterface;
26
use Soluble\MediaTools\Video\Config\FFProbeConfigInterface;
27
use Soluble\MediaTools\Video\Exception\InfoProcessReaderExceptionInterface;
28
use Soluble\MediaTools\Video\Exception\InfoReaderExceptionInterface;
29
use Soluble\MediaTools\Video\Exception\InvalidFFProbeJsonException;
30
use Soluble\MediaTools\Video\Exception\MissingFFProbeBinaryException;
31
use Soluble\MediaTools\Video\Exception\MissingInputFileException;
32
use Soluble\MediaTools\Video\Exception\ProcessFailedException;
33
use Soluble\MediaTools\Video\Exception\RuntimeReaderException;
34
use Symfony\Component\Process\Exception as SPException;
35
use Symfony\Component\Process\Process;
36
37
class VideoInfoReader implements VideoInfoReaderInterface
38
{
39
    use PathAssertionsTrait;
40
41
    /** @var FFProbeConfigInterface */
42
    private $ffprobeConfig;
43
44
    /** @var LoggerInterface */
45
    private $logger;
46
47
    /** @var CacheInterface */
48
    private $cache;
49
50 31
    public function __construct(FFProbeConfigInterface $ffProbeConfig, ?LoggerInterface $logger = null, ?CacheInterface $cache = null)
51
    {
52 31
        $this->ffprobeConfig = $ffProbeConfig;
53 31
        $this->logger        = $logger ?? new NullLogger();
54 31
        $this->cache         = $cache ?? new NullCache();
55 31
    }
56
57
    /**
58
     * Return ready-to-run symfony process object that you can use
59
     * to `run()` or `start()` programmatically. Useful if you want to make
60
     * things your way...
61
     *
62
     * @see https://symfony.com/doc/current/components/process.html
63
     */
64 13
    public function getSymfonyProcess(string $inputFile, ?ProcessParamsInterface $processParams = null): Process
65
    {
66
        $ffprobeCmd = [
67 13
            $this->ffprobeConfig->getBinary(),
68
            //'-v',
69
            //'quiet',
70 13
            '-print_format',
71 13
            'json',
72 13
            '-show_format',
73 13
            '-show_streams',
74 13
            '-i',
75 13
            $inputFile,
76
        ];
77
78 13
        $pp = $processParams ?? $this->ffprobeConfig->getProcessParams();
79
80 13
        return (new ProcessFactory($ffprobeCmd, $pp))();
81
    }
82
83
    /**
84
     * @throws InfoReaderExceptionInterface
85
     * @throws InfoProcessReaderExceptionInterface
86
     * @throws ProcessFailedException
87
     * @throws InvalidFFProbeJsonException
88
     * @throws MissingInputFileException
89
     * @throws MissingFFProbeBinaryException
90
     * @throws RuntimeReaderException
91
     */
92 15
    public function getInfo(string $file, ?CacheInterface $cache = null): VideoInfo
93
    {
94 15
        $cache = $cache ?? $this->cache;
95
96
        // global try/catch to call logger
97
98
        try {
99
            try {
100 15
                $this->ensureFileReadable($file, true);
101 2
            } catch (FileNotFoundException | FileNotReadableException | FileEmptyException $e) {
102 2
                throw new MissingInputFileException($e->getMessage());
103
            }
104
105 13
            $process = $this->getSymfonyProcess($file);
106
107
            try {
108 13
                $key       = $this->getCacheKey($process, $file);
109 13
                $videoInfo = $this->loadVideoInfoFromCacheKey($file, $cache, $key);
110 13
                if ($videoInfo === null) {
111
                    // cache failure or corrupted, let's fallback to running ffprobe
112 13
                    $cache->set($key, null);
113 13
                    $process->mustRun();
114 11
                    $output    = $process->getOutput();
115 11
                    $videoInfo = VideoInfo::createFromFFProbeJson($file, $output, $this->logger);
116 11
                    $cache->set($key, $output);
117
                }
118
119
                // Exception mapping
120 2
            } catch (JsonParseException $e) {
121
                throw new InvalidFFProbeJsonException($e->getMessage());
122 2
            } catch (SPException\ProcessFailedException $e) {
123 2
                $process = $e->getProcess();
124 2
                if ($process->getExitCode() === 127 ||
125 2
                    mb_strpos(mb_strtolower($process->getExitCodeText()), 'command not found') !== false) {
126 1
                    throw new MissingFFProbeBinaryException($process, $e);
127
                }
128 1
                throw new ProcessFailedException($process, $e);
129
            } catch (SPException\ProcessTimedOutException | SPException\ProcessSignaledException $e) {
130
                throw new ProcessFailedException($e->getProcess(), $e);
131
            } catch (SPException\RuntimeException $e) {
132 11
                throw new RuntimeReaderException($e->getMessage());
133
            }
134 4
        } catch (\Throwable $e) {
135 4
            $this->logException($e, $file);
136 4
            throw $e;
137
        }
138
139 11
        return $videoInfo;
140
    }
141
142 13
    private function loadVideoInfoFromCacheKey(string $file, CacheInterface $cache, string $cacheKey): ?VideoInfo
143
    {
144 13
        $output = $cache->get($cacheKey, null);
145 13
        if ($output !== null && $output !== '') {
146
            try {
147 3
                return VideoInfo::createFromFFProbeJson($file, $output, $this->logger);
148 1
            } catch (\Throwable $e) {
149 1
                return null;
150
            }
151
        }
152
153 13
        return null;
154
    }
155
156 4
    private function logException(\Throwable $e, string $file): void
157
    {
158 4
        $exceptionNs = explode('\\', get_class($e));
159 4
        $this->logger->log(
160 4
            ($e instanceof MissingInputFileException) ? LogLevel::WARNING : LogLevel::ERROR,
161 4
            sprintf(
162 4
                'VideoInfoReader %s: \'%s\'. (%s)',
163 4
                $exceptionNs[count($exceptionNs) - 1],
164 4
                $file,
165 4
                $e->getMessage()
166
            )
167
        );
168 4
    }
169
170 13
    private function getCacheKey(Process $process, string $file): string
171
    {
172 13
        return sha1(sprintf('%s | %s | %s | %s', $process->getCommandLine(), $file, (string) filesize($file), (string) filemtime($file)));
173
    }
174
}
175