PluginProbe
FV Player 8 / 8.1
FV Player 8 v8.1
trunk 8.0.18 8.0.19 8.0.20 8.0.21 8.0.25 8.0.27 8.1 8.1.3
fv-player / models / stats.php

stats.php in FV Player 8 8.1, at models/stats.php

1,741 lines 62.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class FV_Player_Stats {
8
9 var $used = false;
10 var $cache_directory = false;
11
12 public function __construct() {
13 global $fv_fp;
14 $this->cache_directory = WP_CONTENT_DIR."/fv-player-tracking";
15
16 add_action( 'admin_init', array( $this, 'register_meta_boxes' ), 9 );
17
18 add_filter( 'fv_flowplayer_conf', array( $this, 'option' ) );
19
20 add_filter( 'fv_flowplayer_attributes', array( $this, 'shortcode' ), 10, 3 );
21
22 if ( function_exists('wp_next_scheduled') ) {
23 if( !wp_next_scheduled( 'fv_player_stats' ) && $fv_fp->_get_option('video_stats_enable')) {
24 wp_schedule_event( time(), '5minutes', 'fv_player_stats' );
25 } else if( wp_next_scheduled( 'fv_player_stats' ) && !$fv_fp->_get_option('video_stats_enable') ) {
26 wp_clear_scheduled_hook( 'fv_player_stats' );
27 }
28 }
29
30 add_action( 'fv_player_stats', array ( $this, 'parse_cached_files' ) );
31
32 add_action( 'fv_player_update', array( $this, 'db_init' ) );
33
34 // add_action( 'admin_init', array( $this, 'db_init' ) );
35
36 add_action( 'admin_init', array( $this, 'folder_init' ) );
37
38 add_action( 'admin_menu', array( $this, 'stats_link' ), 13 );
39
40 add_filter( 'manage_users_columns', array( $this, 'users_column' ) );
41 add_filter( 'manage_users_custom_column', array( $this, 'users_column_content' ), 10, 3 );
42 add_filter( 'manage_users_sortable_columns', array( $this, 'users_sortable_columns' ) );
43
44 if( is_admin() ) {
45 add_action( 'pre_user_query', array( $this, 'users_sort' ) );
46 add_action( 'wp_ajax_fv_player_stats_users_search', array( $this, 'user_stats_search' ) );
47 }
48
49 }
50
51 function stats_link() {
52 global $fv_fp;
53 if ( $fv_fp->_get_option('video_stats_enable') ) {
54 add_submenu_page( 'fv_player', 'FV Player Stats', 'Stats', 'manage_options', 'fv_player_stats', 'fv_player_stats_page' );
55 add_submenu_page( 'fv_player', 'FV Player User Stats', 'User Stats', 'manage_options', 'fv_player_stats_users', 'fv_player_stats_page' );
56 }
57 }
58
59 function get_stat_columns() {
60 return array( 'play', 'seconds', 'click' );
61 }
62
63 public static function get_table_name() {
64 global $wpdb;
65 return $wpdb->prefix . 'fv_player_stats';
66 }
67
68 function db_init( $force = false ) {
69 global $fv_fp;
70
71 if( !$force && !$fv_fp->_get_option('video_stats_enable') ) {
72 return;
73 }
74
75 global $wpdb;
76 $table_name = $this->get_table_name();
77
78 $sql = "CREATE TABLE `$table_name` (
79 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
80 `id_video` INT(11) NOT NULL,
81 `id_player` INT(11) NOT NULL,
82 `id_post` INT(11) NOT NULL,
83 `user_id` INT(11) NOT NULL,
84 `guest_user_id` INT(11) NOT NULL,
85 `date` DATE NULL DEFAULT NULL,\n";
86
87 foreach( $this->get_stat_columns() AS $column ) {
88 $sql .= "`".$column."` INT(11) NOT NULL,\n";
89 }
90
91 $sql .= "PRIMARY KEY (`id`),
92 INDEX `date` (`date`),
93 INDEX `id_video` (`id_video`),
94 INDEX `id_player` (`id_player`),
95 INDEX `id_post` (`id_post`),
96 INDEX `user_id` (`user_id`),
97 INDEX `guest_user_id` (`guest_user_id`)
98 ) " . $wpdb->get_charset_collate() . ";";
99
100 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
101
102 dbDelta($sql);
103 }
104
105 function folder_init( $force = false ) {
106 if ( !WP_Filesystem() ) {
107 return;
108 }
109
110 global $fv_fp;
111 global $wp_filesystem;
112
113 if( !$force && !$fv_fp->_get_option('video_stats_enable') ) {
114 if( $wp_filesystem->exists( $this->cache_directory ) ) {
115 $wp_filesystem->rmdir( $this->cache_directory, true );
116 }
117
118 return;
119 }
120
121 if( !$wp_filesystem->exists($this->cache_directory) ){
122 $wp_filesystem->mkdir( $this->cache_directory );
123 }
124 }
125
126 function option( $conf ) {
127 global $fv_fp, $blog_id;
128 if( $this->used || $fv_fp->_get_option('js-everywhere') || $fv_fp->_get_option('video_stats_enable') ) { // we want to enable the tracking if it's used, if FV Player JS is enabled globally or if the tracking is enabled globally
129 $conf['fv_stats'] = array(
130 'url' => flowplayer::get_plugin_url().'/controller/track.php',
131 'blog_id' => $blog_id,
132 'user_id' => get_current_user_id(),
133 'nonce' => wp_create_nonce( 'fv_player_track' ),
134 );
135 if( $fv_fp->_get_option('video_stats_enable') ) $conf['fv_stats']['enabled'] = true;
136 }
137
138 return $conf;
139 }
140
141 function register_meta_boxes() {
142 add_meta_box( 'fv_player_stats' , 'Video Stats', array( $this, 'options_html' ), 'fv_flowplayer_settings', 'normal', 'low' );
143 }
144
145 function options_html() {
146 global $fv_fp;
147 ?>
148 <p><?php esc_html_e( 'Track user activity on your site. Users who can edit the post are excluded. You can see the stats in the FV Player menu.', 'fv-player' ); ?></p>
149 <table class="form-table2">
150 <?php
151 $fv_fp->_get_checkbox(__( 'Enable', 'fv-player' ), 'video_stats_enable', __('Gives you a daily count of video plays.'), __('Uses a simple PHP script with a cron job to make sure these stats don\'t slow down your server too much.'));
152 $fv_fp->_get_checkbox(__( 'Track Guest User IDs', 'fv-player' ), 'video_stats_enable_guest', __('Uses cookies to remember non-logged in users returning to website. Leave disabled to only get summary stats for all non-logged in users.'), '');
153 ?>
154 <tr>
155 <td colspan="4">
156 <a class="fv-wordpress-flowplayer-save button button-primary" href="#"><?php esc_html_e( 'Save', 'fv-player' ); ?></a>
157 <a class="button fv-help-link" href="https://foliovision.com/player/analytics/user-stats" target="_blank">Help</a>
158 </td>
159 </tr>
160 </table>
161 <?php
162 }
163
164 function shortcode( $attributes, $media, $fv_fp ) {
165
166 if( ! empty( $fv_fp->aCurArgs['stats'] ) || $fv_fp->_get_option('video_stats_enable') ) {
167 global $post;
168
169 // Do not track if user can edit the post
170 if ( ! empty( $post->ID ) ) {
171 if (
172 current_user_can( 'edit_others_posts' ) ||
173 ! empty( $post->post_author ) && absint( $post->post_author ) == get_current_user_id()
174 // TODO: Also check the FV Player player author
175 ) {
176 return $attributes;
177 }
178 }
179
180 if ( ! empty( $fv_fp->aCurArgs['stats'] ) && $fv_fp->aCurArgs['stats'] != 'no' ) {
181 $this->used = true;
182 }
183
184 if( !empty($fv_fp->aCurArgs['stats']) ) {
185 $attributes['data-fv_stats'] = $fv_fp->aCurArgs['stats'];
186 }
187
188 $player_id = 0; // 0 if shortcode
189
190 if( $fv_fp->current_player() ) {
191 $player_id = $fv_fp->current_player()->getId();
192 }
193
194 if( !empty($post->ID ) ) {
195 // TODO: Add signature to avoid faking the stats by users
196 $attributes['data-fv_stats_data'] = wp_json_encode( array(
197 'player_id' => $player_id,
198 'post_id' => $post->ID,
199 ) );
200 }
201 }
202
203 return $attributes;
204 }
205
206 /**
207 * Process post counters from cache file and update post meta
208 * @param resource &$fp file handler
209 * @param string $type Type of stats being parsed
210 * @return void
211 */
212 function process_cached_data( &$fp, $type ) {
213 global $wpdb;
214
215 $table_name = $this->get_table_name();
216
217 if( !in_array($type, $this->get_stat_columns() ) ) return;
218
219 if( flock( $fp, LOCK_EX ) ) {
220 $encoded_data = fgets( $fp );
221 $data = json_decode( $encoded_data, true );
222
223 ftruncate( $fp, 0 );
224 //UNLOCK, process data later
225 flock( $fp, LOCK_UN );
226
227 $json_error = json_last_error();
228 if( $json_error !== JSON_ERROR_NONE ) {
229 //file_put_contents( ABSPATH . 'failed_json_decode.log', gmdate('r')."\n".var_export( array( 'err' => $json_error, 'data' => $encoded_data ), true )."\n", FILE_APPEND );
230 return;
231 }
232
233 if( !is_array( $data ) || empty( $data ) )
234 return;
235
236 if( is_array($data) ) {
237 foreach( $data AS $index => $item ) {
238 $video_id = intval($item['video_id']);
239 $player_id = intval($item['player_id']);
240 $post_id = intval($item['post_id']);
241 $user_id = intval($item['user_id']);
242 $guest_user_id = intval($item['guest_user_id']);
243 $value = intval($item[$type]);
244
245 if( $user_id ) {
246 $meta_key = 'fv_player_stats_'.$type;
247 $meta_value = $value + intval( get_user_meta( $user_id, $meta_key, true ) );
248 if( $meta_value > 0 ) {
249 update_user_meta( $user_id, $meta_key, $meta_value );
250 }
251
252 }
253
254 if( $video_id ) {
255 global $FV_Player_Db;
256 $video = new FV_Player_Db_Video( $video_id, array(), $FV_Player_Db );
257
258 if( $video ) {
259 $meta_value = $value + intval($video->getMetaValue('stats_'.$type,true));
260 if( $meta_value > 0 ) {
261 $video->updateMetaValue( 'stats_'.$type, $meta_value );
262 }
263 }
264 }
265
266 $existing = $wpdb->get_row( $wpdb->prepare("SELECT * FROM `{$wpdb->prefix}fv_player_stats` WHERE date = %s AND id_video = %d AND id_post = %d AND id_player = %d AND user_id = %d AND guest_user_id = %d", date_i18n( 'Y-m-d' ), $video_id, $post_id, $player_id, $user_id, $guest_user_id ) );
267
268 if( $existing ) {
269 $wpdb->update(
270 $table_name,
271 array(
272 $type => $value + $existing->{$type}, // update plays in db
273 ),
274 array( 'id_video' => $video_id , 'date' => date_i18n( 'Y-m-d' ), 'id_player' => $player_id, 'id_post' => $post_id, 'user_id' => $user_id, 'guest_user_id' => $guest_user_id ), // update by video id, date, player id, post id, user ID and guest user ID
275 array(
276 '%d'
277 ),
278 array(
279 '%d',
280 '%s',
281 '%d',
282 '%d',
283 '%d'
284 )
285 );
286 } else { // insert new row
287 $wpdb->insert(
288 $table_name,
289 array(
290 'id_video' => $video_id,
291 'id_player' => $player_id,
292 'id_post' => $post_id,
293 'user_id' => $user_id,
294 'guest_user_id' => $guest_user_id,
295 'date' => date_i18n( 'Y-m-d' ),
296 $type => $value
297 ),
298 array(
299 '%d',
300 '%d',
301 '%d',
302 '%d',
303 '%d',
304 '%s',
305 '%d'
306 )
307 );
308 }
309 }
310 }
311 }
312 else {
313 echo "Error: failed to obtain file lock.";
314 }
315 }
316
317 /**
318 * Loads directory with cache files, and process those, which belongs to current blog
319 * @return void
320 */
321 function parse_cached_files() {
322 // just in case...
323 $this->db_init( true );
324 $this->folder_init( true );
325
326 $cache_files = scandir( $this->cache_directory );
327 foreach( $cache_files as $filename ) {
328 if( preg_match( '/^([^-]+)-([^\.]+)\.data$/', $filename, $matches ) ) {
329 $type = $matches[1];
330 if( !in_array($type, $this->get_stat_columns() ) ) continue;
331
332 $blog_id = intval($matches[2]);
333
334 if( get_current_blog_id() != $blog_id ) continue;
335
336 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
337 $fp = fopen( $this->cache_directory."/".$filename, 'r+');
338 $this->process_cached_data( $fp, $type );
339
340 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
341 fclose( $fp );
342 }
343 }
344 }
345
346 public function top_ten_users_by_plays( $interval, $user_type = 'user' ) {
347 global $wpdb;
348
349 $excluded = $this->get_posts_to_exclude();
350
351 $offset = 0;
352 $limit = 50000;
353 $grouped = array();
354
355 // Determine limit by the amount of PHP memory available
356 if ( intval( ini_get('memory_limit') ) > 32 ) {
357 $limit = intval( ini_get('memory_limit') ) * 800;
358 }
359
360 do {
361 if( $user_type == 'user' ) {
362 $results = $wpdb->get_results(
363 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
364 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
365 $wpdb->prepare(
366 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
367 "SELECT user_id, play FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT %d, %d",
368 array_merge(
369 array(
370 $interval[0],
371 $interval[1]
372 ),
373 $excluded['values'],
374 array(
375 $offset,
376 $limit
377 )
378 )
379 )
380 );
381
382 } else {
383 $results = $wpdb->get_results(
384 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
385 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
386 $wpdb->prepare(
387 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
388 "SELECT guest_user_id AS user_id, play FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND guest_user_id > 0 LIMIT %d, %d",
389 array_merge(
390 array(
391 $interval[0],
392 $interval[1]
393 ),
394 $excluded['values'],
395 array(
396 $offset,
397 $limit
398 )
399 )
400 )
401 );
402 }
403
404 // Group by user ID and sum up the plays, it's faster in PHP than MySQL.
405 if ( ! empty( $results ) ) {
406 foreach( $results as $row ) {
407 $user_id = $row->user_id;
408 $grouped[ $user_id ] = isset( $grouped[ $user_id ] ) ? $grouped[ $user_id ] + $row->play : $row->play;
409 }
410 }
411
412 $offset += $limit;
413
414 } while( ! empty( $results ) && count( $results ) >= $limit );
415
416 arsort( $grouped );
417
418 $grouped = array_slice( $grouped, 0, 10, true );
419
420 return array_keys( $grouped );
421 }
422
423 public function top_ten_users_by_watch_time( $interval, $user_type = 'user' ) {
424 global $wpdb;
425
426 $excluded = $this->get_posts_to_exclude();
427
428 $offset = 0;
429 $limit = 50000;
430 $grouped = array();
431
432 // Determine limit by the amount of PHP memory available
433 if ( intval( ini_get('memory_limit') ) > 32 ) {
434 $limit = intval( ini_get('memory_limit') ) * 800;
435 }
436
437 do {
438 if( $user_type == 'user' ) {
439 $results = $wpdb->get_results(
440 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
441 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
442 $wpdb->prepare(
443 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
444 "SELECT user_id, seconds FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT %d, %d",
445 array_merge(
446 array(
447 $interval[0],
448 $interval[1]
449 ),
450 $excluded['values'],
451 array(
452 $offset,
453 $limit
454 )
455 )
456 )
457 );
458
459 } else {
460 $results = $wpdb->get_results(
461 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
462 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
463 $wpdb->prepare(
464 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
465 "SELECT guest_user_id AS user_id, seconds FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND guest_user_id > 0 LIMIT %d, %d",
466 array_merge(
467 array(
468 $interval[0],
469 $interval[1]
470 ),
471 $excluded['values'],
472 array(
473 $offset,
474 $limit
475 )
476 )
477 )
478 );
479 }
480
481 // Group by user ID and sum up the plays, it's faster in PHP than MySQL.
482 if ( ! empty( $results ) ) {
483 foreach( $results as $row ) {
484 $user_id = $row->user_id;
485 $grouped[ $user_id ] = isset( $grouped[ $user_id ] ) ? $grouped[ $user_id ] + $row->seconds : $row->seconds;
486 }
487 }
488
489 $offset += $limit;
490
491 } while( ! empty( $results ) && count( $results ) >= $limit );
492
493 arsort( $grouped );
494
495 $grouped = array_slice( $grouped, 0, 10, true );
496
497 return array_keys( $grouped );
498 }
499
500 public function top_ten_videos_or_posts_by_plays( $type, $interval, $user_id ) {
501 global $wpdb;
502
503 // Sanitize input for SQL
504 if ( ! in_array( $type, array( 'post', 'video' ) ) ) {
505 $type = 'video';
506 }
507
508 $excluded = $this->get_posts_to_exclude();
509
510 if( is_numeric( $user_id ) ) {
511 $results = $wpdb->get_col(
512 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
513 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
514 $wpdb->prepare(
515 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
516 "SELECT id_" . esc_sql( $type ) . " FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d GROUP BY id_" . esc_sql( $type ) . " ORDER BY sum(play) DESC LIMIT 10",
517 array_merge(
518 array(
519 $interval[0],
520 $interval[1]
521 ),
522 $excluded['values'],
523 array(
524 $user_id
525 )
526 )
527 )
528 );
529
530 } else {
531 $results = $wpdb->get_col(
532 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
533 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
534 $wpdb->prepare(
535 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
536 "SELECT id_" . esc_sql( $type ) . " FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY id_" . esc_sql( $type ) . " ORDER BY sum(play) DESC LIMIT 10",
537 array_merge(
538 array(
539 $interval[0],
540 $interval[1]
541 ),
542 $excluded['values']
543 )
544 )
545 );
546 }
547
548 return $results;
549 }
550
551 public function top_ten_videos_by_watch_time( $interval, $user_id ) {
552 global $wpdb;
553
554 $valid_interval = $this->check_watch_time_in_interval( $interval, $user_id );
555
556 if( !$valid_interval ) {
557 return false;
558 }
559
560 $excluded = $this->get_posts_to_exclude();
561
562 if( is_numeric( $user_id ) ) {
563 $results = $wpdb->get_col(
564 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
565 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
566 $wpdb->prepare(
567 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
568 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d GROUP BY id_video ORDER BY sum(seconds) DESC LIMIT 10",
569 array_merge(
570 array(
571 $interval[0],
572 $interval[1]
573 ),
574 $excluded['values'],
575 array(
576 $user_id
577 )
578 )
579 )
580 );
581
582 } else {
583 $results = $wpdb->get_col(
584 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
585 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
586 $wpdb->prepare(
587 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
588 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY id_video ORDER BY sum(seconds) DESC LIMIT 10",
589 array_merge(
590 array(
591 $interval[0],
592 $interval[1]
593 ),
594 $excluded['values']
595 )
596 )
597 );
598 }
599
600 return $results;
601 }
602
603 public function get_video_ad_video_ids( $interval ) {
604 global $wpdb;
605
606 $excluded = $this->get_posts_to_exclude();
607
608 $results = $wpdb->get_col(
609 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
610 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
611 $wpdb->prepare(
612 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
613 "SELECT s.id_video as id_video FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videometa` AS m ON m.id_video = s.id_video WHERE m.meta_key = 'is_video_ad' AND date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY id_video",
614 array_merge(
615 array(
616 $interval[0],
617 $interval[1]
618 ),
619 $excluded['values']
620 )
621 )
622 );
623
624 return $results;
625 }
626
627 public function check_watch_time_in_interval( $interval, $user_id ) {
628 global $wpdb;
629
630 $excluded = $this->get_posts_to_exclude();
631
632 if( is_numeric( $user_id ) ) {
633 $results = $wpdb->get_col(
634 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
635 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
636 $wpdb->prepare(
637 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
638 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d AND seconds > 0 LIMIT 1",
639 array_merge(
640 array(
641 $interval[0],
642 $interval[1]
643 ),
644 $excluded['values'],
645 array(
646 $user_id
647 )
648 )
649 )
650 );
651
652 } else {
653 $results = $wpdb->get_col(
654 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
655 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
656 $wpdb->prepare(
657 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
658 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND seconds > 0 LIMIT 1",
659 array_merge(
660 array(
661 $interval[0],
662 $interval[1]
663 ),
664 $excluded['values']
665 )
666 )
667 );
668 }
669
670 return !empty($results);
671 }
672
673 /**
674 * Get post IDs to exclude for stats
675 *
676 * @return array Array of post IDs with 0 value always included to make sure the query SQL is valid
677 */
678 public function get_posts_to_exclude() {
679
680 // exclude posts with filter
681 $exclude_posts_query_args = apply_filters( 'fv_player_stats_view_exclude_posts_query_args', false );
682 if( $exclude_posts_query_args ) {
683 $exclude_posts_query = new WP_Query( $exclude_posts_query_args );
684 if( !empty($exclude_posts_query->posts) ) {
685 // We count +1 for the 0 value
686 $placeholders = implode( ', ', array_fill( 0, count( $exclude_posts_query->posts ) + 1, '%d' ) );
687 }
688
689 return array(
690 'placeholder' => $placeholders,
691 // We append the 0 value too
692 'values' => array_merge( array( 0 ), wp_list_pluck( $exclude_posts_query->posts, 'ID' ) ),
693 );
694
695 }
696
697 // No posts to exclude? We still return the 0 post ID
698 return array(
699 'placeholder' => '%d',
700 'values' => array( 0 ),
701 );
702 }
703
704 public function get_top_user_stats( $metric, $range ) {
705 global $wpdb, $fv_fp;
706
707 // dynamic interval based on range
708 $interval = self::get_interval_from_range( $range );
709
710 $guest_stats = $fv_fp->_get_option('video_stats_enable_guest');
711
712 $datasets = false;
713 $top_ids_user = array();
714 $top_ids_arr_user = array();
715 $top_ids_guest = array();
716 $top_ids_arr_guest = array();
717 $top_ids_results_user = array();
718 $top_ids_results_guest = array();
719 $results_user = array();
720 $results_guest = array();
721 $datasets_users = array();
722 $datasets_guests = array();
723
724 if( $metric == 'play' ) { // play stats
725 $top_ids_results_user = $this->top_ten_users_by_plays( $interval, 'user' );
726 if( $guest_stats ) $top_ids_results_guest = $this->top_ten_users_by_plays( $interval, 'guest' );
727 } else { // watch time stats
728 $top_ids_results_user = $this->top_ten_users_by_watch_time( $interval, 'user' );
729 if( $guest_stats ) $top_ids_results_guest = $this->top_ten_users_by_watch_time( $interval, 'guest' );
730 }
731
732 // if both empty, return false
733 if ( empty( $top_ids_results_user ) && empty( $top_ids_results_guest ) ) {
734 return false;
735 }
736
737 // regular users
738 if( !empty($top_ids_results_user) ) {
739 $top_ids_arr_user = array_values( $top_ids_results_user );
740 $top_ids_user = array_map( 'intval', array_values( $top_ids_arr_user ) );
741
742 $placeholders = implode( ', ', array_fill( 0, count( $top_ids_user ), '%d' ) );
743
744 if( $metric == 'play' ) {
745 $results_user = $wpdb->get_results(
746 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
747 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
748 $wpdb->prepare(
749 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
750 "SELECT date, user_id, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND user_id IN( $placeholders ) GROUP BY user_id, date",
751 array_merge(
752 array(
753 $interval[0],
754 $interval[1]
755 ),
756 $top_ids_user
757 )
758 ),
759 ARRAY_A
760 );
761
762 } else {
763 $results_user = $wpdb->get_results(
764 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
765 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
766 $wpdb->prepare(
767 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
768 "SELECT date, user_id, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND user_id IN( $placeholders ) GROUP BY user_id, date",
769 array_merge(
770 array(
771 $interval[0],
772 $interval[1]
773 ),
774 $top_ids_user
775 )
776 ),
777 ARRAY_A
778 );
779 }
780 }
781
782 // guest users
783 if( $guest_stats && !empty($top_ids_results_guest) ) {
784 // TODO: Fix if empty, the SQL below will fail
785 $top_ids_arr_guest = array_values( $top_ids_results_guest );
786 $top_ids_guest = array_map( 'intval', array_values( $top_ids_arr_guest ) );
787
788 $placeholders = implode( ', ', array_fill( 0, count( $top_ids_guest ), '%d' ) );
789
790 if( $metric == 'play' ) {
791 $results_guest = $wpdb->get_results(
792 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
793 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
794 $wpdb->prepare(
795 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
796 "SELECT date, guest_user_id, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND guest_user_id IN( $placeholders ) GROUP BY guest_user_id, date",
797 array_merge(
798 array(
799 $interval[0],
800 $interval[1]
801 ),
802 $top_ids_guest
803 )
804 ),
805 ARRAY_A
806 );
807
808 } else {
809 $results_guest = $wpdb->get_results(
810 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
811 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
812 $wpdb->prepare(
813 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
814 "SELECT date, guest_user_id, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND guest_user_id IN( $placeholders ) GROUP BY guest_user_id, date",
815 array_merge(
816 array(
817 $interval[0],
818 $interval[1]
819 ),
820 $top_ids_guest
821 )
822 ),
823 ARRAY_A
824 );
825 }
826 }
827
828 // process data for regular users
829 if( !empty($results_user) ) {
830 $datasets_users = $this->process_graph_data( $results_user, $top_ids_arr_user, $range, 'user', $metric );
831 }
832
833 // process data for guest users
834 if( !empty($results_guest) ) {
835 $datasets_guests = $this->process_graph_data( $results_guest, $top_ids_arr_guest, $range, 'guest', $metric );
836 }
837
838 // merge datasets
839 $datasets = array_merge( $datasets_users, $datasets_guests );
840
841 return $datasets;
842 }
843
844 public function get_top_video_watch_time_stats( $range, $user_id ) {
845 global $wpdb;
846
847 // dynamic interval based on range
848 $interval = self::get_interval_from_range( $range );
849
850 $type = 'video';
851 $datasets = false;
852
853 $top_ids_results = $this->top_ten_videos_by_watch_time( $interval, $user_id ); // get top video ids
854
855 if( !empty($top_ids_results) ) {
856 $top_ids = array_map( 'intval', array_values( $top_ids_results ) );
857 $top_ids[] = 0; // add 0 to make sure the SQL is valid
858 $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) );
859 } else {
860 return false;
861 }
862
863 if( is_numeric( $user_id ) ) {
864 $results = $wpdb->get_results(
865 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
866 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
867 $wpdb->prepare(
868 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
869 "SELECT date, id_player, id_video, title, src, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) AND user_id = %d GROUP BY id_video, date",
870 array_merge(
871 array(
872 $interval[0],
873 $interval[1]
874 ),
875 $top_ids,
876 array(
877 $user_id
878 )
879 )
880 ),
881 ARRAY_A
882 );
883
884 } else {
885 $results = $wpdb->get_results(
886 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
887 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
888 $wpdb->prepare(
889 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
890 "SELECT date, id_player, id_video, title, src, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) GROUP BY id_video, date",
891 array_merge(
892 array(
893 $interval[0],
894 $interval[1]
895 ),
896 $top_ids
897 )
898 ),
899 ARRAY_A
900 );
901 }
902
903 if( !empty($results) ) {
904 $datasets = $this->process_graph_data( $results, $top_ids, $range, $type, 'seconds' );
905 }
906
907 return $datasets;
908 }
909
910 public function get_top_video_post_stats( $type, $range, $user_id ) {
911 global $wpdb;
912
913 // dynamic interval based on range
914 $interval = self::get_interval_from_range( $range );
915
916 $datasets = false;
917 $top_ids_results = $this->top_ten_videos_or_posts_by_plays( $type, $interval, $user_id ); // get top video ids
918
919 if( !empty($top_ids_results) ) {
920 $top_ids = array_map( 'intval', array_values( $top_ids_results ) );
921 $top_ids[] = 0; // add 0 to make sure the SQL is valid
922 $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) );
923 } else {
924 return false;
925 }
926
927 if( is_numeric( $user_id ) ) {
928 if( $type == 'video' ) { // video stats
929 $results = $wpdb->get_results(
930 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
931 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
932 $wpdb->prepare(
933 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
934 "SELECT date, id_player, id_video, title, src, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) AND user_id = %d GROUP BY id_video, date",
935 array_merge(
936 array(
937 $interval[0],
938 $interval[1]
939 ),
940 $top_ids,
941 array(
942 $user_id
943 )
944 )
945 ),
946 ARRAY_A
947 );
948 } else if( $type == 'post' ) { // post stats
949 $results = $wpdb->get_results(
950 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
951 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
952 $wpdb->prepare(
953 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
954 "SELECT date, id_post, id_video, post_title, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}posts` AS p ON s.id_post = p.ID WHERE date BETWEEN %s AND %s AND id_post IN( $placeholders ) AND user_id = %d GROUP BY id_post, date",
955 array_merge(
956 array(
957 $interval[0],
958 $interval[1]
959 ),
960 $top_ids,
961 array(
962 $user_id
963 )
964 )
965 ),
966 ARRAY_A
967 );
968 }
969
970 } else {
971 if( $type == 'video' ) { // video stats
972 $results = $wpdb->get_results(
973 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
974 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
975 $wpdb->prepare(
976 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
977 "SELECT date, id_player, id_video, title, src, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) GROUP BY id_video, date",
978 array_merge(
979 array(
980 $interval[0],
981 $interval[1]
982 ),
983 $top_ids
984 )
985 ),
986 ARRAY_A
987 );
988 } else if( $type == 'post' ) { // post stats
989 $results = $wpdb->get_results(
990 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
991 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
992 $wpdb->prepare(
993 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
994 "SELECT date, id_post, id_video, post_title, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}posts` AS p ON s.id_post = p.ID WHERE date BETWEEN %s AND %s AND id_post IN( $placeholders ) GROUP BY id_post, date",
995 array_merge(
996 array(
997 $interval[0],
998 $interval[1]
999 ),
1000 $top_ids
1001 )
1002 ),
1003 ARRAY_A
1004 );
1005 }
1006 }
1007
1008 if( !empty($results) ) {
1009 $datasets = $this->process_graph_data( $results, $top_ids, $range, $type );
1010 }
1011
1012 return $datasets;
1013 }
1014
1015 public function get_top_video_ad_data( $range, $metric ) {
1016 global $wpdb;
1017
1018 // dynamic interval based on range
1019 $interval = self::get_interval_from_range( $range );
1020
1021 $datasets = false;
1022
1023 // we track ads based on video
1024 $type = 'video';
1025
1026 $top_ids_results = $this->get_video_ad_video_ids( $interval );
1027
1028 if( !empty($top_ids_results) ) {
1029 $top_ids = array_map( 'intval', array_values( $top_ids_results ) );
1030 $top_ids[] = 0; // add 0 to make sure the SQL is valid
1031 $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) );
1032 } else {
1033 return false;
1034 }
1035
1036 $results = $wpdb->get_results(
1037 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
1038 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1039 $wpdb->prepare(
1040 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1041 "SELECT date, id_player, id_video, title, src, SUM($metric) AS {$metric} FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) GROUP BY id_video, date",
1042 array_merge(
1043 array(
1044 $interval[0],
1045 $interval[1]
1046 ),
1047 $top_ids
1048 )
1049 ),
1050 ARRAY_A
1051 );
1052
1053 if( !empty($results) ) {
1054 $datasets = $this->process_graph_data( $results, $top_ids, $range, $type, $metric );
1055 }
1056
1057 return $datasets;
1058 }
1059
1060 public function get_player_stats( $player_id, $range) {
1061 global $wpdb;
1062
1063 $interval = self::get_interval_from_range( $range );
1064 $datasets = false;
1065
1066 $results = $wpdb->get_results(
1067 $wpdb->prepare(
1068 "SELECT date, id_video, src, title, player_name, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_players` AS p ON s.id_player = p.id JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND s.id_player IN( %d ) GROUP BY date, id_video",
1069 $interval[0],
1070 $interval[1],
1071 $player_id
1072 ),
1073 ARRAY_A
1074 );
1075
1076 if( !empty($results) ) {
1077 $ids_arr = array();
1078 foreach( $results as $row ) {
1079 $ids_arr[] = $row['id_video'];
1080 }
1081
1082 // Make sure each video is only considered once, otherwise this ends up multiplying the stats is loading for one player only
1083 $ids_arr = array_unique( $ids_arr );
1084
1085 $datasets = $this->process_graph_data( $results, $ids_arr, $range, 'video' );
1086 }
1087
1088 return $datasets;
1089 }
1090
1091 public function get_users_by_time_range( $range, $user_id = false ) {
1092 global $wpdb;
1093
1094 $excluded = $this->get_posts_to_exclude();
1095 $interval = self::get_interval_from_range( $range );
1096
1097 if( $user_id ) {
1098 $result = $wpdb->get_results(
1099 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1100 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1101 $wpdb->prepare(
1102 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1103 "SELECT u.ID, display_name, user_email, SUM( play ) AS play FROM `{$wpdb->users}` AS u LEFT JOIN `{$wpdb->prefix}fv_player_stats` AS s ON u.ID = s.user_id AND date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) WHERE u.ID = %d GROUP BY u.ID ORDER BY display_name",
1104 array_merge(
1105 array(
1106 $interval[0],
1107 $interval[1]
1108 ),
1109 $excluded['values'],
1110 array(
1111 $user_id
1112 )
1113 )
1114 ),
1115 ARRAY_A
1116 );
1117
1118 } else {
1119 $result = $wpdb->get_results(
1120 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1121 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1122 $wpdb->prepare(
1123 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1124 "SELECT u.ID, display_name, user_email, SUM( play ) AS play FROM `{$wpdb->users}` AS u LEFT JOIN `{$wpdb->prefix}fv_player_stats` AS s ON u.ID = s.user_id AND date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY u.ID ORDER BY display_name",
1125 array_merge(
1126 array(
1127 $interval[0],
1128 $interval[1]
1129 ),
1130 $excluded['values']
1131 )
1132 ),
1133 ARRAY_A
1134 );
1135 }
1136
1137 if ( ! $result ) {
1138 $result = array();
1139 }
1140
1141 return $result;
1142 }
1143
1144 public function get_valid_dates( $user_id ) {
1145 global $wpdb;
1146
1147 $excluded = $this->get_posts_to_exclude();
1148
1149 $dates_all = array( 'this_week' => 'This Week', 'last_week' => 'Last Week', 'this_month' => 'This Month', 'last_month' => 'Last Month' );
1150 $years = $this->get_all_years();
1151 $dates_all = $dates_all + $years; // merge
1152 $dates_valid = array();
1153
1154 $this_year = (int) gmdate( 'Y' );
1155 $last_year = $this_year - 1;
1156
1157 foreach( $dates_all as $key => $value ) {
1158
1159 $interval = self::get_interval_from_range( $key );
1160
1161 if( $user_id ) {
1162 $result = $wpdb->get_results(
1163 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1164 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1165 $wpdb->prepare(
1166 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1167 "SELECT date FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d LIMIT 1",
1168 array_merge(
1169 array(
1170 $interval[0],
1171 $interval[1]
1172 ),
1173 $excluded['values'],
1174 array(
1175 $user_id
1176 )
1177 )
1178 ),
1179 ARRAY_A
1180 );
1181
1182 } else {
1183 $result = $wpdb->get_results(
1184 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1185 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1186 $wpdb->prepare(
1187 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1188 "SELECT date FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT 1",
1189 array_merge(
1190 array(
1191 $interval[0],
1192 $interval[1]
1193 ),
1194 $excluded['values']
1195 )
1196 ),
1197 ARRAY_A
1198 );
1199 }
1200
1201 if( $key == $this_year) {
1202 $key = 'this_year';
1203 $value = 'This Year';
1204 } else if( $key == $last_year ) {
1205 $key = 'last_year';
1206 $value = 'Last Year';
1207 }
1208
1209 $dates_valid[$key] = array();
1210
1211 if( !empty($result) ) {
1212 $dates_valid[$key]['disabled'] = false;
1213 } else {
1214 $dates_valid[$key]['disabled'] = true;
1215 }
1216
1217 $dates_valid[$key]['value'] = $value;
1218 }
1219
1220 return $dates_valid;
1221 }
1222
1223 public function get_valid_interval( $user_id ) {
1224 // we need to check every interval for user to check if there is any data
1225 $intervals = array(
1226 'this_week',
1227 'last_week',
1228 'this_month',
1229 'last_month',
1230 );
1231
1232 $years = $this->get_all_years();
1233
1234 $intervals = $intervals + $years; // merge
1235
1236 // TODO: optimize performance, no need to use SUM or ORDER BY, limit 1 would be enough
1237 foreach( $intervals as $k => $interval ) {
1238 $data = $this->get_top_video_watch_time_stats( $interval, $user_id );
1239
1240 // if there is no data for this interval, remove it from the list
1241 if( empty($data) ) {
1242 unset($intervals[$k]);
1243 }
1244
1245 }
1246
1247 return $intervals;
1248 }
1249
1250 public static function get_interval_from_range( $range ) {
1251
1252 if( strcmp( 'this_week', $range ) === 0 ) { // this week
1253 $start = gmdate('Y-m-d', strtotime('-7 days') );
1254 $end = gmdate('Y-m-d', time() );
1255
1256 } else if( strcmp( 'last_week', $range ) === 0 ) { // last week
1257 $previous_week = strtotime("-1 week +1 day");
1258
1259 // convert to datetime
1260 $previous_week = gmdate('Y-m-d', $previous_week);
1261
1262 // respect the start of week day by wordpress
1263 $start_end_week = get_weekstartend($previous_week);
1264
1265 $start = gmdate('Y-m-d', $start_end_week['start']);
1266 $end = gmdate('Y-m-d', $start_end_week['end']);
1267
1268 } else if( strcmp( 'this_month', $range ) === 0 ) { // this month
1269 $start = gmdate('Y-m-01');
1270 $end = gmdate('Y-m-t');
1271
1272 } else if( strcmp( 'last_month', $range ) === 0 ) { // last month
1273 $first_day_last_month = strtotime('first day of last month');
1274 $last_day_last_month = strtotime('last day of last month');
1275
1276 $start = gmdate('Y-m-01', $first_day_last_month );
1277 $end = gmdate('Y-m-t', $last_day_last_month );
1278
1279 } else if( strcmp( 'this_year', $range ) === 0 ) { // this year
1280 $start = gmdate('Y-01-01');
1281 $end = gmdate('Y-12-31');
1282
1283 } else if( strcmp( 'last_year', $range ) === 0 ) { // last year
1284 $start = gmdate('Y-01-01', strtotime('-1 year'));
1285 $end = gmdate('Y-12-31', strtotime('-1 year'));
1286
1287 } else if( is_numeric($range)) { // specific year like 2021
1288 $start = intval( $range ) . '-01-01';
1289 $end = intval( $range ) . '-12-31';
1290 }
1291
1292 return array( $start, $end);
1293 }
1294
1295 /**
1296 * Get the desired date range
1297 *
1298 * @param string|int $range this_week, last_week, this_month, last_month, this_year, last_year or year number
1299 * @param mixed $base_date (optional) The base date to use for this_week
1300 * @return array All the days in the date range in YYYY-MM-DD format.
1301 */
1302 private function get_dates_in_range( $range, $base_date = false ) {
1303 $dates = array();
1304
1305 $time = time();
1306 if ( $base_date ) {
1307 $time = strtotime( $base_date );
1308 }
1309
1310 if( strcmp( 'this_week', $range ) === 0 ) {
1311 $end_day = gmdate('Y-m-d', $time );
1312 $start_day = gmdate('Y-m-d', strtotime( '-7 days', $time ) );
1313 $dates = $this->get_days_between_dates( $start_day, $end_day );
1314 } else if( strcmp( 'last_week', $range ) === 0 ) {
1315 $previous_week = strtotime("-1 week +1 day");
1316
1317 // convert to datetime
1318 $previous_week = gmdate('Y-m-d', $previous_week);
1319
1320 // respect the start of week day by wordpress
1321 $start_end_week = get_weekstartend($previous_week);
1322
1323 $start_week = gmdate('Y-m-d', $start_end_week['start']);
1324 $end_week = gmdate('Y-m-d', $start_end_week['end']);
1325
1326 $dates = $this->get_days_between_dates( $start_week, $end_week );
1327 } else if( strcmp( 'this_month', $range ) === 0 ) {
1328 $start_day = gmdate('Y-m-01');
1329 $end_day = gmdate('Y-m-d');
1330 $dates = $this->get_days_between_dates( $start_day, $end_day );
1331 } else if( strcmp( 'last_month', $range ) === 0 ) {
1332 $first_day_last_month = strtotime('first day of last month');
1333 $last_day_last_month = strtotime('last day of last month');
1334
1335 $start_day = gmdate('Y-m-01', $first_day_last_month );
1336 $end_day = gmdate('Y-m-t', $last_day_last_month );
1337
1338 $dates = $this->get_days_between_dates( $start_day, $end_day );
1339 } else if( strcmp( 'this_year', $range ) === 0 ) {
1340 $start_day = gmdate('Y-01-01');
1341 $end_day = gmdate('Y-m-d');
1342 $dates = $this->get_days_between_dates( $start_day, $end_day );
1343 } else if( strcmp( 'last_year', $range ) === 0 ) {
1344 $start_day = gmdate('Y-01-01', strtotime('-1 year'));
1345 $end_day = gmdate('Y-12-31', strtotime('-1 year'));
1346 $dates = $this->get_days_between_dates( $start_day, $end_day );
1347 } else if( is_numeric($range) ) { // get dates for specific year like 2021
1348 $start_day = intval( $range ) . '-01-01';
1349 $end_day = intval( $range ) . '-12-31';
1350 $dates = $this->get_days_between_dates( $start_day, $end_day );
1351 }
1352
1353 return $dates;
1354 }
1355
1356 function get_all_years() {
1357 global $wpdb;
1358
1359 $years = array();
1360
1361 $oldest_year = (int) $wpdb->get_var("SELECT YEAR(date) FROM {$wpdb->prefix}fv_player_stats ORDER BY id ASC LIMIT 1");
1362
1363 // add every year from oldest to current, when oldest is 2021 and current is 2025, it will add 2021, 2022, 2023, 2024, 2025
1364 for( $i = $oldest_year; $i <= gmdate('Y'); $i++ ) {
1365 $j = strval($i);
1366 $years[$j] = $j;
1367 }
1368
1369 // reorder years from newest to oldest
1370 $years = array_reverse( $years, true );
1371
1372 return $years;
1373 }
1374
1375 private function get_days_between_dates( $start_day, $end_day ) {
1376 $dates = array();
1377
1378 $current = strtotime($start_day);
1379 $end = strtotime($end_day);
1380
1381 while( $current <= $end ) {
1382 $dates[] = gmdate('Y-m-d', $current);
1383 $current = strtotime('+1 day', $current);
1384 }
1385
1386 return $dates;
1387 }
1388
1389 private function get_date_labels( $results ) {
1390 $date_labels = array();
1391
1392 foreach( $results as $row) {
1393 if( !in_array( $row['date'], $date_labels ) ) {
1394 $date_labels[strtotime($row['date'])] = $row['date'];
1395 }
1396 }
1397
1398 ksort($date_labels);
1399
1400 return array_values($date_labels);
1401 }
1402
1403 /**
1404 * Group the database result rows by the video or post ID for the desired date range.
1405 *
1406 * @param array $raw_db_results Each item is array like:
1407 * array(
1408 * 'date' => '2024-09-03',
1409 * 'id_player' => '14',
1410 * 'id_video' => '912',
1411 * 'title' => 'My Video',
1412 * 'play' => '1',
1413 * ),
1414 * array(
1415 * 'date' => '2024-09-05',
1416 * 'id_player' => '171',
1417 * 'id_video' => '912',
1418 * 'title' => 'My Video',
1419 * 'play' => '1',
1420 * ),
1421 * array(
1422 * 'date' => '2024-09-07',
1423 * 'id_player' => '14',
1424 * 'id_video' => '912',
1425 * 'title' => 'My Video',
1426 * 'play' => '1',
1427 * )
1428 *
1429 * @param mixed $top_ids_arr
1430 * @param string|int $range this_week, last_week, this_month, last_month, this_year, last_year or year number
1431 * @param string $type video or post
1432 * @param string $metric play or seconds or clicks
1433 * @param string $base_date (optional) The base date to use for $range
1434 *
1435 * @return array Summary of the daily video plays per video or post (see $type) by id_video or is_post:
1436 * 912 => array(
1437 * '2024-09-02' => array( 'play' => 0 ),
1438 * 'name' => 'My Video',
1439 * '2024-09-03' => array( 'play' => '1' ),
1440 * '2024-09-04' => array( 'play' => 0 ),
1441 * '2024-09-05' => array( 'play' => 1 ),
1442 * '2024-09-06' => array( 'play' => 0 ),
1443 * '2024-09-07' => array( 'play' => 1 ),
1444 * '2024-09-08' => array( 'play' => 0 ),
1445 * '2024-09-09' => array( 'play' => 0 ),
1446 * ),
1447 */
1448 private function process_graph_data( $raw_db_results, $top_ids_arr, $range, $type, $metric = 'play', $base_date = false ) {
1449 $datasets = array();
1450
1451 $date_labels = $this->get_dates_in_range( $range, $base_date );
1452
1453 // order data for graph,
1454 foreach( $top_ids_arr as $id ) {
1455 foreach( $date_labels as $date ) {
1456 foreach( $raw_db_results as $row) {
1457 if( ( ( $type == 'video' || $type == 'player' ) && ( isset($row['id_' . $type ]) && $row['id_' . $type ] == $id ) ) || ( isset($row['user_id']) && $row['user_id'] == $id ) || ( isset($row['guest_user_id']) && $row['guest_user_id'] == $id ) || ( isset($row['id_post']) && $row['id_post'] == $id ) ) {
1458 if( !isset($datasets[$id]) ) {
1459 $datasets[$id] = array();
1460 }
1461
1462 // aggregate data by date
1463 if( strcmp( $date, $row['date'] ) == 0 ) { // date row exists
1464 if( $metric === 'play' && isset($row['play']) ) {
1465 if( isset($datasets[$id][$date]['play']) ) {
1466 $datasets[$id][$date]['play'] += $row['play'];
1467 } else {
1468 $datasets[$id][$date]['play'] = $row['play'];
1469 }
1470 }
1471
1472 if( $metric === 'seconds' && isset($row['seconds']) ) {
1473 if( isset($datasets[$id][$date]['seconds']) ) {
1474 $datasets[$id][$date]['seconds'] += $row['seconds'];
1475 } else {
1476 $datasets[$id][$date]['seconds'] = (int) $row['seconds'];
1477 }
1478 }
1479
1480 if( $metric === 'click' && isset($row['click']) ) {
1481 if( isset($datasets[$id][$date]['click']) ) {
1482 $datasets[$id][$date]['click'] += $row['click'];
1483 } else {
1484 $datasets[$id][$date]['click'] = $row['click'];
1485 }
1486 }
1487
1488 } else { // date row dont exists, add 0 plays/seconds - dont overwrite if value already set
1489 if( $metric === 'play' && !isset( $datasets[$id][$date]['play']) ) $datasets[$id][$date]['play'] = 0;
1490 if( $metric === 'seconds' && !isset( $datasets[$id][$date]['seconds']) ) $datasets[$id][$date]['seconds'] = 0;
1491 if( $metric === 'click' && !isset( $datasets[$id][$date]['click']) ) $datasets[$id][$date]['click'] = 0;
1492 }
1493
1494 // add labels
1495 if( !isset($datasets[$id]['name']) ) {
1496 if( $type == 'video' || $type == 'player' ) {
1497 $datasets[$id]['name'] = $this->get_video_name( $row );
1498 } else if( $type == 'post' ) {
1499 $datasets[$id]['name'] = !empty($row['post_title'] ) ? $row['post_title'] : 'id_post_' . $row['id_post'] ;
1500 } else if( $type == 'user' ) {
1501 $user_data = get_userdata( intval($row['user_id']) );
1502
1503 if( $user_data === false ) {
1504 $datasets[$id]['name'] = 'Guest Users';
1505 } else {
1506 $datasets[$id]['name'] = $user_data->display_name;
1507 }
1508 } else if( $type == 'guest') {
1509 $datasets[$id]['name'] = 'Guest ' . $row['guest_user_id'];
1510 }
1511 }
1512 }
1513 }
1514 }
1515 }
1516
1517 $datasets['date-labels'] = $date_labels; // date will be used as X axis label
1518
1519 return $datasets;
1520 }
1521
1522 function get_video_name( $row ) {
1523 if( ! empty( $row['title'] ) ) {
1524 return $row['title'];
1525 }
1526
1527 $src = $row['src'];
1528
1529 // check if youtube
1530 if( FV_Player_YouTube()->is_youtube( $src ) ) {
1531 // get youtube id
1532 preg_match( '/[\\?\\&]v=([^\\?\\&]+)/', $src, $matches );
1533 if( isset($matches[1]) ) {
1534 $id = $matches[1];
1535 $name = 'Youtube: ' . $id;
1536
1537 return $name;
1538 }
1539 }
1540
1541 // check if vimeo
1542 if( function_exists('FV_Player_Pro_Vimeo') && FV_Player_Pro_Vimeo()->is_vimeo($src) ) {
1543 // get vimeo id
1544 preg_match( '/vimeo\.com\/([0-9]+)/', $src, $matches );
1545 if( isset($matches[1]) ) {
1546 $id = $matches[1];
1547 $name = 'Vimeo: ' . $id;
1548
1549 return $name;
1550 }
1551
1552 }
1553
1554 // parse title
1555 $name = flowplayer::get_title_from_src($src);
1556
1557 return $name;
1558 }
1559
1560 function users_column( $columns ) {
1561 global $fv_fp;
1562 if ( $fv_fp->_get_option('video_stats_enable') ) {
1563 $columns['fv_player_stats_user_play_today'] = "Video Plays Today";
1564 $columns['fv_player_stats_user_seconds_today'] = "Video Minutes Today";
1565 }
1566 return $columns;
1567 }
1568
1569 function users_column_content( $content, $column_name, $user_id ) {
1570 $field = false;
1571
1572 if ( 'fv_player_stats_user_play_today' === $column_name ) {
1573 $field = 'play';
1574 } else if ( 'fv_player_stats_user_seconds_today' === $column_name ) {
1575 $field = 'seconds';
1576 }
1577
1578 if( $field ) {
1579
1580 // TODO: Preload to avoid too many SQL queries
1581 global $wpdb;
1582
1583 if ( 'play' === $field ) {
1584 $val = $wpdb->get_var(
1585 $wpdb->prepare(
1586 "SELECT sum(play) FROM {$wpdb->prefix}fv_player_stats WHERE user_id = %d AND date = %s",
1587 $user_id,
1588 date_i18n( 'Y-m-d' )
1589 )
1590 );
1591 } else if ( 'seconds' === $field ) {
1592 $val = $wpdb->get_var(
1593 $wpdb->prepare(
1594 "SELECT sum(seconds) FROM {$wpdb->prefix}fv_player_stats WHERE user_id = %d AND date = %s",
1595 $user_id,
1596 date_i18n( 'Y-m-d' )
1597 )
1598 );
1599 }
1600
1601 if ( $val ) {
1602
1603 if( 'seconds' === $field ) {
1604 $val = ceil($val/60) . ' min';
1605 }
1606
1607 $url = add_query_arg(
1608 array(
1609 'page' => 'fv_player_stats_users',
1610 'user_id' => $user_id
1611 ),
1612 admin_url( 'admin.php' )
1613 );
1614 $content = '<a href="' . $url . '">' . $val . '</a>';
1615 }
1616 }
1617
1618 return $content;
1619 }
1620
1621 function users_sortable_columns( $columns ) {
1622 $columns['fv_player_stats_user_play_today'] = 'fv_player_stats_user_play_today';
1623 $columns['fv_player_stats_user_seconds_today'] = 'fv_player_stats_user_seconds_today';
1624 return $columns;
1625 }
1626
1627 function users_sort($userquery) {
1628 global $wpdb;
1629
1630 $field = false;
1631
1632 if ( 'fv_player_stats_user_play_today' === $userquery->query_vars['orderby'] ) {
1633 $field = 'play';
1634 } else if ( 'fv_player_stats_user_seconds_today' === $userquery->query_vars['orderby'] ) {
1635 $field = 'seconds';
1636 }
1637
1638 if ( $field ) {
1639 $userquery->query_fields .= ", sum(" . $field . ") AS " . $field . " ";
1640 $userquery->query_from .= " LEFT OUTER JOIN {$wpdb->prefix}fv_player_stats AS stats ON ($wpdb->users.ID = stats.user_id) ";
1641 $userquery->query_where .= " AND stats.date = '" . date_i18n( 'Y-m-d' ) . "' ";
1642 $userquery->query_orderby = " GROUP BY wp_users.ID ORDER BY " . $field . " ".($userquery->query_vars["order"] == "ASC" ? "ASC " : "DESC ");
1643 }
1644 }
1645
1646 function user_stats_search() {
1647 if( isset($_GET['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'fv-player-stats-users-search' ) && isset($_GET['q']) && isset($_GET['date_range']) ) {
1648 $search = sanitize_text_field( $_GET['q'] );
1649 $date_range = sanitize_text_field( $_GET['date_range'] );
1650
1651 // search for users by login, nicename or email
1652 $users = get_users( array(
1653 'search' => '*' . $search . '*',
1654 'search_columns' => array( 'user_login', 'display_name' ,'user_nicename', 'user_email' ),
1655 ) );
1656
1657 $results = array();
1658 foreach( $users AS $user ) {
1659 $data = $this->get_users_by_time_range( $date_range, $user->ID ); // check if user has any data in the selected date range
1660
1661 if( $data ) {
1662 $plays = $data[0]['play'] ? $data[0]['play'] : 0;
1663
1664 $item = array(
1665 'id' => $user->ID, // used as value for option
1666 'text' => $user->display_name . '-' . $user->user_email . ' ( ' . number_format_i18n( $plays, 0) . ' plays )' // used as label for option
1667 );
1668
1669 if( !$plays ) {
1670 $item['disabled'] = true; // disable option if user has no data in the selected date range
1671 }
1672
1673 $results[] = $item;
1674 }
1675 }
1676
1677 echo wp_json_encode( array( 'results' => $results ) );
1678 }
1679
1680 die();
1681 }
1682
1683 }
1684
1685 global $FV_Player_Stats;
1686 $FV_Player_Stats = new FV_Player_Stats();
1687
1688 function fv_player_stats_top( $args = array() ) {
1689 $args = wp_parse_args( $args, array(
1690 'taxonomy' => false,
1691 'term' => false ) );
1692
1693 extract($args);
1694
1695 global $wpdb;
1696
1697 if( $taxonomy && $term ) {
1698 $raw = $wpdb->get_results(
1699 $wpdb->prepare("
1700 SELECT p.id, vm.id_video, vm.meta_value AS stats_play, pm.meta_value AS post_id
1701 FROM {$wpdb->prefix}fv_player_videometa AS vm
1702 JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) > 0
1703 JOIN {$wpdb->prefix}fv_player_playermeta AS pm ON p.id = pm.id_player
1704 INNER JOIN {$wpdb->prefix}term_relationships AS tr ON (pm.meta_value = tr.object_id)
1705 INNER JOIN {$wpdb->prefix}term_taxonomy AS tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)
1706 INNER JOIN {$wpdb->prefix}terms AS t ON (t.term_id = tt.term_id)
1707 WHERE vm.meta_key = 'stats_play'
1708 AND pm.meta_key = 'post_id'
1709 AND tt.taxonomy = %s
1710 AND t.name = %s
1711 ORDER BY CAST(vm.meta_value AS unsigned) DESC",
1712 $taxonomy,
1713 $term
1714 )
1715 );
1716
1717 } else {
1718 $raw = $wpdb->get_results( "
1719 SELECT p.id, vm.id_video, vm.meta_value AS stats_play, pm.meta_value AS post_id
1720 FROM {$wpdb->prefix}fv_player_videometa AS vm
1721 JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) > 0
1722 JOIN {$wpdb->prefix}fv_player_playermeta AS pm ON p.id = pm.id_player
1723 WHERE vm.meta_key = 'stats_play'
1724 AND pm.meta_key = 'post_id'
1725 ORDER BY CAST(vm.meta_value AS unsigned) DESC"
1726 );
1727 }
1728
1729 // sice there might be multiple players for a single post_id we count these together
1730 $top = array();
1731 foreach( $raw AS $record ) {
1732 if( empty($top[$record->post_id]) ) $top[$record->post_id] = 0;
1733 $top[$record->post_id] += $record->stats_play;
1734 }
1735
1736 asort($top);
1737 $top = array_reverse($top,true);
1738
1739 return $top;
1740 }
1741