Issues (47)

src/gateways/YouTube.php (1 issue)

1
<?php
2
/**
3
 * @link      https://dukt.net/videos/
4
 * @copyright Copyright (c) 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
            'prompt' => 'consent',
93
        ];
94
    }
95
96
    /**
97
     * @inheritdoc
98
     */
99
    public function getOauthProviderOptions(bool $parse = true): array
100
    {
101
        $options = parent::getOauthProviderOptions($parse);
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
                    'icon' => 'video-camera',
142
                ]),
143
                new Collection([
144
                    'name' => 'Liked videos',
145
                    'method' => 'likes',
146
                    'icon' => 'thumb-up'
147
                ])
148
            ]
149
        ]);
150
151
152
        // Playlists
153
154
        $playlists = $this->getCollectionsPlaylists();
155
156
        $collections = [];
157
158
        foreach ($playlists as $playlist) {
159
            $collections[] = new Collection([
160
                'name' => $playlist['title'],
161
                'method' => 'playlist',
162
                'options' => ['id' => $playlist['id']],
163
                'icon' => 'list'
164
            ]);
165
        }
166
167
        if ($collections !== []) {
168
            $sections[] = new Section([
169
                'name' => 'Playlists',
170
                'collections' => $collections,
171
            ]);
172
        }
173
174
        return $sections;
175
    }
176
177
    /**
178
     * @inheritDoc
179
     *
180
     * @param string $id
181
     *
182
     * @return Video
183
     * @throws VideoNotFoundException
184
     * @throws \dukt\videos\errors\ApiResponseException
185
     */
186
    public function getVideoById(string $id): Video
187
    {
188
        $data = $this->get('videos', [
189
            'query' => [
190
                'part' => 'snippet,statistics,contentDetails',
191
                'id' => $id
192
            ]
193
        ]);
194
195
        $videos = $this->parseVideos($data['items']);
196
197
        if (\count($videos) === 1) {
198
            return array_pop($videos);
199
        }
200
201
        throw new VideoNotFoundException('Video not found.');
202
    }
203
204
    /**
205
     * @inheritDoc
206
     *
207
     * @return string
208
     */
209
    public function getEmbedFormat(): string
210
    {
211
        return 'https://www.youtube.com/embed/%s?wmode=transparent';
212
    }
213
214
    /**
215
     * @inheritDoc
216
     *
217
     * @param $url
218
     *
219
     * @return bool|string
220
     */
221
    public function extractVideoIdFromUrl(string $url)
222
    {
223
        // check if url works with this service and extract video_id
224
225
        $video_id = false;
226
227
        $regexp = ['/^https?:\/\/(www\.youtube\.com|youtube\.com|youtu\.be).*\/(watch\?v=)?(.*)/', 3];
228
229
        if (preg_match($regexp[0], $url, $matches, PREG_OFFSET_CAPTURE) > 0) {
230
            // regexp match key
231
            $match_key = $regexp[1];
232
233
            // define video id
234
            $video_id = $matches[$match_key][0];
235
236
            // Fixes the youtube &feature_gdata bug
237
            if (strpos($video_id, '&')) {
238
                $video_id = substr($video_id, 0, strpos($video_id, '&'));
239
            }
240
        }
241
242
        // here we should have a valid video_id or false if service not matching
243
        return $video_id;
244
    }
245
246
    /**
247
     * @inheritDoc
248
     *
249
     * @return bool
250
     */
251
    public function supportsSearch(): bool
252
    {
253
        return true;
254
    }
255
256
    // Protected Methods
257
    // =========================================================================
258
259
    /**
260
     * Returns an authenticated Guzzle client
261
     *
262
     * @return Client
263
     * @throws \yii\base\InvalidConfigException
264
     */
265
    protected function createClient(): Client
266
    {
267
        $options = [
268
            'base_uri' => $this->getApiUrl(),
269
            'headers' => [
270
                'Authorization' => 'Bearer ' . $this->getOauthToken()->getToken()
271
            ]
272
        ];
273
274
        return new Client($options);
275
    }
276
277
    /**
278
     * Returns a list of liked videos.
279
     *
280
     * @param array $params
281
     *
282
     * @return array
283
     * @throws \dukt\videos\errors\ApiResponseException
284
     */
285
    protected function getVideosLikes(array $params = []): array
286
    {
287
        $query = [];
288
        $query['part'] = 'snippet,statistics,contentDetails';
289
        $query['myRating'] = 'like';
290
        $query = array_merge($query, $this->paginationQueryFromParams($params));
291
292
        $videosResponse = $this->get('videos', ['query' => $query]);
293
294
        $videos = $this->parseVideos($videosResponse['items']);
295
296
        return array_merge([
297
            'videos' => $videos,
298
        ], $this->paginationResponse($videosResponse, $videos));
299
    }
300
301
    /**
302
     * Returns a list of videos in a playlist.
303
     *
304
     * @param array $params
305
     *
306
     * @return array
307
     * @throws \dukt\videos\errors\ApiResponseException
308
     */
309
    protected function getVideosPlaylist(array $params = []): array
310
    {
311
        // Get video IDs from playlist items
312
313
        $videoIds = [];
314
315
        $query = [];
316
        $query['part'] = 'id,snippet';
317
        $query['playlistId'] = $params['id'];
318
        $query = array_merge($query, $this->paginationQueryFromParams($params));
319
320
        $playlistItemsResponse = $this->get('playlistItems', ['query' => $query]);
321
322
        foreach ($playlistItemsResponse['items'] as $item) {
323
            $videoId = $item['snippet']['resourceId']['videoId'];
324
            $videoIds[] = $videoId;
325
        }
326
327
328
        // Get videos from video IDs
329
330
        $query = [];
331
        $query['part'] = 'snippet,statistics,contentDetails';
332
        $query['id'] = implode(',', $videoIds);
333
334
        $videosResponse = $this->get('videos', ['query' => $query]);
335
        $videos = $this->parseVideos($videosResponse['items']);
336
337
        return array_merge([
338
            'videos' => $videos,
339
        ], $this->paginationResponse($playlistItemsResponse, $videos));
340
    }
341
342
    /**
343
     * Returns a list of videos from a search request.
344
     *
345
     * @param array $params
346
     *
347
     * @return array
348
     * @throws \dukt\videos\errors\ApiResponseException
349
     */
350
    protected function getVideosSearch(array $params = []): array
351
    {
352
        // Get video IDs from search results
353
        $videoIds = [];
354
355
        $query = [];
356
        $query['part'] = 'id';
357
        $query['type'] = 'video';
358
        $query['q'] = $params['q'];
359
        $query = array_merge($query, $this->paginationQueryFromParams($params));
360
361
        $searchResponse = $this->get('search', ['query' => $query]);
362
363
        foreach ($searchResponse['items'] as $item) {
364
            $videoIds[] = $item['id']['videoId'];
365
        }
366
367
368
        // Get videos from video IDs
369
370
        if ($videoIds !== []) {
371
372
            $query = [];
373
            $query['part'] = 'snippet,statistics,contentDetails';
374
            $query['id'] = implode(',', $videoIds);
375
376
            $videosResponse = $this->get('videos', ['query' => $query]);
377
378
            $videos = $this->parseVideos($videosResponse['items']);
379
380
            return array_merge([
381
                'videos' => $videos,
382
            ], $this->paginationResponse($searchResponse, $videos));
383
        }
384
385
        return [];
386
    }
387
388
    /**
389
     * Returns a list of uploaded videos.
390
     *
391
     * @param array $params
392
     *
393
     * @return array
394
     * @throws \dukt\videos\errors\ApiResponseException
395
     */
396
    protected function getVideosUploads(array $params = []): array
397
    {
398
        $uploadsPlaylistId = $this->getSpecialPlaylistId('uploads');
399
400
        if (!$uploadsPlaylistId) {
401
            return [];
402
        }
403
404
405
        // Retrieve video IDs
406
407
        $query = [];
408
        $query['part'] = 'id,snippet';
409
        $query['playlistId'] = $uploadsPlaylistId;
410
        $query = array_merge($query, $this->paginationQueryFromParams($params));
411
412
        $playlistItemsResponse = $this->get('playlistItems', ['query' => $query]);
413
414
        $videoIds = [];
415
416
        foreach ($playlistItemsResponse['items'] as $item) {
417
            $videoId = $item['snippet']['resourceId']['videoId'];
418
            $videoIds[] = $videoId;
419
        }
420
421
422
        // Retrieve videos from video IDs
423
424
        $query = [];
425
        $query['part'] = 'snippet,statistics,contentDetails,status';
426
        $query['id'] = implode(',', $videoIds);
427
428
        $videosResponse = $this->get('videos', ['query' => $query]);
429
430
        $videos = $this->parseVideos($videosResponse['items']);
431
432
        return array_merge([
433
            'videos' => $videos,
434
        ], $this->paginationResponse($playlistItemsResponse, $videos));
435
    }
436
437
    // Private Methods
438
    // =========================================================================
439
440
    /**
441
     * @return string
442
     */
443
    private function getApiUrl(): string
444
    {
445
        return 'https://www.googleapis.com/youtube/v3/';
446
    }
447
448
    /**
449
     * @return array
450
     * @throws \dukt\videos\errors\ApiResponseException
451
     */
452
    private function getCollectionsPlaylists(): array
453
    {
454
        $data = $this->get('playlists', [
455
            'query' => [
456
                'part' => 'snippet',
457
                'mine' => 'true',
458
                'maxResults' => 50,
459
            ]
460
        ]);
461
462
        return $this->parseCollections($data['items']);
463
    }
464
465
    /**
466
     * @return null|mixed
467
     * @throws \dukt\videos\errors\ApiResponseException
468
     */
469
    private function getSpecialPlaylists()
470
    {
471
        $channelsQuery = [
472
            'part' => 'contentDetails',
473
            'mine' => 'true'
474
        ];
475
476
        $channelsResponse = $this->get('channels', ['query' => $channelsQuery]);
477
478
        if (isset($channelsResponse['items'][0])) {
479
            $channel = $channelsResponse['items'][0];
480
481
            return $channel['contentDetails']['relatedPlaylists'];
482
        }
483
484
        return null;
485
    }
486
487
    /**
488
     * Retrieves playlist ID for special playlists of type: likes, favorites, uploads.
489
     *
490
     * @param string $type
491
     *
492
     * @return null|mixed
493
     * @throws \dukt\videos\errors\ApiResponseException
494
     */
495
    private function getSpecialPlaylistId(string $type)
496
    {
497
        $specialPlaylists = $this->getSpecialPlaylists();
498
499
        if (isset($specialPlaylists[$type])) {
500
            return $specialPlaylists[$type];
501
        }
502
503
        return null;
504
    }
505
506
    /**
507
     * @param array $params
508
     *
509
     * @return array
510
     */
511
    private function paginationQueryFromParams(array $params = []): array
512
    {
513
        // Pagination
514
515
        $pagination = [
516
            'page' => 1,
517
            'perPage' => $this->getVideosPerPage(),
518
            'moreToken' => false
519
        ];
520
521
        if (!empty($params['perPage'])) {
522
            $pagination['perPage'] = $params['perPage'];
523
        }
524
525
        if (!empty($params['moreToken'])) {
526
            $pagination['moreToken'] = $params['moreToken'];
527
        }
528
529
530
        // Query
531
532
        $query = [];
533
        $query['maxResults'] = $pagination['perPage'];
534
535
        if (!empty($pagination['moreToken'])) {
536
            $query['pageToken'] = $pagination['moreToken'];
537
        }
538
539
        return $query;
540
    }
541
542
    /**
543
     * @param $response
544
     * @param $videos
545
     *
546
     * @return array
547
     */
548
    private function paginationResponse($response, $videos): array
549
    {
550
        $more = false;
551
552
        if (!empty($response['nextPageToken']) && (is_array($videos) || $videos instanceof \Countable ? \count($videos) : 0) > 0) {
553
            $more = true;
554
        }
555
556
        return [
557
            'prevPage' => $response['prevPageToken'] ?? null,
558
            'moreToken' => $response['nextPageToken'] ?? null,
559
            'more' => $more
560
        ];
561
    }
562
563
    /**
564
     * @param $item
565
     *
566
     * @return array
567
     */
568
    private function parseCollection($item): array
569
    {
570
        $collection = [];
571
        $collection['id'] = $item['id'];
572
        $collection['title'] = $item['snippet']['title'];
573
        $collection['totalVideos'] = 0;
574
        $collection['url'] = 'title';
575
576
        return $collection;
577
    }
578
579
    /**
580
     * @param $items
581
     *
582
     * @return array
583
     */
584
    private function parseCollections($items): array
585
    {
586
        $collections = [];
587
588
        foreach ($items as $item) {
589
            $collection = $this->parseCollection($item);
590
            $collections[] = $collection;
591
        }
592
593
        return $collections;
594
    }
595
596
    /**
597
     * @param $data
598
     *
599
     * @return Video
600
     * @throws \Exception
601
     */
602
    private function parseVideo($data): Video
603
    {
604
        $video = new Video;
605
        $video->raw = $data;
606
        $video->authorName = $data['snippet']['channelTitle'];
607
        $video->authorUrl = 'http://youtube.com/channel/' . $data['snippet']['channelId'];
608
        $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 DateTime|null 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...
609
        $video->description = $data['snippet']['description'];
610
        $video->gatewayHandle = 'youtube';
611
        $video->gatewayName = 'YouTube';
612
        $video->id = $data['id'];
613
        $video->plays = $data['statistics']['viewCount'];
614
        $video->title = $data['snippet']['title'];
615
        $video->url = 'http://youtu.be/' . $video->id;
616
617
        // Video Duration
618
        $interval = new \DateInterval($data['contentDetails']['duration']);
619
        $video->durationSeconds = (int)date_create('@0')->add($interval)->getTimestamp();
620
        $video->duration8601 = $data['contentDetails']['duration'];
621
622
        // Thumbnails
623
        $video->thumbnailSource = $this->getThumbnailSource($data['snippet']['thumbnails']);
624
625
        // Privacy
626
        if (!empty($data['status']['privacyStatus']) && $data['status']['privacyStatus'] === 'private') {
627
            $video->private = true;
628
        }
629
630
        return $video;
631
    }
632
633
    /**
634
     * Get the thumbnail source.
635
     *
636
     * @param array $thumbnails
637
     * @return null|string
638
     */
639
    private function getThumbnailSource(array $thumbnails)
640
    {
641
        if (!isset($thumbnails['maxres'])) {
642
            return $this->getLargestThumbnail($thumbnails);
643
        }
644
645
        return $thumbnails['maxres']['url'];
646
    }
647
648
    /**
649
     * Get the largest thumbnail from an array of thumbnails.
650
     *
651
     * @param array $thumbnails
652
     * @return null|string
653
     */
654
    private function getLargestThumbnail(array $thumbnails): ?string
655
    {
656
        $largestSize = 0;
657
        $largestThumbnail = null;
658
659
        foreach ($thumbnails as $thumbnail) {
660
            if ($thumbnail['width'] > $largestSize) {
661
                // Set thumbnail source with the largest thumbnail
662
                $largestThumbnail = $thumbnail['url'];
663
                $largestSize = $thumbnail['width'];
664
            }
665
        }
666
667
        return $largestThumbnail;
668
    }
669
670
    /**
671
     * @param $data
672
     *
673
     * @return array
674
     * @throws \Exception
675
     */
676
    private function parseVideos($data): array
677
    {
678
        $videos = [];
679
680
        foreach ($data as $videoData) {
681
            $video = $this->parseVideo($videoData);
682
            $videos[] = $video;
683
        }
684
685
        return $videos;
686
    }
687
}
688