Completed
Push — master ( 738ba0...8b90c1 )
by Dorian
01:21
created

YouTube::extractDownloads()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 12
nc 2
nop 1
1
<?php declare(strict_types=1);
2
3
namespace App\Platform\YouTube;
4
5
use App\Domain\Collection;
6
use App\Domain\Content;
7
use App\Domain\Path;
8
use App\Domain\PathPart;
9
use App\Platform\Platform;
10
use App\UI\UserInterface;
11
use App\YoutubeDl\Exception as YoutubeDlException;
12
use App\YoutubeDl\YoutubeDl;
13
use Symfony\Component\Filesystem\Filesystem;
14
15
final class YouTube implements Platform
16
{
17
    use FilesystemManager;
18
19
    // See https://stackoverflow.com/a/37704433/389519
20
    private const YOUTUBE_URL_REGEX = <<<REGEX
21
/\b((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?/i
22
REGEX;
23
    private const YOUTUBE_URL_REGEX_MATCHES_ID_INDEX = 5;
24
    private const YOUTUBE_URL_PREFIX = 'https://www.youtube.com/watch?v=';
25
26
    /**
27
     * @param UserInterface $ui
28
     * @param array $options
29
     * @param bool $dryRun
30
     */
31
    public function __construct(UserInterface $ui, array $options, bool $dryRun = false)
32
    {
33
        $this->ui = $ui;
34
        $this->options = $options;
35
        $this->dryRun = $dryRun;
36
    }
37
38
    /**
39
     * @param \App\Domain\Content[]|\App\Domain\Collection $contents
40
     * @param \App\Domain\PathPart $rootPathPart
41
     * @throws \RuntimeException
42
     */
43
    public function synchronizeContents(Collection $contents, PathPart $rootPathPart): void
44
    {
45
        if ($contents->isEmpty()) {
46
            return;
47
        }
48
49
        $platformPathPart = new PathPart($this->options['path_part']);
50
        $downloadPath = new Path([$rootPathPart, $platformPathPart]);
51
52
        // Add the platform path part and get a collection of downloads
53
        $downloads = new Collection();
54
        foreach ($contents as $content) {
55
            $content->getPath()->add($platformPathPart);
56
57
            foreach ($this->extractDownloads($content) as $download) {
58
                $downloads->add($download);
59
            }
60
        }
61
62
        $this->cleanFilesystem($downloads, $downloadPath);
63
        $this->download($downloads, $downloadPath);
64
    }
65
66
    /**
67
     * @param \App\Domain\Content $content
68
     *
69
     * @return \App\Platform\YouTube\Download[]|\App\Domain\Collection
70
     */
71
    private function extractDownloads(Content $content): Collection
72
    {
73
        $downloads = new Collection();
74
75
        if (preg_match_all(static::YOUTUBE_URL_REGEX, $content->getData(), $youtubeUrls)) {
76
            foreach ((array) $youtubeUrls[static::YOUTUBE_URL_REGEX_MATCHES_ID_INDEX] as $youtubeId) {
77
                foreach (array_keys($this->options['youtube_dl']['options']) as $videoFileType) {
78
                    $downloads->add(
79
                        new Download(
80
                            $content->getPath(),
81
                            $youtubeId,
82
                            $videoFileType,
83
                            $this->options['patterns']['extensions'][$videoFileType]
84
                        )
85
                    );
86
                }
87
            }
88
        }
89
90
        return $downloads;
91
    }
92
93
    /**
94
     * @param \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads
95
     * @param \App\Domain\Path $downloadPath
96
     *
97
     * @throws \RuntimeException
98
     */
99
    private function download(Collection $downloads, Path $downloadPath)
100
    {
101
        // Try to create the downloads directory... 'cause if it fails, nothing will work.
102
        (new Filesystem())->mkdir((string) $downloadPath);
103
104
        $this->ui->writeln('Download files from YouTube... '.PHP_EOL);
105
106
        // Filter out downloads that have already been downloaded
107
        /** @var \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads */
108
        $downloads = $downloads->filter(
109
            function (Download $download) {
110
                $shouldBeDownloaded = true;
111
                try {
112
                    if ($this->getDownloadFolderFinder($download)->hasResults()) {
113
                        $shouldBeDownloaded = false;
114
                    }
115
                } catch (\InvalidArgumentException $e) {
116
                    // Here we know that the download folder will exist.
117
                }
118
119
                return $shouldBeDownloaded;
120
            }
121
        );
122
123
        if ($downloads->isEmpty()) {
124
            $this->ui->writeln($this->ui->indent().'<comment>Nothing to download.</comment>'.PHP_EOL);
125
126
            return;
127
        }
128
129
        $this->ui->writeln(
130
            sprintf(
131
                '%sThe script is about to download <question> %s </question> files into <info>%s</info>. '.PHP_EOL,
132
                $this->ui->indent(),
133
                $downloads->count(),
134
                (string) $downloadPath
135
            )
136
        );
137
138
        $this->ui->write($this->ui->indent());
139
        if ($this->skip() || !$this->ui->confirm()) {
140
            $this->ui->writeln(PHP_EOL.'<info>Done.</info>'.PHP_EOL);
141
142
            return;
143
        }
144
145
        $errors = [];
146
        foreach ($downloads as $download) {
147
            $this->ui->write(
148
                sprintf(
149
                    '%s* [<comment>%s</comment>][<comment>%s</comment>] Download the %s file in <info>%s</info>... ',
150
                    $this->ui->indent(2),
151
                    $download->getVideoId(),
152
                    $download->getFileType(),
153
                    $download->getFileExtension(),
154
                    $download->getPath()
155
                )
156
            );
157
158
            $options = $this->options['youtube_dl']['options'][$download->getFileType()];
159
            $nbAttempts = \count($options);
160
161
            $attempt = 0;
162
            while (true) {
163
                try {
164
                    $this->doDownload($download, $options[$attempt]);
165
166
                    $this->ui->writeln('<info>Done.</info>');
167
                    break;
168
169
                } catch (YoutubeDlException\ChannelRemovedByUserException
170
                    | YoutubeDlException\VideoBlockedByCopyrightException
171
                    | YoutubeDlException\VideoRemovedByUserException
172
                    | YoutubeDlException\VideoUnavailableException
173
                $e) {
174
                    $this->ui->logError($e->getMessage(), $errors);
175
                    break;
176
177
                // These are (supposedly) connection/download errors, so we try again
178
                } catch (\Exception $e) {
179
                    $attempt++;
180
181
                    // Maximum number of attempts reached, move along...
182
                    if ($attempt === $nbAttempts) {
183
                        $this->ui->logError($e->getMessage(), $errors);
184
                        break;
185
                    }
186
                }
187
            }
188
        }
189
        $this->ui->displayErrors($errors, 'download of files', 'error', 1);
190
191
        $this->ui->writeln(PHP_EOL.'<info>Done.</info>'.PHP_EOL);
192
    }
193
194
    /**
195
     * @param \App\Platform\YouTube\Download $download
196
     * @param array $youtubeDlOptions
197
     *
198
     * @throws \Exception
199
     */
200
    private function doDownload(Download $download, array $youtubeDlOptions)
201
    {
202
        $dl = new YoutubeDl($youtubeDlOptions);
203
        $dl->setDownloadPath((string) $download->getPath());
204
205
        try {
206
            (new Filesystem())->mkdir((string) $download->getPath());
207
            $dl->download(static::YOUTUBE_URL_PREFIX.$download->getVideoId());
208
        } catch (\Exception $e) {
209
210
            // Add more custom exceptions than those already provided by YoutubeDl
211
            if (preg_match('/this video is unavailable/i', $e->getMessage())) {
212
                throw new YoutubeDlException\VideoUnavailableException(
213
                    sprintf('The video %s is unavailable.', $download->getVideoId()), 0, $e
214
                );
215
            }
216
            if (preg_match('/this video has been removed by the user/i', $e->getMessage())) {
217
                throw new YoutubeDlException\VideoRemovedByUserException(
218
                    sprintf('The video %s has been removed by its user.', $download->getVideoId()), 0, $e
219
                );
220
            }
221
            if (preg_match('/the uploader has closed their YouTube account/i', $e->getMessage())) {
222
                throw new YoutubeDlException\ChannelRemovedByUserException(
223
                    sprintf('The channel that published the video %s has been removed.', $download->getVideoId()), 0, $e
224
                );
225
            }
226
            if (preg_match('/who has blocked it on copyright grounds/i', $e->getMessage())) {
227
                throw new YoutubeDlException\VideoBlockedByCopyrightException(
228
                    sprintf('The video %s has been block for copyright infringement.', $download->getVideoId()), 0, $e
229
                );
230
            }
231
232
            throw $e;
233
        }
234
    }
235
}
236