Completed
Push — master ( 8b90c1...6d49a3 )
by Dorian
03:10
created

YouTube::download()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 50
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 8.6315
c 0
b 0
f 0
cc 6
eloc 33
nc 9
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
        // Try to create the downloads directory... 'cause if it fails, nothing will work.
53
        (new Filesystem())->mkdir((string) $downloadPath);
54
55
        // Add the platform path part and get a collection of downloads
56
        $downloads = new Collection();
57
        foreach ($contents as $content) {
58
            $content->getPath()->add($platformPathPart);
59
60
            foreach ($this->extractDownloads($content) as $download) {
61
                $downloads->add($download);
62
            }
63
        }
64
65
        $this->cleanFilesystem($downloads, $downloadPath);
66
67
        $downloads = $this->filterAlreadyDownloaded($downloads);
68
69
        if ($this->shouldDownload($downloads, $downloadPath)) {
1 ignored issue
show
Bug introduced by
It seems like $downloads defined by $this->filterAlreadyDownloaded($downloads) on line 67 can also be of type array<integer,object<App...form\YouTube\Download>>; however, App\Platform\YouTube\YouTube::shouldDownload() does only seem to accept object<App\Domain\Collection>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
70
            $this->download($downloads);
1 ignored issue
show
Bug introduced by
It seems like $downloads defined by $this->filterAlreadyDownloaded($downloads) on line 67 can also be of type array<integer,object<App...form\YouTube\Download>>; however, App\Platform\YouTube\YouTube::download() does only seem to accept object<App\Domain\Collection>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
71
        }
72
    }
73
74
    /**
75
     * @param \App\Domain\Content $content
76
     *
77
     * @return \App\Platform\YouTube\Download[]|\App\Domain\Collection
78
     */
79
    private function extractDownloads(Content $content): Collection
80
    {
81
        $downloads = new Collection();
82
83
        if (preg_match_all(static::YOUTUBE_URL_REGEX, $content->getData(), $youtubeUrls)) {
84
            foreach ((array) $youtubeUrls[static::YOUTUBE_URL_REGEX_MATCHES_ID_INDEX] as $youtubeId) {
85
                foreach (array_keys($this->options['youtube_dl']['options']) as $videoFileType) {
86
                    $downloads->add(
87
                        new Download(
88
                            $content->getPath(),
89
                            $youtubeId,
90
                            $videoFileType,
91
                            $this->options['patterns']['extensions'][$videoFileType]
92
                        )
93
                    );
94
                }
95
            }
96
        }
97
98
        return $downloads;
99
    }
100
101
    /**
102
     * @param \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads
103
     *
104
     * @return \App\Platform\YouTube\Download[]|\App\Domain\Collection
105
     */
106
    private function filterAlreadyDownloaded(Collection $downloads): Collection
107
    {
108
        return $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
124
    /**
125
     * @param \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads
126
     * @param \App\Domain\Path $downloadPath
127
     *
128
     * @return bool
129
     */
130
    private function shouldDownload(Collection $downloads, Path $downloadPath): bool
131
    {
132
        $this->ui->writeln('Download files from YouTube... '.PHP_EOL);
133
134
        if ($downloads->isEmpty()) {
135
            $this->ui->writeln($this->ui->indent().'<comment>Nothing to download.</comment>'.PHP_EOL);
136
137
            return false;
138
        }
139
140
        $this->ui->writeln(
141
            sprintf(
142
                '%sThe script is about to download <question> %s </question> files into <info>%s</info>. '.PHP_EOL,
143
                $this->ui->indent(),
144
                $downloads->count(),
145
                (string) $downloadPath
146
            )
147
        );
148
149
        $this->ui->write($this->ui->indent());
150
        if ($this->skip() || !$this->ui->confirm()) {
151
            $this->ui->writeln(PHP_EOL.'<info>Done.</info>'.PHP_EOL);
152
153
            return false;
154
        }
155
156
        return true;
157
    }
158
159
    /**
160
     * @param \App\Platform\YouTube\Download[]|\App\Domain\Collection $downloads
161
     *
162
     * @throws \RuntimeException
163
     */
164
    private function download(Collection $downloads)
165
    {
166
        $errors = [];
167
        foreach ($downloads as $download) {
168
            $this->ui->write(
169
                sprintf(
170
                    '%s* [<comment>%s</comment>][<comment>%s</comment>] Download the %s file in <info>%s</info>... ',
171
                    $this->ui->indent(2),
172
                    $download->getVideoId(),
173
                    $download->getFileType(),
174
                    $download->getFileExtension(),
175
                    $download->getPath()
176
                )
177
            );
178
179
            $options = $this->options['youtube_dl']['options'][$download->getFileType()];
180
            $nbAttempts = \count($options);
181
182
            $attempt = 0;
183
            while (true) {
184
                try {
185
                    $this->doDownload($download, $options[$attempt]);
186
187
                    $this->ui->writeln('<info>Done.</info>');
188
                    break;
189
190
                } catch (YoutubeDlException\ChannelRemovedByUserException
191
                    | YoutubeDlException\VideoBlockedByCopyrightException
192
                    | YoutubeDlException\VideoRemovedByUserException
193
                    | YoutubeDlException\VideoUnavailableException
194
                $e) {
195
                    $this->ui->logError($e->getMessage(), $errors);
196
                    break;
197
198
                // These are (supposedly) connection/download errors, so we try again
199
                } catch (\Exception $e) {
200
                    $attempt++;
201
202
                    // Maximum number of attempts reached, move along...
203
                    if ($attempt === $nbAttempts) {
204
                        $this->ui->logError($e->getMessage(), $errors);
205
                        break;
206
                    }
207
                }
208
            }
209
        }
210
        $this->ui->displayErrors($errors, 'download of files', 'error', 1);
211
212
        $this->ui->writeln(PHP_EOL.'<info>Done.</info>'.PHP_EOL);
213
    }
214
215
    /**
216
     * @param \App\Platform\YouTube\Download $download
217
     * @param array $youtubeDlOptions
218
     *
219
     * @throws \Exception
220
     */
221
    private function doDownload(Download $download, array $youtubeDlOptions)
222
    {
223
        $dl = new YoutubeDl($youtubeDlOptions);
224
        $dl->setDownloadPath((string) $download->getPath());
225
226
        try {
227
            (new Filesystem())->mkdir((string) $download->getPath());
228
            $dl->download(static::YOUTUBE_URL_PREFIX.$download->getVideoId());
229
        } catch (\Exception $e) {
230
231
            // Add more custom exceptions than those already provided by YoutubeDl
232
            if (preg_match('/this video is unavailable/i', $e->getMessage())) {
233
                throw new YoutubeDlException\VideoUnavailableException(
234
                    sprintf('The video %s is unavailable.', $download->getVideoId()), 0, $e
235
                );
236
            }
237
            if (preg_match('/this video has been removed by the user/i', $e->getMessage())) {
238
                throw new YoutubeDlException\VideoRemovedByUserException(
239
                    sprintf('The video %s has been removed by its user.', $download->getVideoId()), 0, $e
240
                );
241
            }
242
            if (preg_match('/the uploader has closed their YouTube account/i', $e->getMessage())) {
243
                throw new YoutubeDlException\ChannelRemovedByUserException(
244
                    sprintf('The channel that published the video %s has been removed.', $download->getVideoId()), 0, $e
245
                );
246
            }
247
            if (preg_match('/who has blocked it on copyright grounds/i', $e->getMessage())) {
248
                throw new YoutubeDlException\VideoBlockedByCopyrightException(
249
                    sprintf('The video %s has been block for copyright infringement.', $download->getVideoId()), 0, $e
250
                );
251
            }
252
253
            throw $e;
254
        }
255
    }
256
}
257