· 6 years ago · Sep 05, 2019, 10:56 AM
1<?php
2
3/**
4 * Class Meks_Video_Importer_Youtube
5 *
6 * Works with Youtube API
7 *
8 * @since 1.0.0
9 */
10if (!class_exists('Meks_Video_Importer_Youtube')):
11 class Meks_Video_Importer_Youtube {
12
13 /**
14 * @var $response_body returned from youtube
15 * @since 1.0.0
16 */
17 private $response_body;
18
19 /**
20 * @var $second_response_body in order to compare embeddable with not embeddable videos we need to make two requests
21 * @since 1.0.3
22 */
23 private $second_response_body;
24
25 /**
26 * @var mixed|void default query arg for getting data from youtube
27 * @since 1.0.0
28 */
29 private $query_defaults;
30
31 /**
32 * Call this method to get singleton
33 *
34 * @return Meks_Video_Importer_Youtube
35 * @since 1.0.0
36 */
37 public static function getInstance() {
38 static $instance = null;
39 if (null === $instance) {
40 $instance = new static;
41 }
42
43 return $instance;
44 }
45
46 /**
47 * Meks_Video_Importer_Youtube constructor.
48 *
49 * @since 1.0.0
50 */
51 public function __construct() {
52
53 // Ajax
54 add_action('wp_ajax_mvi_fetch_from_youtube', array($this, 'ajax_fetch_from_youtube'));
55 add_action('wp_ajax_mvi_save_youtube_settings', array($this, 'ajax_save_settings'));
56
57 // Frontend
58 add_action('meks-video-importer-print-providers', array($this, 'print_options'));
59 add_action('meks-video-importer-settings', array($this, 'print_settings'));
60 add_action('admin_enqueue_scripts', array($this, 'localize_messages'), 99);
61 add_filter('meks-video-importer-valid-providers', array($this, 'are_credentials_valid'));
62
63 $this->query_defaults = apply_filters('meks-video-importer-youtube-default-query', array(
64 'part' => 'snippet',
65 'maxResults' => 50,
66 'key' => $this->get_api_key(),
67 ));
68 }
69
70 /**
71 * Localize error messages
72 */
73 public function localize_messages() {
74
75 wp_localize_script('meks-video-importer-script', 'meks_video_importer_youtube', array(
76 'empty_id_or_type' => __('Please select Type and fill the ID.', 'meks-video-importer'),
77 )
78 );
79 }
80
81 /**
82 * Get YouTube API key from options
83 *
84 * @return mixed|string
85 * @since 1.0.0
86 */
87 private function get_api_key() {
88 $youtube_apy_key = get_option('mvi-youtube-api-key');
89
90 return !empty($youtube_apy_key) ? $youtube_apy_key : '';
91 }
92
93 /**
94 * Save and verify YouTube settings
95 *
96 * @since 1.0.0
97 */
98 public function ajax_save_settings() {
99
100 if (isset($_POST['key'])) {
101
102 // Verify API credentials
103 if ($this->verify_credentials($_POST['key'])) {
104 update_option('mvi-youtube-api-key', $_POST['key']);
105 update_option('mvi-youtube-api-key-verified', 1);
106 wp_send_json_success(array('message' => '<span class="dashicons dashicons-yes"></span>' . __('Successfully verified.', 'meks-video-importer')));
107 }
108
109 }
110
111 delete_option('mvi-youtube-api-key-verified');
112 update_option('mvi-youtube-api-key', $_POST['key']);
113 wp_send_json_error(array('message' => '<span class="dashicons dashicons-no"></span>' . __('Credentials not verified. Please try adding it again', 'meks-video-importer')));
114
115 }
116
117 /**
118 * Verify Youtube Settings
119 */
120 public function verify_credentials($api_key) {
121
122 if(empty($api_key)){
123 return false;
124 }
125
126 $checkKeyQuery = $this->make_query(
127 array(
128 'type' => 'search',
129 'id' => 'YouTube+Data+API',
130 ),
131 array(
132 'type' => 'video',
133 'key' => $api_key,
134 )
135 );
136
137 return meks_video_importer_is_valid_response($checkKeyQuery);
138 }
139
140 /**
141 * Check if credentials are valid and verified. This is "meks-video-importer-redirect" filter callback.
142 *
143 * @param $redirect_array
144 * @return array
145 * @since 1.0.0
146 */
147 public function are_credentials_valid($redirect_array) {
148 $youtube_apy_key = get_option('mvi-youtube-api-key');
149 $youtube_apy_key_verified = get_option('mvi-youtube-api-key-verified');
150
151 if (!empty($youtube_apy_key) && !empty($youtube_apy_key_verified))
152 $redirect_array[] = 'youtube';
153
154 return $redirect_array;
155 }
156
157 /**
158 * Print options for importing posts
159 *
160 * @since 1.0.0
161 */
162 public function print_options() {
163 require_once plugin_dir_path(dirname(__FILE__)) . 'partials/youtube.php';
164 }
165
166 /**
167 * Print options for settings page
168 *
169 * @since 1.0.0
170 */
171 public function print_settings() {
172 require_once plugin_dir_path(dirname(__FILE__)) . 'partials/youtube-settings.php';
173 }
174
175 /**
176 * Ajax for fetching posts from youtube
177 *
178 * @since 1.0.0
179 */
180 public function ajax_fetch_from_youtube() {
181 if (!isset($_POST['type']) || empty($_POST['type']) || !isset($_POST['id']) || empty($_POST['id'])) {
182 wp_send_json_error(array('message' => __('Invalid request', 'meks-video-importer')));
183 }
184
185 $this->make_request();
186
187 $table = new Meks_Video_Importer_List_Table();
188 $table->set_items($this->format_for_table_display());
189
190 wp_send_json_success(array('res' => $this->response_body, 'table' => $table->display()));
191 }
192
193 /**
194 * Main function for making request towards youtube
195 *
196 * @since 1.0.2
197 */
198 private function make_request() {
199 $response = $this->make_query();
200
201 if (!meks_video_importer_is_valid_response($response)) {
202 $this->respond_with_error_message($response);
203 }
204
205 $this->response_body = json_decode($response['body']);
206
207 if ($_POST['type'] != 'search') {
208 $this->recursive_fetch_paged_from_youtube($this->response_body);
209 }
210
211 $this->make_only_embeddable_request();
212
213 $this->intersect_videos();
214 }
215
216 /**
217 * Send human readable error message as ajax response
218 *
219 * @param $response
220 * @since 1.0.4
221 */
222 private function respond_with_error_message( $response ) {
223
224 if( empty($response) ){
225 wp_send_json_error(array('message' => 'Cannot retrieve videos from YouTube. Please make sure query parameters are correct.', 'meks-video-importer'));
226 }
227
228 if( is_wp_error($response) ){
229 wp_send_json_error(array('message' => $response->get_error_message()));
230 }
231
232 if( $response['response']['code'] < 200 || $response['response']['code'] >= 400 ){
233 wp_send_json_error(array('message' => 'Youtube returned ' . $response['response']['code'] . ' status code. Please make sure query parameters are correct.', 'meks-video-importer'));
234 }
235
236
237 wp_send_json_error(array('message' => __('Cannot retrieve videos from YouTube. Please make sure query parameters are correct.', 'meks-video-importer')));
238 }
239
240 /**
241 * Fetch only embeddable videos
242 * Note: This will not work in case of playlist query
243 *
244 * @return bool
245 * @since 1.0.4
246 */
247 private function make_only_embeddable_request() {
248 if($_POST['type'] == 'playlist'){
249 return false;
250 }
251
252 $embeddable_args = array(
253 'type' => 'video',
254 'videoEmbeddable' => 'true',
255 'videoSyndicated' => 'true',
256 'format'=> 5,
257 );
258
259 $second_response = $this->make_query(array(), $embeddable_args);
260
261 if (!meks_video_importer_is_valid_response($second_response)) {
262 return false;
263 }
264
265 $this->second_response_body = json_decode($second_response['body']);
266
267 if($_POST['type'] == 'search'){
268 return false;
269 }
270
271 $this->recursive_fetch_paged_from_youtube($this->second_response_body, $embeddable_args);
272 }
273
274 /**
275 * Intersect between embeddable and not embeddable videos
276 *
277 * @return mixed
278 * @since 1.0.4
279 */
280 private function intersect_videos() {
281
282 if(empty($this->response_body->items) || empty($this->second_response_body->items)){
283 return false;
284 }
285
286 $intersected = array();
287
288 foreach ( $this->response_body->items as $first_request_video ) {
289 $first_request_video->embeddable = false;
290
291 foreach ( $this->second_response_body->items as $second_request_video ) {
292 if($first_request_video->id == $second_request_video->id ){
293 $first_request_video->embeddable = true;
294 }
295 }
296
297 if(!$first_request_video->embeddable){
298 $first_request_video->mvi_message = '<p>' . __('This video is not embeddable', 'meks-video-importer') . '</p>';
299 }
300
301 $intersected[] = $first_request_video;
302 }
303
304 return $intersected;
305 }
306
307 /**
308 * Execute query to youtube
309 *
310 * @param array $args
311 * @param array $append_query
312 * @return array|WP_Error
313 * @since 1.0.0
314 */
315 private function make_query($args = array(), array $append_query = array()) {
316 if (empty($args)) {
317 $args = $_POST;
318 }
319
320 switch ($args['type']) {
321 case "search":
322 $url = $this->build_url(array(
323 'q' => $args['id'],
324 ), $append_query);
325 break;
326 case "channelId":
327 $url = $this->build_url(array(
328 'channelId' => $args['id'],
329 ), $append_query);
330 break;
331 case "userId":
332 $channelId = $this->get_channel_by_user_id($args['id']);
333
334 if (!$channelId) {
335 wp_send_json_error(array('message' => __('Channel not found.', 'meks-video-importer')));
336 }
337
338 $url = $this->build_url(array(
339 'channelId' => $channelId,
340 'order' => 'date',
341 ), $append_query);
342 break;
343 case "playlist":
344 default:
345 $url = $this->build_url(array(
346 'playlistId' => $args['id'],
347 'part' => 'snippet,status',
348 ), $append_query, 'playlistItems');
349 break;
350 }
351
352 return wp_remote_get($url);
353 }
354
355 public function get_single_video( $video_id ){
356 $url = $this->build_url( array('part' => 'snippet', 'id' => $video_id ) , array(), 'videos' );
357
358 $response = wp_remote_get( $url );
359
360 if (!meks_video_importer_is_valid_response($response)) {
361 return false;
362 }
363
364 $video = json_decode( $response['body'] );
365
366 if(!isset($video->items[0]->snippet)){
367 return false;
368 }
369
370 return $video->items[0]->snippet;
371
372 }
373
374 /**
375 * Helepr for building URL for fetching query
376 *
377 * @param $query_args
378 * @param array $append_query
379 * @param string $endpoint
380 * @return string
381 * @since 1.0.0
382 */
383 private function build_url($query_args, array $append_query = array(), $endpoint = 'search') {
384
385 $defaults = $this->query_defaults;
386 $query = meks_video_importer_parse_args($query_args, $defaults);
387
388 if (!empty($append_query)) {
389 $query = meks_video_importer_parse_args($append_query, $query);
390 }
391
392 return add_query_arg($query, 'https://www.googleapis.com/youtube/v3/' . $endpoint);
393 }
394
395 /**
396 * Helper for old type of YouTube Channels that have URL like "https://www.youtube.com/user/username"
397 * It executes query for getting channel id
398 *
399 * @param $id - Channel ID
400 * @return bool
401 * @since 1.0.0
402 */
403 private function get_channel_by_user_id($id) {
404
405 $userIdQueryArgs = array_merge(array(
406 'forUsername' => $id,
407 ), $this->query_defaults);
408
409 $userIdQueryArgs['part'] = 'id';
410
411 $userIdQuery = wp_remote_get('https://www.googleapis.com/youtube/v3/channels', array('body' => $userIdQueryArgs));
412
413 if (!meks_video_importer_is_valid_response($userIdQuery))
414 return false;
415
416 $userIdQueryJson = json_decode($userIdQuery['body']);
417
418 return !empty($userIdQueryJson->items[0]->id) ? $userIdQueryJson->items[0]->id : false;
419 }
420
421 /**
422 * Get all pages of playlist or channel
423 *
424 * @since 1.0.0
425 * @param $response_body
426 * @param array $additional_query_args
427 * @return boolean
428 */
429 private function recursive_fetch_paged_from_youtube(&$response_body, array $additional_query_args = array()) {
430 if (empty($response_body->nextPageToken)){
431 return false;
432 }
433
434 $query_args = array(
435 'pageToken' => $response_body->nextPageToken
436 );
437
438 if(!empty($additional_query_args)){
439 $query_args = array_merge($query_args, $additional_query_args);
440 }
441
442 $paged = $this->make_query(false, $query_args);
443 $paged_body = json_decode($paged['body']);
444 $response_body->items = array_merge($response_body->items, $paged_body->items);
445
446 if (!empty($paged_body->nextPageToken)) {
447 $response_body->nextPageToken = $paged_body->nextPageToken;
448 } else {
449 unset($response_body->nextPageToken);
450 return false;
451 }
452
453 $this->recursive_fetch_paged_from_youtube($response_body, $additional_query_args);
454 }
455
456 /**
457 * Format response data for displaying Table with videos
458 *
459 * @return array
460 * @since 1.0.0
461 */
462 private function format_for_table_display() {
463 $formatted = array();
464
465 foreach ($this->response_body->items as $video) {
466
467 if($video->snippet->resourceId->kind != 'youtube#video' && $video->id->kind != 'youtube#video'){
468 continue;
469 }
470
471 if(!empty($video->status->privacyStatus) && $video->status->privacyStatus === 'private'){
472 continue;
473 }
474
475 $formatted[] = array(
476 'id' => $this->get_video_id( $video ),
477 'image' => $video->snippet->thumbnails->high->url,
478 'image_max' => $this->get_largest_image( $video->snippet->thumbnails ),
479 'url' => 'https://www.youtube.com/watch?v=' . $this->get_video_id( $video ),
480 'date' => $video->snippet->publishedAt,
481 'title' => $video->snippet->title,
482 'description' => $video->snippet->description,
483 'message' => $video->mvi_message,
484 'embeddable' => $video->embeddable,
485 );
486 }
487
488 return $formatted;
489 }
490
491 /**
492 * Helper for getting video id
493 *
494 * @param $video
495 * @return null
496 * @since 1.0.0
497 */
498 private function get_video_id($video) {
499 if (!empty($video->snippet->resourceId->videoId)) {
500 return $video->snippet->resourceId->videoId;
501 }
502
503 if (!empty($video->id->videoId)) {
504 return $video->id->videoId;
505 }
506
507 return null;
508 }
509
510 /**
511 * Helper for getting biggest image available
512 *
513 * @param $thumbnails
514 * @return null
515 * @since 1.0.0
516 */
517 private function get_largest_image($thumbnails) {
518 $youtube_thumbnail_sizes = array(
519 'maxres', 'standard', 'high', 'medium', 'default',
520 );
521
522 foreach ($youtube_thumbnail_sizes as $youtube_thumbnail_size) {
523 if (!empty($thumbnails->{$youtube_thumbnail_size}->url)) {
524 return $thumbnails->{$youtube_thumbnail_size}->url;
525 }
526 }
527
528 return null;
529 }
530
531 /**
532 * Youtube options
533 *
534 * @return array
535 * @since 1.0.0
536 */
537 public function get_select_options() {
538 return apply_filters('meks-video-importer-youtube-select-options', array(
539 'playlist' => __("Playlist", 'meks-video-importer'),
540 'channelId' => __("Channel", 'meks-video-importer'),
541 'search' => __("Search", 'meks-video-importer'),
542 'userId' => __("User", 'meks-video-importer'),
543 ));
544 }
545
546 /**
547 * Get youtube options from saved template
548 *
549 * @return array
550 * @since 1.0.0
551 */
552 public function get_options_from_template() {
553 $defaults = array(
554 'mvi-youtube-type' => 'playlist',
555 'mvi-youtube-id' => '',
556 );
557
558 if (!isset($_GET['template']) || empty($_GET['template'])) {
559 return $defaults;
560 }
561
562 $template_options = Meks_Video_Importer_Saved_Templates::getInstance()->get_template($_GET['template']);
563
564 if (!empty($template_options)) {
565 return meks_video_importer_parse_args($template_options, $defaults);
566 }
567
568 return $defaults;
569 }
570
571 /**
572 * Get Youtube's API key with verified confirmation
573 *
574 * @return array
575 * @since 1.0.0
576 */
577 public function get_access_credentials() {
578 $defaults = array(
579 'mvi-youtube-api-key-verified' => false,
580 'mvi-youtube-api-key' => '',
581 );
582
583 $access_credentials = array(
584 'mvi-youtube-api-key-verified' => get_option('mvi-youtube-api-key-verified'),
585 'mvi-youtube-api-key' => get_option('mvi-youtube-api-key'),
586 );
587
588 if (!empty($access_credentials['mvi-youtube-api-key-verified'])) {
589 return meks_video_importer_parse_args($access_credentials, $defaults);
590 }
591
592 return $defaults;
593 }
594 }
595endif;