Completed
Push — master ( 98be7b...7a3330 )
by Dorian
01:30
created

YouTube   B

Complexity

Total Complexity 33

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 15
dl 0
loc 308
rs 8.6166
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
B synchronizeContents() 0 30 5
A getAllDownloadsFolderFinder() 0 14 1
B getDownloadFolderFinder() 0 29 1
A skip() 0 10 2
A extractDownloads() 0 21 4
A filterAlreadyDownloaded() 0 17 3
B shouldDownload() 0 28 5
B download() 0 50 6
B doDownload() 0 38 6
1
<?php declare(strict_types=1);
2
3
namespace App\Platform\YouTube;
4
5
use App\Domain\Collection\Contents;
6
use App\Domain\Content;
7
use App\Domain\Collection\Path;
8
use App\Domain\PathPart;
9
use App\Platform\Platform;
10
use App\Platform\YouTube\Exception as YouTubeException;
11
use Symfony\Component\Filesystem\Filesystem;
12
use Symfony\Component\Finder\Finder;
13
14
final class YouTube implements Platform
15
{
16
    use FilesystemManager;
17
18
    // See https://stackoverflow.com/a/37704433/389519
19
    private const YOUTUBE_URL_REGEX = <<<REGEX
20
/\b((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?/i
21
REGEX;
22
    private const YOUTUBE_URL_REGEX_MATCHES_ID_INDEX = 5;
23
    private const YOUTUBE_URL_PREFIX = 'https://www.youtube.com/watch?v=';
24
25
    /**
26
     * @param \App\Domain\Collection\Contents $contents
27
     * @param \App\Domain\PathPart $rootPathPart
28
     *
29
     * @throws \RuntimeException
30
     */
31
    public function synchronizeContents(Contents $contents, PathPart $rootPathPart): void
32
    {
33
        if ($contents->isEmpty()) {
34
            return;
35
        }
36
37
        $platformPathPart = new PathPart($this->options['path_part']);
38
        $downloadPath = new Path([$rootPathPart, $platformPathPart]);
39
40
        // Try to create the downloads directory... 'cause if it fails, nothing will work.
41
        (new Filesystem())->mkdir((string) $downloadPath);
42
43
        // Add the platform path part and get a collection of downloads
44
        $downloads = new Downloads();
0 ignored issues
show
Bug introduced by
The call to Downloads::__construct() misses a required argument $elements.

This check looks for function calls that miss required arguments.

Loading history...
45
        foreach ($contents as $content) {
46
            $content->getPath()->add($platformPathPart);
47
48
            foreach ($this->extractDownloads($content) as $download) {
49
                $downloads->add($download);
50
            }
51
        }
52
53
        $this->cleanFilesystem($downloads, $downloadPath);
54
55
        $downloads = $this->filterAlreadyDownloaded($downloads);
56
57
        if ($this->shouldDownload($downloads, $downloadPath)) {
58
            $this->download($downloads, $downloadPath);
59
        }
60
    }
61
62
    /** @noinspection LowerAccessLevelInspection */
63
    /**
64
     * @param \App\Domain\Collection\Path $downloadPath
65
     *
66
     * @return \Symfony\Component\Finder\Finder
67
     */
68
    protected function getAllDownloadsFolderFinder(Path $downloadPath): Finder
69
    {
70
        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
71
        return (new Finder())
72
            ->directories()
73
            ->in((string) $downloadPath)
74
            ->sort(function (\SplFileInfo $fileInfoA, \SplFileInfo $fileInfoB) {
75
                // Sort the result by folder depth
76
                $a = substr_count($fileInfoA->getRealPath(), DIRECTORY_SEPARATOR);
77
                $b = substr_count($fileInfoB->getRealPath(), DIRECTORY_SEPARATOR);
78
79
                return $a <=> $b;
80
            });
81
    }
82
83
    /** @noinspection LowerAccessLevelInspection */
84
    /**
85
     * @param \App\Platform\YouTube\Download $download
86
     *
87
     * @return \Symfony\Component\Finder\Finder|\SplFileInfo[]
88
     * @throws \InvalidArgumentException
89
     */
90
    protected function getDownloadFolderFinder(Download $download): Finder
91
    {
92
        $placeholders = [
93
            '%video_id%' => $download->getVideoId(),
94
            '%file_extension%' => $download->getFileExtension(),
95
        ];
96
97
        $downloadPathPart = new PathPart([
98
            'path' => (string) $download->getPath(),
99
            'priority' => 0,
100
        ]);
101
        $folderPathPart = new PathPart([
102
            'path' => $this->options['patterns']['folder'],
103
            'priority' => 1,
104
            'substitutions' => $placeholders,
105
        ]);
106
107
        return (new Finder())
108
            ->files()
109
            ->depth('== 0')
110
            ->in((string) new Path([$downloadPathPart, $folderPathPart]))
111
            ->name(
112
                str_replace(
113
                    array_keys($placeholders),
114
                    array_values($placeholders),
115
                    $this->options['patterns']['filename']
116
                )
117
            );
118
    }
119
120
    /** @noinspection LowerAccessLevelInspection */
121
    /**
122
     * @return bool
123
     */
124
    protected function skip(): bool
125
    {
126
        if ($this->ui->isDryRun()) {
127
            $this->ui->writeln('<info>[DRY-RUN]</info> Not doing anything...'.PHP_EOL);
128
129
            return true;
130
        }
131
132
        return false;
133
    }
134
135
    /**
136
     * @param \App\Domain\Content $content
137
     *
138
     * @return \App\Platform\YouTube\Downloads
139
     */
140
    private function extractDownloads(Content $content): Downloads
141
    {
142
        $downloads = new Downloads();
0 ignored issues
show
Bug introduced by
The call to Downloads::__construct() misses a required argument $elements.

This check looks for function calls that miss required arguments.

Loading history...
143
144
        if (preg_match_all(static::YOUTUBE_URL_REGEX, $content->getData(), $youtubeUrls)) {
145
            foreach ((array) $youtubeUrls[static::YOUTUBE_URL_REGEX_MATCHES_ID_INDEX] as $youtubeId) {
146
                foreach (array_keys($this->options['youtube_dl']['options']) as $videoFileType) {
147
                    $downloads->add(
148
                        new Download(
149
                            $content->getPath(),
150
                            $youtubeId,
151
                            $videoFileType,
152
                            $this->options['patterns']['extensions'][$videoFileType]
153
                        )
154
                    );
155
                }
156
            }
157
        }
158
159
        return $downloads;
160
    }
161
162
    /**
163
     * @param \App\Platform\YouTube\Downloads $downloads
164
     *
165
     * @return \App\Platform\YouTube\Downloads
166
     */
167
    private function filterAlreadyDownloaded(Downloads $downloads): Downloads
168
    {
169
        return $downloads->filter(
170
            function (Download $download) {
171
                $shouldBeDownloaded = true;
172
                try {
173
                    if ($this->getDownloadFolderFinder($download)->hasResults()) {
174
                        $shouldBeDownloaded = false;
175
                    }
176
                } catch (\InvalidArgumentException $e) {
177
                    // Here we know that the download folder will exist.
178
                }
179
180
                return $shouldBeDownloaded;
181
            }
182
        );
183
    }
184
185
    /**
186
     * @param \App\Platform\YouTube\Downloads $downloads
187
     * @param \App\Domain\Collection\Path $downloadPath
188
     *
189
     * @return bool
190
     */
191
    private function shouldDownload(Downloads $downloads, Path $downloadPath): bool
192
    {
193
        $this->ui->writeln('Download files from YouTube... '.PHP_EOL);
194
195
        if ($downloads->isEmpty()) {
196
            $this->ui->writeln($this->ui->indent().'<comment>Nothing to download.</comment>'.PHP_EOL);
197
198
            return false;
199
        }
200
201
        $this->ui->writeln(
202
            sprintf(
203
                '%sThe script is about to download <question> %s </question> files into <info>%s</info>. '.PHP_EOL,
204
                $this->ui->indent(),
205
                $downloads->count(),
206
                (string) $downloadPath
207
            )
208
        );
209
210
        $this->ui->write($this->ui->indent());
211
        if ($this->skip() || !$this->ui->confirm()) {
212
            $this->ui->writeln(($this->ui->isDryRun() ? '' : PHP_EOL).'<info>Done.</info>'.PHP_EOL);
213
214
            return false;
215
        }
216
217
        return true;
218
    }
219
220
    /**
221
     * @param \App\Platform\YouTube\Downloads $downloads
222
     * @param \App\Domain\Collection\Path $downloadPath
223
     *
224
     * @throws \RuntimeException
225
     */
226
    private function download(Downloads $downloads, Path $downloadPath)
227
    {
228
        $errors = [];
229
        foreach ($downloads as $download) {
230
            $this->ui->write(
231
                sprintf(
232
                    '%s* [<comment>%s</comment>][<comment>%s</comment>] Download the %s file in <info>%s</info>... ',
233
                    $this->ui->indent(2),
234
                    $download->getVideoId(),
235
                    $download->getFileType(),
236
                    $download->getFileExtension(),
237
                    str_replace((string) $downloadPath.DIRECTORY_SEPARATOR, '', $download->getPath())
238
                )
239
            );
240
241
            $options = $this->options['youtube_dl']['options'][$download->getFileType()];
242
            $nbAttempts = \count($options);
243
244
            $attempt = 0;
245
            while (true) {
246
                try {
247
                    $this->doDownload($download, $options[$attempt]);
248
249
                    $this->ui->writeln('<info>Done.</info>');
250
                    break;
251
252
                } catch (YouTubeException\ChannelRemovedByUserException
253
                    | YouTubeException\VideoBlockedByCopyrightException
254
                    | YouTubeException\VideoRemovedByUserException
255
                    | YouTubeException\VideoUnavailableException
256
                $e) {
257
                    $this->ui->logError($e->getMessage(), $errors);
258
                    break;
259
260
                // These are (supposedly) connection/download errors, so we try again
261
                } catch (\Exception $e) {
262
                    $attempt++;
263
264
                    // Maximum number of attempts reached, move along...
265
                    if ($attempt === $nbAttempts) {
266
                        $this->ui->logError($e->getMessage(), $errors);
267
                        break;
268
                    }
269
                }
270
            }
271
        }
272
        $this->ui->displayErrors($errors, 'download of files', 'error', 1);
273
274
        $this->ui->writeln(PHP_EOL.'<info>Done.</info>'.PHP_EOL);
275
    }
276
277
    /**
278
     * @param \App\Platform\YouTube\Download $download
279
     * @param array $downloadOptions
280
     *
281
     * @throws \Exception
282
     */
283
    private function doDownload(Download $download, array $downloadOptions)
284
    {
285
        $youtubeDlOptions = $this->options['youtube_dl'];
286
287
        /** @var \YouTubeDl\YouTubeDl $dl */
288
        $dl = new $youtubeDlOptions['class_name']($downloadOptions);
289
        $dl->setDownloadPath((string) $download->getPath());
290
291
        try {
292
            (new Filesystem())->mkdir((string) $download->getPath());
293
            $dl->download(static::YOUTUBE_URL_PREFIX.$download->getVideoId());
294
        } catch (\Exception $e) {
295
296
            // Add more custom exceptions than those already provided by YoutubeDl
297
            if (preg_match('/this video is unavailable/i', $e->getMessage())) {
298
                throw new YouTubeException\VideoUnavailableException(
299
                    sprintf('The video %s is unavailable.', $download->getVideoId()), 0, $e
300
                );
301
            }
302
            if (preg_match('/this video has been removed by the user/i', $e->getMessage())) {
303
                throw new YouTubeException\VideoRemovedByUserException(
304
                    sprintf('The video %s has been removed by its user.', $download->getVideoId()), 0, $e
305
                );
306
            }
307
            if (preg_match('/the uploader has closed their YouTube account/i', $e->getMessage())) {
308
                throw new YouTubeException\ChannelRemovedByUserException(
309
                    sprintf('The channel that published the video %s has been removed.', $download->getVideoId()), 0, $e
310
                );
311
            }
312
            if (preg_match('/who has blocked it on copyright grounds/i', $e->getMessage())) {
313
                throw new YouTubeException\VideoBlockedByCopyrightException(
314
                    sprintf('The video %s has been block for copyright infringement.', $download->getVideoId()), 0, $e
315
                );
316
            }
317
318
            throw $e;
319
        }
320
    }
321
}
322