Passed
Push — master ( 74d8e9...36c944 )
by Benjamin
13:38 queued 09:09
created

YouTube::getLargestThumbnail()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link      https://dukt.net/videos/
4
 * @copyright Copyright (c) 2018, Dukt
5
 * @license   https://github.com/dukt/videos/blob/v2/LICENSE.md
6
 */
7
8
namespace dukt\videos\gateways;
9
10
use dukt\videos\base\Gateway;
11
use dukt\videos\errors\VideoNotFoundException;
12
use dukt\videos\models\Collection;
13
use dukt\videos\models\Section;
14
use dukt\videos\models\Video;
15
use GuzzleHttp\Client;
16
17
/**
18
 * YouTube represents the YouTube gateway
19
 *
20
 * @author    Dukt <[email protected]>
21
 * @since     1.0
22
 */
23
class YouTube extends Gateway
24
{
25
    // Public Methods
26
    // =========================================================================
27
28
    /**
29
     * @inheritDoc
30
     *
31
     * @return string
32
     */
33
    public function getIconAlias(): string
34
    {
35
        return '@dukt/videos/icons/youtube.svg';
36
    }
37
38
    /**
39
     * @inheritDoc
40
     *
41
     * @return string
42
     */
43
    public function getName(): string
44
    {
45
        return 'YouTube';
46
    }
47
48
    /**
49
     * Returns the OAuth provider’s name.
50
     *
51
     * @return string
52
     */
53
    public function getOauthProviderName(): string
54
    {
55
        return 'Google';
56
    }
57
58
    /**
59
     * Returns the OAuth provider’s API console URL.
60
     *
61
     * @return string
62
     */
63
    public function getOauthProviderApiConsoleUrl(): string
64
    {
65
        return 'https://console.developers.google.com/';
66
    }
67
68
    /**
69
     * @inheritDoc
70
     *
71
     * @return array
72
     */
73
    public function getOauthScope(): array
74
    {
75
        return [
76
            'https://www.googleapis.com/auth/userinfo.profile',
77
            'https://www.googleapis.com/auth/userinfo.email',
78
            'https://www.googleapis.com/auth/youtube',
79
            'https://www.googleapis.com/auth/youtube.readonly'
80
        ];
81
    }
82
83
    /**
84
     * @inheritDoc
85
     *
86
     * @return array
87
     */
88
    public function getOauthAuthorizationOptions(): array
89
    {
90
        return [
91
            'access_type' => 'offline',
92
            'approval_prompt' => 'force'
93
        ];
94
    }
95
96
    /**
97
     * @inheritdoc
98
     */
99
    public function getOauthProviderOptions(): array
100
    {
101
        $options = parent::getOauthProviderOptions();
102
103
        if(!isset($options['useOidcMode'])) {
104
            $options['useOidcMode'] = true;
105
        }
106
107
        return $options;
108
    }
109
110
    /**
111
     * @inheritDoc
112
     *
113
     * @param array $options
114
     *
115
     * @return \League\OAuth2\Client\Provider\Google
116
     */
117
    public function createOauthProvider(array $options): \League\OAuth2\Client\Provider\Google
118
    {
119
        return new \League\OAuth2\Client\Provider\Google($options);
120
    }
121
122
    /**
123
     * @inheritDoc
124
     *
125
     * @return array
126
     * @throws \dukt\videos\errors\ApiResponseException
127
     */
128
    public function getExplorerSections(): array
129
    {
130
        $sections = [];
131
132
133
        // Library
134
135
        $sections[] = new Section([
136
            'name' => 'Library',
137
            'collections' => [
138
                new Collection([
139
                    'name' => 'Uploads',
140
                    'method' => 'uploads',
141
                ]),
142
                new Collection([
143
                    'name' => 'Liked videos',
144
                    'method' => 'likes',
145
                ])
146
            ]
147
        ]);
148
149
150
        // Playlists
151
152
        $playlists = $this->getCollectionsPlaylists();
153
154
        $collections = [];
155
156
        foreach ($playlists as $playlist) {
157
            $collections[] = new Collection([
158
                'name' => $playlist['title'],
159
                'method' => 'playlist',
160
                'options' => ['id' => $playlist['id']],
161
            ]);
162
        }
163
164
        if (\count($collections) > 0) {
165
            $sections[] = new Section([
166
                'name' => 'Playlists',
167
                'collections' => $collections,
168
            ]);
169
        }
170
171
        return $sections;
172
    }
173
174
    /**
175
     * @inheritDoc
176
     *
177
     * @param string $id
178
     *
179
     * @return Video
180
     * @throws VideoNotFoundException
181
     * @throws \dukt\videos\errors\ApiResponseException
182
     */
183
    public function getVideoById(string $id): Video
184
    {
185
        $data = $this->get('videos', [
186
            'query' => [
187
                'part' => 'snippet,statistics,contentDetails',
188
                'id' => $id
189
            ]
190
        ]);
191
192
        $videos = $this->parseVideos($data['items']);
193
194
        if (\count($videos) === 1) {
195
            return array_pop($videos);
196
        }
197
198
        throw new VideoNotFoundException('Video not found.');
199
    }
200
201
    /**
202
     * @inheritDoc
203
     *
204
     * @return string
205
     */
206
    public function getEmbedFormat(): string
207
    {
208
        return 'https://www.youtube.com/embed/%s?wmode=transparent';
209
    }
210
211
    /**
212
     * @inheritDoc
213
     *
214
     * @param $url
215
     *
216
     * @return bool|string
217
     */
218
    public function extractVideoIdFromUrl(string $url)
219
    {
220
        // check if url works with this service and extract video_id
221
222
        $video_id = false;
223
224
        $regexp = ['/^https?:\/\/(www\.youtube\.com|youtube\.com|youtu\.be).*\/(watch\?v=)?(.*)/', 3];
225
226
        if (preg_match($regexp[0], $url, $matches, PREG_OFFSET_CAPTURE) > 0) {
227
            // regexp match key
228
            $match_key = $regexp[1];
229
230
            // define video id
231
            $video_id = $matches[$match_key][0];
232
233
            // Fixes the youtube &feature_gdata bug
234
            if (strpos($video_id, '&')) {
235
                $video_id = substr($video_id, 0, strpos($video_id, '&'));
236
            }
237
        }
238
239
        // here we should have a valid video_id or false if service not matching
240
        return $video_id;
241
    }
242
243
    /**
244
     * @inheritDoc
245
     *
246
     * @return bool
247
     */
248
    public function supportsSearch(): bool
249
    {
250
        return true;
251
    }
252
253
    // Protected Methods
254
    // =========================================================================
255
256
    /**
257
     * Returns an authenticated Guzzle client
258
     *
259
     * @return Client
260
     * @throws \yii\base\InvalidConfigException
261
     */
262
    protected function createClient(): Client
263
    {
264
        $options = [
265
            'base_uri' => $this->getApiUrl(),
266
            'headers' => [
267
                'Authorization' => 'Bearer '.$this->getOauthToken()->getToken()
268
            ]
269
        ];
270
271
        return new Client($options);
272
    }
273
274
    /**
275
     * Returns a list of liked videos.
276
     *
277
     * @param array $params
278
     *
279
     * @return array
280
     * @throws \dukt\videos\errors\ApiResponseException
281
     */
282
    protected function getVideosLikes(array $params = []): array
283
    {
284
        $query = [];
285
        $query['part'] = 'snippet,statistics,contentDetails';
286
        $query['myRating'] = 'like';
287
        $query = array_merge($query, $this->paginationQueryFromParams($params));
288
289
        $videosResponse = $this->get('videos', ['query' => $query]);
290
291
        $videos = $this->parseVideos($videosResponse['items']);
292
293
        return array_merge([
294
            'videos' => $videos,
295
        ], $this->paginationResponse($videosResponse, $videos));
296
    }
297
298
    /**
299
     * Returns a list of videos in a playlist.
300
     *
301
     * @param array $params
302
     *
303
     * @return array
304
     * @throws \dukt\videos\errors\ApiResponseException
305
     */
306
    protected function getVideosPlaylist(array $params = []): array
307
    {
308
        // Get video IDs from playlist items
309
310
        $videoIds = [];
311
312
        $query = [];
313
        $query['part'] = 'id,snippet';
314
        $query['playlistId'] = $params['id'];
315
        $query = array_merge($query, $this->paginationQueryFromParams($params));
316
317
        $playlistItemsResponse = $this->get('playlistItems', ['query' => $query]);
318
319
        foreach ($playlistItemsResponse['items'] as $item) {
320
            $videoId = $item['snippet']['resourceId']['videoId'];
321
            $videoIds[] = $videoId;
322
        }
323
324
325
        // Get videos from video IDs
326
327
        $query = [];
328
        $query['part'] = 'snippet,statistics,contentDetails';
329
        $query['id'] = implode(',', $videoIds);
330
331
        $videosResponse = $this->get('videos', ['query' => $query]);
332
        $videos = $this->parseVideos($videosResponse['items']);
333
334
        return array_merge([
335
            'videos' => $videos,
336
        ], $this->paginationResponse($playlistItemsResponse, $videos));
337
    }
338
339
    /**
340
     * Returns a list of videos from a search request.
341
     *
342
     * @param array $params
343
     *
344
     * @return array
345
     * @throws \dukt\videos\errors\ApiResponseException
346
     */
347
    protected function getVideosSearch(array $params = []): array
348
    {
349
        // Get video IDs from search results
350
        $videoIds = [];
351
352
        $query = [];
353
        $query['part'] = 'id';
354
        $query['type'] = 'video';
355
        $query['q'] = $params['q'];
356
        $query = array_merge($query, $this->paginationQueryFromParams($params));
357
358
        $searchResponse = $this->get('search', ['query' => $query]);
359
360
        foreach ($searchResponse['items'] as $item) {
361
            $videoIds[] = $item['id']['videoId'];
362
        }
363
364
365
        // Get videos from video IDs
366
367
        if (\count($videoIds) > 0) {
368
369
            $query = [];
370
            $query['part'] = 'snippet,statistics,contentDetails';
371
            $query['id'] = implode(',', $videoIds);
372
373
            $videosResponse = $this->get('videos', ['query' => $query]);
374
375
            $videos = $this->parseVideos($videosResponse['items']);
376
377
            return array_merge([
378
                'videos' => $videos,
379
            ], $this->paginationResponse($searchResponse, $videos));
380
        }
381
382
        return [];
383
    }
384
385
    /**
386
     * Returns a list of uploaded videos.
387
     *
388
     * @param array $params
389
     *
390
     * @return array
391
     * @throws \dukt\videos\errors\ApiResponseException
392
     */
393
    protected function getVideosUploads(array $params = []): array
394
    {
395
        $uploadsPlaylistId = $this->getSpecialPlaylistId('uploads');
396
397
        if (!$uploadsPlaylistId) {
398
            return [];
399
        }
400
401
402
        // Retrieve video IDs
403
404
        $query = [];
405
        $query['part'] = 'id,snippet';
406
        $query['playlistId'] = $uploadsPlaylistId;
407
        $query = array_merge($query, $this->paginationQueryFromParams($params));
408
409
        $playlistItemsResponse = $this->get('playlistItems', ['query' => $query]);
410
411
        $videoIds = [];
412
413
        foreach ($playlistItemsResponse['items'] as $item) {
414
            $videoId = $item['snippet']['resourceId']['videoId'];
415
            $videoIds[] = $videoId;
416
        }
417
418
419
        // Retrieve videos from video IDs
420
421
        $query = [];
422
        $query['part'] = 'snippet,statistics,contentDetails,status';
423
        $query['id'] = implode(',', $videoIds);
424
425
        $videosResponse = $this->get('videos', ['query' => $query]);
426
427
        $videos = $this->parseVideos($videosResponse['items']);
428
429
        return array_merge([
430
            'videos' => $videos,
431
        ], $this->paginationResponse($playlistItemsResponse, $videos));
432
    }
433
434
    // Private Methods
435
    // =========================================================================
436
437
    /**
438
     * @return string
439
     */
440
    private function getApiUrl(): string
441
    {
442
        return 'https://www.googleapis.com/youtube/v3/';
443
    }
444
445
    /**
446
     * @return array
447
     * @throws \dukt\videos\errors\ApiResponseException
448
     */
449
    private function getCollectionsPlaylists(): array
450
    {
451
        $data = $this->get('playlists', [
452
            'query' => [
453
                'part' => 'snippet',
454
                'mine' => 'true'
455
            ]
456
        ]);
457
458
        return $this->parseCollections($data['items']);
459
    }
460
461
    /**
462
     * @return null|mixed
463
     * @throws \dukt\videos\errors\ApiResponseException
464
     */
465
    private function getSpecialPlaylists()
466
    {
467
        $channelsQuery = [
468
            'part' => 'contentDetails',
469
            'mine' => 'true'
470
        ];
471
472
        $channelsResponse = $this->get('channels', ['query' => $channelsQuery]);
473
474
        if (isset($channelsResponse['items'][0])) {
475
            $channel = $channelsResponse['items'][0];
476
477
            return $channel['contentDetails']['relatedPlaylists'];
478
        }
479
480
        return null;
481
    }
482
483
    /**
484
     * Retrieves playlist ID for special playlists of type: likes, favorites, uploads.
485
     *
486
     * @param string $type
487
     *
488
     * @return null|mixed
489
     * @throws \dukt\videos\errors\ApiResponseException
490
     */
491
    private function getSpecialPlaylistId(string $type)
492
    {
493
        $specialPlaylists = $this->getSpecialPlaylists();
494
495
        if (isset($specialPlaylists[$type])) {
496
            return $specialPlaylists[$type];
497
        }
498
499
        return null;
500
    }
501
502
    /**
503
     * @param array $params
504
     *
505
     * @return array
506
     */
507
    private function paginationQueryFromParams(array $params = []): array
508
    {
509
        // Pagination
510
511
        $pagination = [
512
            'page' => 1,
513
            'perPage' => $this->getVideosPerPage(),
514
            'moreToken' => false
515
        ];
516
517
        if (!empty($params['perPage'])) {
518
            $pagination['perPage'] = $params['perPage'];
519
        }
520
521
        if (!empty($params['moreToken'])) {
522
            $pagination['moreToken'] = $params['moreToken'];
523
        }
524
525
526
        // Query
527
528
        $query = [];
529
        $query['maxResults'] = $pagination['perPage'];
530
531
        if (!empty($pagination['moreToken'])) {
532
            $query['pageToken'] = $pagination['moreToken'];
533
        }
534
535
        return $query;
536
    }
537
538
    /**
539
     * @param $response
540
     * @param $videos
541
     *
542
     * @return array
543
     */
544
    private function paginationResponse($response, $videos): array
545
    {
546
        $more = false;
547
548
        if (!empty($response['nextPageToken']) && \count($videos) > 0) {
549
            $more = true;
550
        }
551
552
        return [
553
            'prevPage' => $response['prevPageToken'] ?? null,
554
            'moreToken' => $response['nextPageToken'] ?? null,
555
            'more' => $more
556
        ];
557
    }
558
559
    /**
560
     * @param $item
561
     *
562
     * @return array
563
     */
564
    private function parseCollection($item): array
565
    {
566
        $collection = [];
567
        $collection['id'] = $item['id'];
568
        $collection['title'] = $item['snippet']['title'];
569
        $collection['totalVideos'] = 0;
570
        $collection['url'] = 'title';
571
572
        return $collection;
573
    }
574
575
    /**
576
     * @param $items
577
     *
578
     * @return array
579
     */
580
    private function parseCollections($items): array
581
    {
582
        $collections = [];
583
584
        foreach ($items as $item) {
585
            $collection = $this->parseCollection($item);
586
            $collections[] = $collection;
587
        }
588
589
        return $collections;
590
    }
591
592
    /**
593
     * @param $data
594
     *
595
     * @return Video
596
     * @throws \Exception
597
     */
598
    private function parseVideo($data): Video
599
    {
600
        $video = new Video;
601
        $video->raw = $data;
602
        $video->authorName = $data['snippet']['channelTitle'];
603
        $video->authorUrl = 'http://youtube.com/channel/'.$data['snippet']['channelId'];
604
        $video->date = strtotime($data['snippet']['publishedAt']);
0 ignored issues
show
Documentation Bug introduced by
It seems like strtotime($data['snippet']['publishedAt']) of type integer is incompatible with the declared type null|DateTime of property $date.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
605
        $video->description = $data['snippet']['description'];
606
        $video->gatewayHandle = 'youtube';
607
        $video->gatewayName = 'YouTube';
608
        $video->id = $data['id'];
609
        $video->plays = $data['statistics']['viewCount'];
610
        $video->title = $data['snippet']['title'];
611
        $video->url = 'http://youtu.be/'.$video->id;
612
613
        // Video Duration
614
        $interval = new \DateInterval($data['contentDetails']['duration']);
615
        $video->durationSeconds = (int) $interval->format('%s');
616
617
        // Thumbnails
618
        $video->thumbnailSource = $this->getLargestThumbnail($data['snippet']['thumbnails']);
619
620
        // Privacy
621
        if (!empty($data['status']['privacyStatus']) && $data['status']['privacyStatus'] === 'private') {
622
            $video->private = true;
623
        }
624
625
        return $video;
626
    }
627
628
    /**
629
     * Get the largest thumbnail from an array of thumbnails.
630
     *
631
     * @param array $thumbnails
632
     * @return null|string
633
     */
634
    private function getLargestThumbnail(array $thumbnails)
635
    {
636
        $largestSize = 0;
637
        $largestThumbnail = null;
638
639
        foreach ($thumbnails as $thumbnail) {
640
            if ($thumbnail['width'] > $largestSize) {
641
                // Set thumbnail source with the largest thumbnail
642
                $largestThumbnail = $thumbnail['url'];
643
                $largestSize = $thumbnail['width'];
644
            }
645
        }
646
647
        return $largestThumbnail;
648
    }
649
650
    /**
651
     * @param $data
652
     *
653
     * @return array
654
     * @throws \Exception
655
     */
656
    private function parseVideos($data): array
657
    {
658
        $videos = [];
659
660
        foreach ($data as $videoData) {
661
            $video = $this->parseVideo($videoData);
662
            $videos[] = $video;
663
        }
664
665
        return $videos;
666
    }
667
}
668