Passed
Push — master ( c7c8df...03432c )
by Darko
12:27
created

MediaProcessingService   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 69
eloc 190
c 2
b 1
f 0
dl 0
loc 305
rs 2.88

7 Methods

Rating   Name   Duplication   Size   Complexity  
A addVideoMediaInfo() 0 15 3
F createVideoSample() 0 70 17
A saveJPGSample() 0 14 2
B createSampleImage() 0 37 9
C getVideoTime() 0 63 15
F addAudioInfoAndSample() 0 82 22
A __construct() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like MediaProcessingService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MediaProcessingService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Services;
4
5
use App\Models\Category;
6
use App\Models\Release;
7
use Blacklight\Categorize;
8
use Blacklight\ElasticSearchSiteSearch;
9
use Blacklight\ManticoreSearch;
10
use Blacklight\ReleaseExtra;
11
use Blacklight\ReleaseImage;
12
use FFMpeg\Coordinate\Dimension;
13
use FFMpeg\Coordinate\TimeCode;
14
use FFMpeg\FFMpeg;
15
use FFMpeg\FFProbe;
16
use FFMpeg\Filters\Video\ResizeFilter;
17
use FFMpeg\Format\Audio\Vorbis;
18
use FFMpeg\Format\Video\Ogg;
19
use Illuminate\Support\Facades\File;
20
use Illuminate\Support\Facades\Log;
21
use Mhor\MediaInfo\MediaInfo;
22
23
class MediaProcessingService
24
{
25
    public function __construct(
26
        private readonly FFMpeg $ffmpeg,
27
        private readonly FFProbe $ffprobe,
28
        private readonly MediaInfo $mediaInfo,
29
        private readonly ReleaseImage $releaseImage,
30
        private readonly ReleaseExtra $releaseExtra,
31
        private readonly ManticoreSearch $manticore,
32
        private readonly ElasticSearchSiteSearch $elasticsearch,
33
        private readonly Categorize $categorize,
34
    ) {}
35
36
    public function getVideoTime(string $videoLocation): string
37
    {
38
        $time = null;
39
        try {
40
            if ($this->ffprobe->isValid($videoLocation)) {
41
                $val = $this->ffprobe->format($videoLocation)->get('duration');
42
                if (is_string($val) || is_numeric($val)) {
43
                    $time = (string) $val;
44
                }
45
            }
46
        } catch (\Throwable $e) {
47
            if (config('app.debug') === true) {
48
                Log::debug($e->getMessage());
49
            }
50
        }
51
52
        if (empty($time)) {
53
            return '';
54
        }
55
56
        // Case 1: matches ffmpeg log style `time=.. bitrate=` (optionally with hours)
57
        if (preg_match('/time=(\d{1,2}:\d{1,2}:)?(\d{1,2})\.(\d{1,2})\s*bitrate=/i', $time, $numbers)) {
58
            if ($numbers[3] > 0) {
59
                $numbers[3]--;
60
            } elseif (! empty($numbers[1])) {
61
                $numbers[2]--;
62
                $numbers[3] = '99';
63
            }
64
65
            return '00:00:'.str_pad((string) $numbers[2], 2, '0', STR_PAD_LEFT).'.'.str_pad((string) $numbers[3], 2, '0', STR_PAD_LEFT);
66
        }
67
68
        // Case 1b: matches `time=MM:SS.xx` (without trailing bitrate)
69
        if (preg_match('/time=(\d{2}):(\d{2})\.(\d{2})/i', $time, $m)) {
70
            $sec = (int) $m[2];
71
            $hund = (int) $m[3];
72
            if ($hund > 0) {
73
                $hund--;
74
            } else {
75
                if ($sec > 0) {
76
                    $sec--;
77
                    $hund = 99;
78
                }
79
            }
80
81
            return '00:00:'.str_pad((string) $sec, 2, '0', STR_PAD_LEFT).'.'.str_pad((string) $hund, 2, '0', STR_PAD_LEFT);
82
        }
83
84
        // Case 2: numeric seconds
85
        if (is_numeric($time)) {
86
            $seconds = (float) $time;
87
            if ($seconds <= 0) {
88
                return '';
89
            }
90
            $seconds = max(0.0, $seconds - 0.01);
91
            $whole = (int) floor($seconds);
92
            $hund = (int) round(($seconds - $whole) * 100);
93
            $hund = min($hund, 99);
94
95
            return '00:00:'.str_pad((string) $whole, 2, '0', STR_PAD_LEFT).'.'.str_pad((string) $hund, 2, '0', STR_PAD_LEFT);
96
        }
97
98
        return '';
99
    }
100
101
    public function saveJPGSample(string $guid, string $fileLocation): bool
102
    {
103
        $saved = $this->releaseImage->saveImage(
104
            $guid.'_thumb',
105
            $fileLocation,
106
            $this->releaseImage->jpgSavePath,
107
            650,
108
            650
109
        ) === 1;
110
        if ($saved) {
111
            Release::query()->where('guid', $guid)->update(['jpgstatus' => 1]);
112
        }
113
114
        return $saved;
115
    }
116
117
    public function createSampleImage(string $guid, string $fileLocation, string $tmpPath, bool $enabled, int $width = 800, int $height = 600): bool
118
    {
119
        if (! $enabled) {
120
            return false;
121
        }
122
        if (! File::isFile($fileLocation)) {
123
            return false;
124
        }
125
        $fileName = ($tmpPath.'zzzz'.random_int(5, 12).random_int(5, 12).'.jpg');
126
        $time = $this->getVideoTime($fileLocation);
127
        if ($this->ffprobe->isValid($fileLocation)) {
128
            try {
129
                $this->ffmpeg->open($fileLocation)
130
                    ->frame(TimeCode::fromString($time === '' ? '00:00:03:00' : $time))
0 ignored issues
show
Bug introduced by
The method frame() does not exist on FFMpeg\Media\Audio. It seems like you code against a sub-type of FFMpeg\Media\Audio such as FFMpeg\Media\Video. ( Ignorable by Annotation )

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

130
                    ->/** @scrutinizer ignore-call */ frame(TimeCode::fromString($time === '' ? '00:00:03:00' : $time))
Loading history...
131
                    ->save($fileName);
132
            } catch (\Throwable $e) {
133
                if (config('app.debug') === true) {
134
                    Log::error($e->getMessage());
135
                }
136
            }
137
        }
138
        if (! File::isFile($fileName)) {
139
            return false;
140
        }
141
        $saved = $this->releaseImage->saveImage(
142
            $guid.'_thumb',
143
            $fileName,
144
            $this->releaseImage->imgSavePath,
145
            $width,
146
            $height
147
        );
148
        File::delete($fileName);
149
        if ($saved === 1) {
150
            return true;
151
        }
152
153
        return false;
154
    }
155
156
    public function createVideoSample(string $guid, string $fileLocation, string $tmpPath, bool $enabled, int $durationSeconds): bool
157
    {
158
        if (! $enabled) {
159
            return false;
160
        }
161
        if (! File::isFile($fileLocation)) {
162
            return false;
163
        }
164
        $fileName = ($tmpPath.'zzzz'.$guid.'.ogv');
165
        $newMethod = false;
166
        if ($durationSeconds < 60) {
167
            $time = $this->getVideoTime($fileLocation);
168
            if ($time !== '' && preg_match('/(\d{2}).(\d{2})/', $time, $numbers)) {
169
                $newMethod = true;
170
                if ($numbers[1] <= $durationSeconds) {
171
                    $lowestLength = '00:00:00.00';
172
                } else {
173
                    $lowestLength = ($numbers[1] - $durationSeconds);
174
                    $end = '.'.$numbers[2];
175
                    $lowestLength = match (strlen((string) $lowestLength)) {
176
                        1 => ('00:00:0'.$lowestLength.$end),
177
                        2 => ('00:00:'.$lowestLength.$end),
178
                        default => '00:00:60.00',
179
                    };
180
                }
181
                if ($this->ffprobe->isValid($fileLocation)) {
182
                    try {
183
                        $video = $this->ffmpeg->open($fileLocation);
184
                        $videoSample = $video->clip(TimeCode::fromString($lowestLength), TimeCode::fromSeconds($durationSeconds));
0 ignored issues
show
Bug introduced by
The method clip() does not exist on FFMpeg\Media\Audio. It seems like you code against a sub-type of FFMpeg\Media\Audio such as FFMpeg\Media\Video. ( Ignorable by Annotation )

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

184
                        /** @scrutinizer ignore-call */ 
185
                        $videoSample = $video->clip(TimeCode::fromString($lowestLength), TimeCode::fromSeconds($durationSeconds));
Loading history...
185
                        $format = new Ogg;
186
                        $format->setAudioCodec('libvorbis');
187
                        $videoSample->filters()->resize(new Dimension(320, -1), ResizeFilter::RESIZEMODE_SCALE_HEIGHT);
188
                        $videoSample->save($format, $fileName);
189
                    } catch (\Throwable $e) {
190
                        if (config('app.debug') === true) {
191
                            Log::error($e->getMessage());
192
                        }
193
                    }
194
                }
195
            }
196
        }
197
        if (! $newMethod && $this->ffprobe->isValid($fileLocation)) {
198
            try {
199
                $video = $this->ffmpeg->open($fileLocation);
200
                $videoSample = $video->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($durationSeconds));
201
                $format = new Ogg;
202
                $format->setAudioCodec('libvorbis');
203
                $videoSample->filters()->resize(new Dimension(320, -1), ResizeFilter::RESIZEMODE_SCALE_HEIGHT);
204
                $videoSample->save($format, $fileName);
205
            } catch (\Throwable $e) {
206
                if (config('app.debug') === true) {
207
                    Log::error($e->getMessage());
208
                }
209
            }
210
        }
211
        if (! File::isFile($fileName)) {
212
            return false;
213
        }
214
        $newFile = ($this->releaseImage->vidSavePath.$guid.'.ogv');
215
        if (! @File::move($fileName, $newFile)) {
216
            $copied = @File::copy($fileName, $newFile);
217
            File::delete($fileName);
218
            if (! $copied) {
219
                return false;
220
            }
221
        }
222
        @chmod($newFile, 0764);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

222
        /** @scrutinizer ignore-unhandled */ @chmod($newFile, 0764);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
223
        Release::query()->where('guid', $guid)->update(['videostatus' => 1]);
224
225
        return true;
226
    }
227
228
    public function addVideoMediaInfo(int $releaseId, string $fileLocation): bool
229
    {
230
        if (! File::isFile($fileLocation)) {
231
            return false;
232
        }
233
        try {
234
            $xmlArray = $this->mediaInfo->getInfo($fileLocation, true);
235
            \App\Models\MediaInfo::addData($releaseId, $xmlArray);
236
            $this->releaseExtra->addFromXml($releaseId, $xmlArray);
237
238
            return true;
239
        } catch (\Throwable $e) {
240
            Log::debug($e->getMessage());
241
242
            return false;
243
        }
244
    }
245
246
    public function addAudioInfoAndSample(
247
        Release $release,
248
        string $fileLocation,
249
        string $fileExtension,
250
        bool $processAudioInfo,
251
        bool $processAudioSample,
252
        string $audioSavePath
253
    ): array {
254
        // Mirror original behavior: defaults depend on flags, not file presence
255
        $retVal = ! $processAudioInfo ? true : false;
256
        $audVal = ! $processAudioSample ? true : false;
257
258
        // Only proceed with file-dependent operations if file exists
259
        if (File::isFile($fileLocation)) {
260
            if ($processAudioInfo) {
261
                try {
262
                    $xmlArray = $this->mediaInfo->getInfo($fileLocation, false);
263
                    if ($xmlArray !== null) {
264
                        foreach ($xmlArray->getAudios() as $track) {
265
                            if ($track->get('album') !== null && $track->get('performer') !== null) {
266
                                if ((int) $release->predb_id === 0 && config('nntmux.rename_music_mediainfo')) {
0 ignored issues
show
Bug introduced by
The property predb_id does not exist on App\Models\Release. Did you mean predb?
Loading history...
267
                                    $ext = strtoupper($fileExtension);
268
                                    if (! empty($track->get('recorded_date')) && preg_match('/(?:19|20)\d\d/', $track->get('recorded_date')->getFullname(), $Year)) {
269
                                        $newName = $track->get('performer')->getFullName().' - '.$track->get('album')->getFullName().' ('.$Year[0].') '.$ext;
270
                                    } else {
271
                                        $newName = $track->get('performer')->getFullName().' - '.$track->get('album')->getFullName().' '.$ext;
272
                                    }
273
                                    if ($ext === 'MP3') {
274
                                        $newCat = Category::MUSIC_MP3;
275
                                    } elseif ($ext === 'FLAC') {
276
                                        $newCat = Category::MUSIC_LOSSLESS;
277
                                    } else {
278
                                        $newCat = $this->categorize->determineCategory($release->groups_id, $newName, $release->fromname);
0 ignored issues
show
Bug introduced by
The property groups_id does not exist on App\Models\Release. Did you mean group?
Loading history...
Bug introduced by
The property fromname does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
279
                                    }
280
                                    $newTitle = escapeString(substr($newName, 0, 255));
281
                                    Release::whereId($release->id)->update([
282
                                        'searchname' => $newTitle,
283
                                        'categories_id' => $newCat['categories_id'] ?? $release->categories_id,
0 ignored issues
show
Bug introduced by
The property categories_id does not exist on App\Models\Release. Did you mean category_ids?
Loading history...
284
                                        'iscategorized' => 1,
285
                                        'isrenamed' => 1,
286
                                        'proc_pp' => 1,
287
                                    ]);
288
                                    if (config('nntmux.elasticsearch_enabled') === true) {
289
                                        $this->elasticsearch->updateRelease($release->id);
290
                                    } else {
291
                                        $this->manticore->updateRelease($release->id);
292
                                    }
293
                                }
294
                                $this->releaseExtra->addFromXml($release->id, $xmlArray);
295
                                $retVal = true;
296
                                break;
297
                            }
298
                        }
299
                    }
300
                } catch (\Throwable $e) {
301
                    Log::debug($e->getMessage());
302
                }
303
            }
304
305
            if ($processAudioSample) {
306
                $audioFileName = ($release->guid.'.ogg');
0 ignored issues
show
Bug introduced by
The property guid does not seem to exist on App\Models\Release. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
307
                if ($this->ffprobe->isValid($fileLocation)) {
308
                    try {
309
                        $audioSample = $this->ffmpeg->open($fileLocation);
310
                        $format = new Vorbis;
311
                        $audioSample->clip(TimeCode::fromSeconds(30), TimeCode::fromSeconds(30));
312
                        $audioSample->save($format, $audioSavePath.$audioFileName);
313
                    } catch (\Throwable $e) {
314
                        if (config('app.debug') === true) {
315
                            Log::error($e->getMessage());
316
                        }
317
                    }
318
                }
319
                if (File::isFile($audioSavePath.$audioFileName)) {
320
                    @chmod($audioSavePath.$audioFileName, 0764);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

320
                    /** @scrutinizer ignore-unhandled */ @chmod($audioSavePath.$audioFileName, 0764);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
321
                    Release::query()->where('id', $release->id)->update(['audiostatus' => 1]);
322
                    $audVal = true;
323
                }
324
            }
325
        }
326
327
        return ['info' => $retVal, 'sample' => $audVal];
328
    }
329
}
330