cache_directory = WP_CONTENT_DIR."/fv-player-tracking"; add_action( 'admin_init', array( $this, 'register_meta_boxes' ), 9 ); add_filter( 'fv_flowplayer_conf', array( $this, 'option' ) ); add_filter( 'fv_flowplayer_attributes', array( $this, 'shortcode' ), 10, 3 ); if ( function_exists('wp_next_scheduled') ) { if( !wp_next_scheduled( 'fv_player_stats' ) && $fv_fp->_get_option('video_stats_enable')) { wp_schedule_event( time(), '5minutes', 'fv_player_stats' ); } else if( wp_next_scheduled( 'fv_player_stats' ) && !$fv_fp->_get_option('video_stats_enable') ) { wp_clear_scheduled_hook( 'fv_player_stats' ); } } add_action( 'fv_player_stats', array ( $this, 'parse_cached_files_cron' ) ); add_action( 'fv_player_update', array( $this, 'db_init' ) ); // add_action( 'admin_init', array( $this, 'db_init' ) ); add_action( 'admin_init', array( $this, 'folder_init' ) ); add_action( 'admin_menu', array( $this, 'stats_link' ), 13 ); add_filter( 'manage_users_columns', array( $this, 'users_column' ) ); add_filter( 'manage_users_custom_column', array( $this, 'users_column_content' ), 10, 3 ); add_filter( 'manage_users_sortable_columns', array( $this, 'users_sortable_columns' ) ); if( is_admin() ) { add_action( 'pre_user_query', array( $this, 'users_sort' ) ); add_action( 'wp_ajax_fv_player_stats_users_search', array( $this, 'user_stats_search' ) ); } add_action( 'wp_ajax_fv_player_stats_test', array( $this, 'stats_test' ) ); } function stats_link() { global $fv_fp; if ( $fv_fp->_get_option('video_stats_enable') ) { add_submenu_page( 'fv_player', 'FV Player Stats', 'Stats', 'manage_options', 'fv_player_stats', 'fv_player_stats_page' ); add_submenu_page( 'fv_player', 'FV Player User Stats', 'User Stats', 'manage_options', 'fv_player_stats_users', 'fv_player_stats_page' ); } } function get_stat_columns() { return array( 'play', 'seconds', 'click' ); } public static function get_table_name() { global $wpdb; return $wpdb->prefix . 'fv_player_stats'; } function db_init( $force = false ) { global $fv_fp; if( !$force && !$fv_fp->_get_option('video_stats_enable') ) { return; } global $wpdb; $table_name = $this->get_table_name(); $sql = "CREATE TABLE `$table_name` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id_video` INT(11) NOT NULL, `id_player` INT(11) NOT NULL, `id_post` INT(11) NOT NULL, `user_id` INT(11) NOT NULL, `guest_user_id` INT(11) NOT NULL, `date` DATE NULL DEFAULT NULL,\n"; foreach( $this->get_stat_columns() AS $column ) { $sql .= "`".$column."` INT(11) NOT NULL,\n"; } $sql .= "PRIMARY KEY (`id`), INDEX `date` (`date`), INDEX `id_video` (`id_video`), INDEX `id_player` (`id_player`), INDEX `id_post` (`id_post`), INDEX `user_id` (`user_id`), INDEX `guest_user_id` (`guest_user_id`) ) " . $wpdb->get_charset_collate() . ";"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta($sql); } function folder_init( $force = false ) { if ( !WP_Filesystem() ) { return; } global $fv_fp; global $wp_filesystem; if( !$force && !$fv_fp->_get_option('video_stats_enable') ) { if( $wp_filesystem->exists( $this->cache_directory ) ) { $wp_filesystem->rmdir( $this->cache_directory, true ); } return; } if( !$wp_filesystem->exists($this->cache_directory) ){ $wp_filesystem->mkdir( $this->cache_directory ); } } function option( $conf ) { global $fv_fp, $blog_id; 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 $conf['fv_stats'] = array( 'url' => flowplayer::get_plugin_url().'/controller/track.php', 'blog_id' => $blog_id, 'user_id' => get_current_user_id(), 'nonce' => wp_create_nonce( 'fv_player_track' ), ); if( $fv_fp->_get_option('video_stats_enable') ) $conf['fv_stats']['enabled'] = true; } return $conf; } function register_meta_boxes() { add_meta_box( 'fv_player_stats' , 'Video Stats', array( $this, 'options_html' ), 'fv_flowplayer_settings', 'normal', 'low' ); } function options_html() { global $fv_fp; $video_stats_enabled = $fv_fp->_get_option('video_stats_enable'); ?>

_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.')); $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.'), ''); ?>
Help Test
aCurArgs['stats'] ) || $fv_fp->_get_option('video_stats_enable') ) { global $post; // Do not track if user can edit the post if ( ! empty( $post->ID ) ) { // Only check once for performance reasons static $user_can_edit_posts; if ( ! isset( $user_can_edit_posts ) ) { $user_can_edit_posts = current_user_can( 'edit_others_posts' ); } $current_user_is_post_author = ! empty( $post->post_author ) && absint( $post->post_author ) == get_current_user_id(); // TODO: Also check the FV Player player author if ( $user_can_edit_posts || $current_user_is_post_author ) { $skip_reason = $user_can_edit_posts ? 'User can edit all posts' : 'User is post author'; // Store reason for skipping to be able to show console warning if debug is enabled if ( $fv_fp->_get_option( 'debug_log' ) ) { $attributes['data-fv_stats_skip'] = $skip_reason; } // Query Monitor plugin integration do_action( 'qm/debug', 'Skip for player ' . $fv_fp->hash . ': ' . $skip_reason ); return $attributes; } } if ( ! empty( $fv_fp->aCurArgs['stats'] ) && $fv_fp->aCurArgs['stats'] != 'no' ) { $this->used = true; } if( !empty($fv_fp->aCurArgs['stats']) ) { $attributes['data-fv_stats'] = $fv_fp->aCurArgs['stats']; } $player_id = 0; // 0 if shortcode if( $fv_fp->current_player() ) { $player_id = $fv_fp->current_player()->getId(); } if( !empty($post->ID ) ) { // TODO: Add signature to avoid faking the stats by users $attributes['data-fv_stats_data'] = wp_json_encode( array( 'player_id' => $player_id, 'post_id' => $post->ID, ) ); } } return $attributes; } /** * Process post counters from cache file and update post meta * @param resource &$fp file handler * @param string $type Type of stats being parsed * @return void */ function process_cached_data( &$fp, $type ) { global $wpdb, $fv_fp; $table_name = $this->get_table_name(); if( !in_array($type, $this->get_stat_columns() ) ) return; if( flock( $fp, LOCK_EX ) ) { $encoded_data = fgets( $fp ); if ( ! $encoded_data ) { $fv_fp->log( "Stats Parsing: File empty." ); return; } $data = json_decode( $encoded_data, true ); ftruncate( $fp, 0 ); //UNLOCK, process data later flock( $fp, LOCK_UN ); $json_error = json_last_error(); if( $json_error !== JSON_ERROR_NONE ) { //file_put_contents( ABSPATH . 'failed_json_decode.log', gmdate('r')."\n".var_export( array( 'err' => $json_error, 'data' => $encoded_data ), true )."\n", FILE_APPEND ); $fv_fp->log( "Stats Parsing: JSON error: " . json_last_error_msg() ); return; } if( !is_array( $data ) || empty( $data ) ) { $fv_fp->log( "Stats Parsing: No data." ); return; } if( is_array($data) ) { foreach( $data AS $index => $item ) { $video_id = intval($item['video_id']); $player_id = intval($item['player_id']); $post_id = intval($item['post_id']); $user_id = intval($item['user_id']); $guest_user_id = intval($item['guest_user_id']); $value = intval($item[$type]); if( $user_id ) { $meta_key = 'fv_player_stats_'.$type; $meta_value = $value + intval( get_user_meta( $user_id, $meta_key, true ) ); if( $meta_value > 0 ) { update_user_meta( $user_id, $meta_key, $meta_value ); } } if( $video_id ) { global $FV_Player_Db; $video = new FV_Player_Db_Video( $video_id, array(), $FV_Player_Db ); if( $video ) { $meta_value = $value + intval($video->getMetaValue('stats_'.$type,true)); if( $meta_value > 0 ) { $video->updateMetaValue( 'stats_'.$type, $meta_value ); } } } $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', false, true ), $video_id, $post_id, $player_id, $user_id, $guest_user_id ) ); if( $existing ) { $fv_fp->log( "Stats Parsing: Updating stats for video #" . $video_id ); $wpdb->update( $table_name, array( $type => $value + $existing->{$type}, // update plays in db ), array( 'id_video' => $video_id , 'date' => date_i18n( 'Y-m-d', false, true ), '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 array( '%d' ), array( '%d', '%s', '%d', '%d', '%d' ) ); } else { // insert new row $fv_fp->log( "Stats Parsing: Inserting stats for video #" . $video_id ); $wpdb->insert( $table_name, array( 'id_video' => $video_id, 'id_player' => $player_id, 'id_post' => $post_id, 'user_id' => $user_id, 'guest_user_id' => $guest_user_id, 'date' => date_i18n( 'Y-m-d', false, true ), $type => $value ), array( '%d', '%d', '%d', '%d', '%d', '%s', '%d' ) ); } } } } else { echo "Error: failed to obtain file lock."; $fv_fp->log( "Stats Parsing: Failed to obtain file lock." ); } } /** * Loads directory with cache files, and process those, which belongs to current blog * @return void */ function parse_cached_files() { global $fv_fp; $fv_fp->log( "Stats Parsing: Starting..." ); // just in case... $this->db_init( true ); $this->folder_init( true ); $cache_files = scandir( $this->cache_directory ); foreach( $cache_files as $filename ) { if( preg_match( '/^([^-]+)-([^\.]+)\.data$/', $filename, $matches ) ) { $type = $matches[1]; if( !in_array($type, $this->get_stat_columns() ) ) continue; $blog_id = intval($matches[2]); if( get_current_blog_id() != $blog_id ) continue; $fv_fp->log( "Stats Parsing: Processing file: " . $filename ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen $fp = fopen( $this->cache_directory."/".$filename, 'r+'); $this->process_cached_data( $fp, $type ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose fclose( $fp ); } } $fv_fp->log( "Stats Parsing: Finished." ); } public function parse_cached_files_cron() { global $fv_fp; $fv_fp->log( "Stats Cron: Starting..." ); $this->parse_cached_files(); } public function top_ten_users_by_plays( $interval, $user_type = 'user' ) { global $wpdb; $excluded = $this->get_posts_to_exclude(); $offset = 0; $limit = 50000; $grouped = array(); // Determine limit by the amount of PHP memory available if ( intval( ini_get('memory_limit') ) > 32 ) { $limit = intval( ini_get('memory_limit') ) * 800; } do { if( $user_type == 'user' ) { $results = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $offset, $limit ) ) ) ); } else { $results = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $offset, $limit ) ) ) ); } // Group by user ID and sum up the plays, it's faster in PHP than MySQL. if ( ! empty( $results ) ) { foreach( $results as $row ) { $user_id = $row->user_id; $grouped[ $user_id ] = isset( $grouped[ $user_id ] ) ? $grouped[ $user_id ] + $row->play : $row->play; } } $offset += $limit; } while( ! empty( $results ) && count( $results ) >= $limit ); arsort( $grouped ); $grouped = array_slice( $grouped, 0, 10, true ); return array_keys( $grouped ); } public function top_ten_users_by_watch_time( $interval, $user_type = 'user' ) { global $wpdb; $excluded = $this->get_posts_to_exclude(); $offset = 0; $limit = 50000; $grouped = array(); // Determine limit by the amount of PHP memory available if ( intval( ini_get('memory_limit') ) > 32 ) { $limit = intval( ini_get('memory_limit') ) * 800; } do { if( $user_type == 'user' ) { $results = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $offset, $limit ) ) ) ); } else { $results = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $offset, $limit ) ) ) ); } // Group by user ID and sum up the plays, it's faster in PHP than MySQL. if ( ! empty( $results ) ) { foreach( $results as $row ) { $user_id = $row->user_id; $grouped[ $user_id ] = isset( $grouped[ $user_id ] ) ? $grouped[ $user_id ] + $row->seconds : $row->seconds; } } $offset += $limit; } while( ! empty( $results ) && count( $results ) >= $limit ); arsort( $grouped ); $grouped = array_slice( $grouped, 0, 10, true ); return array_keys( $grouped ); } public function top_ten_videos_or_posts_by_plays( $type, $interval, $user_id ) { global $wpdb; // Sanitize input for SQL if ( ! in_array( $type, array( 'post', 'video' ) ) ) { $type = 'video'; } $excluded = $this->get_posts_to_exclude(); if( is_numeric( $user_id ) ) { $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $user_id ) ) ) ); } else { $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'] ) ) ); } return $results; } public function top_ten_videos_by_watch_time( $type, $interval, $user_id ) { global $wpdb; // Sanitize input for SQL if ( ! in_array( $type, array( 'post', 'video' ) ) ) { $type = 'video'; } $valid_interval = $this->check_watch_time_in_interval( $interval, $user_id ); if( !$valid_interval ) { return false; } $excluded = $this->get_posts_to_exclude(); if( is_numeric( $user_id ) ) { $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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(seconds) DESC LIMIT 10", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $user_id ) ) ) ); } else { $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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(seconds) DESC LIMIT 10", array_merge( array( $interval[0], $interval[1] ), $excluded['values'] ) ) ); } return $results; } public function get_video_ad_video_ids( $interval ) { global $wpdb; $excluded = $this->get_posts_to_exclude(); $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'] ) ) ); return $results; } public function check_watch_time_in_interval( $interval, $user_id ) { global $wpdb; $excluded = $this->get_posts_to_exclude(); if( is_numeric( $user_id ) ) { $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $user_id ) ) ) ); } else { $results = $wpdb->get_col( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'] ) ) ); } return !empty($results); } /** * Get post IDs to exclude for stats * * @return array Array of post IDs with 0 value always included to make sure the query SQL is valid */ public function get_posts_to_exclude() { // exclude posts with filter $exclude_posts_query_args = apply_filters( 'fv_player_stats_view_exclude_posts_query_args', false ); if( $exclude_posts_query_args ) { $exclude_posts_query = new WP_Query( $exclude_posts_query_args ); if( !empty($exclude_posts_query->posts) ) { // We count +1 for the 0 value $placeholders = implode( ', ', array_fill( 0, count( $exclude_posts_query->posts ) + 1, '%d' ) ); } return array( 'placeholder' => $placeholders, // We append the 0 value too 'values' => array_merge( array( 0 ), wp_list_pluck( $exclude_posts_query->posts, 'ID' ) ), ); } // No posts to exclude? We still return the 0 post ID return array( 'placeholder' => '%d', 'values' => array( 0 ), ); } public function get_top_user_stats( $metric, $range ) { global $wpdb, $fv_fp; // dynamic interval based on range $interval = self::get_interval_from_range( $range ); $guest_stats = $fv_fp->_get_option('video_stats_enable_guest'); $datasets = false; $top_ids_user = array(); $top_ids_arr_user = array(); $top_ids_guest = array(); $top_ids_arr_guest = array(); $top_ids_results_user = array(); $top_ids_results_guest = array(); $results_user = array(); $results_guest = array(); $datasets_users = array(); $datasets_guests = array(); if( $metric == 'play' ) { // play stats $top_ids_results_user = $this->top_ten_users_by_plays( $interval, 'user' ); if( $guest_stats ) $top_ids_results_guest = $this->top_ten_users_by_plays( $interval, 'guest' ); } else { // watch time stats $top_ids_results_user = $this->top_ten_users_by_watch_time( $interval, 'user' ); if( $guest_stats ) $top_ids_results_guest = $this->top_ten_users_by_watch_time( $interval, 'guest' ); } // if both empty, return false if ( empty( $top_ids_results_user ) && empty( $top_ids_results_guest ) ) { return false; } // regular users if( !empty($top_ids_results_user) ) { $top_ids_arr_user = array_values( $top_ids_results_user ); $top_ids_user = array_map( 'intval', array_values( $top_ids_arr_user ) ); $placeholders = implode( ', ', array_fill( 0, count( $top_ids_user ), '%d' ) ); if( $metric == 'play' ) { $results_user = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date, user_id, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND user_id IN( $placeholders ) GROUP BY user_id, date", array_merge( array( $interval[0], $interval[1] ), $top_ids_user ) ), ARRAY_A ); } else { $results_user = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date, user_id, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND user_id IN( $placeholders ) GROUP BY user_id, date", array_merge( array( $interval[0], $interval[1] ), $top_ids_user ) ), ARRAY_A ); } } // guest users if( $guest_stats && !empty($top_ids_results_guest) ) { // TODO: Fix if empty, the SQL below will fail $top_ids_arr_guest = array_values( $top_ids_results_guest ); $top_ids_guest = array_map( 'intval', array_values( $top_ids_arr_guest ) ); $placeholders = implode( ', ', array_fill( 0, count( $top_ids_guest ), '%d' ) ); if( $metric == 'play' ) { $results_guest = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date, guest_user_id, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND guest_user_id IN( $placeholders ) GROUP BY guest_user_id, date", array_merge( array( $interval[0], $interval[1] ), $top_ids_guest ) ), ARRAY_A ); } else { $results_guest = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date, guest_user_id, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND guest_user_id IN( $placeholders ) GROUP BY guest_user_id, date", array_merge( array( $interval[0], $interval[1] ), $top_ids_guest ) ), ARRAY_A ); } } // process data for regular users if( !empty($results_user) ) { $datasets_users = $this->process_graph_data( $results_user, $top_ids_arr_user, $range, 'user', $metric ); } // process data for guest users if( !empty($results_guest) ) { $datasets_guests = $this->process_graph_data( $results_guest, $top_ids_arr_guest, $range, 'guest', $metric ); } // merge datasets $datasets = array_merge( $datasets_users, $datasets_guests ); return $datasets; } public function get_top_video_watch_time_stats( $type, $range, $user_id ) { global $wpdb; // dynamic interval based on range $interval = self::get_interval_from_range( $range ); $datasets = false; $top_ids_results = $this->top_ten_videos_by_watch_time( $type, $interval, $user_id ); // get top video ids if( !empty($top_ids_results) ) { $top_ids = array_map( 'intval', array_values( $top_ids_results ) ); $top_ids[] = 0; // add 0 to make sure the SQL is valid $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) ); } else { return false; } if( is_numeric( $user_id ) ) { if( $type == 'video' ) { // video stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids, array( $user_id ) ) ), ARRAY_A ); } else if( $type == 'post' ) { // post stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date, id_post, post_title, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->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", array_merge( array( $interval[0], $interval[1] ), $top_ids, array( $user_id ) ) ), ARRAY_A ); } } else { if( $type == 'video' ) { // video stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids ) ), ARRAY_A ); } else if( $type == 'post' ) { // post stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date, id_post, post_title, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->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", array_merge( array( $interval[0], $interval[1] ), $top_ids ) ), ARRAY_A ); } } if( !empty($results) ) { $datasets = $this->process_graph_data( $results, $top_ids, $range, $type, 'seconds' ); } return $datasets; } public function get_top_video_post_stats( $type, $range, $user_id ) { global $wpdb; // dynamic interval based on range $interval = self::get_interval_from_range( $range ); $datasets = false; $top_ids_results = $this->top_ten_videos_or_posts_by_plays( $type, $interval, $user_id ); // get top video ids if( !empty($top_ids_results) ) { $top_ids = array_map( 'intval', array_values( $top_ids_results ) ); $top_ids[] = 0; // add 0 to make sure the SQL is valid $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) ); } else { return false; } if( is_numeric( $user_id ) ) { if( $type == 'video' ) { // video stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids, array( $user_id ) ) ), ARRAY_A ); } else if( $type == 'post' ) { // post stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids, array( $user_id ) ) ), ARRAY_A ); } } else { if( $type == 'video' ) { // video stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids ) ), ARRAY_A ); } else if( $type == 'post' ) { // post stats $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids ) ), ARRAY_A ); } } if( !empty($results) ) { $datasets = $this->process_graph_data( $results, $top_ids, $range, $type ); } return $datasets; } public function get_top_video_ad_data( $range, $metric ) { global $wpdb; // dynamic interval based on range $interval = self::get_interval_from_range( $range ); $datasets = false; // we track ads based on video $type = 'video'; $top_ids_results = $this->get_video_ad_video_ids( $interval ); if( !empty($top_ids_results) ) { $top_ids = array_map( 'intval', array_values( $top_ids_results ) ); $top_ids[] = 0; // add 0 to make sure the SQL is valid $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) ); } else { return false; } $results = $wpdb->get_results( // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $top_ids ) ), ARRAY_A ); if( !empty($results) ) { $datasets = $this->process_graph_data( $results, $top_ids, $range, $type, $metric ); } return $datasets; } public function get_player_stats( $player_id, $range) { global $wpdb; $interval = self::get_interval_from_range( $range ); $datasets = false; $results = $wpdb->get_results( $wpdb->prepare( "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", $interval[0], $interval[1], $player_id ), ARRAY_A ); if( !empty($results) ) { $ids_arr = array(); foreach( $results as $row ) { $ids_arr[] = $row['id_video']; } // Make sure each video is only considered once, otherwise this ends up multiplying the stats is loading for one player only $ids_arr = array_unique( $ids_arr ); $datasets = $this->process_graph_data( $results, $ids_arr, $range, 'video' ); } return $datasets; } public function get_users_by_time_range( $range, $user_id = false ) { global $wpdb; $excluded = $this->get_posts_to_exclude(); $interval = self::get_interval_from_range( $range ); if( $user_id ) { $result = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $user_id ) ) ), ARRAY_A ); } else { $result = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'] ) ), ARRAY_A ); } if ( ! $result ) { $result = array(); } return $result; } public function get_valid_dates( $user_id ) { global $wpdb; $excluded = $this->get_posts_to_exclude(); $dates_all = array( 'this_week' => 'This Week', 'last_week' => 'Last Week', 'this_month' => 'This Month', 'last_month' => 'Last Month' ); $years = $this->get_all_years(); $dates_all = $dates_all + $years; // merge $dates_valid = array(); $this_year = (int) gmdate( 'Y' ); $last_year = $this_year - 1; foreach( $dates_all as $key => $value ) { $interval = self::get_interval_from_range( $key ); if( $user_id ) { $result = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "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", array_merge( array( $interval[0], $interval[1] ), $excluded['values'], array( $user_id ) ) ), ARRAY_A ); } else { $result = $wpdb->get_results( // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT date FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT 1", array_merge( array( $interval[0], $interval[1] ), $excluded['values'] ) ), ARRAY_A ); } if( $key == $this_year) { $key = 'this_year'; $value = 'This Year'; } else if( $key == $last_year ) { $key = 'last_year'; $value = 'Last Year'; } $dates_valid[$key] = array(); if( !empty($result) ) { $dates_valid[$key]['disabled'] = false; } else { $dates_valid[$key]['disabled'] = true; } $dates_valid[$key]['value'] = $value; } return $dates_valid; } public function get_valid_interval( $user_id ) { // we need to check every interval for user to check if there is any data $intervals = array( 'this_week', 'last_week', 'this_month', 'last_month', ); $years = $this->get_all_years(); $intervals = $intervals + $years; // merge // TODO: optimize performance, no need to use SUM or ORDER BY, limit 1 would be enough foreach( $intervals as $k => $interval ) { $data = $this->get_top_video_watch_time_stats( 'video', $interval, $user_id ); // if there is no data for this interval, remove it from the list if( empty($data) ) { unset($intervals[$k]); } } return $intervals; } public static function get_interval_from_range( $range ) { if( strcmp( 'this_week', $range ) === 0 ) { // this week $start = gmdate('Y-m-d', strtotime('-7 days') ); $end = gmdate('Y-m-d', time() ); } else if( strcmp( 'last_week', $range ) === 0 ) { // last week $previous_week = strtotime("-1 week +1 day"); // convert to datetime $previous_week = gmdate('Y-m-d', $previous_week); // respect the start of week day by wordpress $start_end_week = get_weekstartend($previous_week); $start = gmdate('Y-m-d', $start_end_week['start']); $end = gmdate('Y-m-d', $start_end_week['end']); } else if( strcmp( 'this_month', $range ) === 0 ) { // this month $start = gmdate('Y-m-01'); $end = gmdate('Y-m-t'); } else if( strcmp( 'last_month', $range ) === 0 ) { // last month $first_day_last_month = strtotime('first day of last month'); $last_day_last_month = strtotime('last day of last month'); $start = gmdate('Y-m-01', $first_day_last_month ); $end = gmdate('Y-m-t', $last_day_last_month ); } else if( strcmp( 'this_year', $range ) === 0 ) { // this year $start = gmdate('Y-01-01'); $end = gmdate('Y-12-31'); } else if( strcmp( 'last_year', $range ) === 0 ) { // last year $start = gmdate('Y-01-01', strtotime('-1 year')); $end = gmdate('Y-12-31', strtotime('-1 year')); } else if( is_numeric($range)) { // specific year like 2021 $start = intval( $range ) . '-01-01'; $end = intval( $range ) . '-12-31'; } return array( $start, $end); } /** * Get the desired date range * * @param string|int $range this_week, last_week, this_month, last_month, this_year, last_year or year number * @param mixed $base_date (optional) The base date to use for this_week * @return array All the days in the date range in YYYY-MM-DD format. */ private function get_dates_in_range( $range, $base_date = false ) { $dates = array(); $time = time(); if ( $base_date ) { $time = strtotime( $base_date ); } if( strcmp( 'this_week', $range ) === 0 ) { $end_day = gmdate('Y-m-d', $time ); $start_day = gmdate('Y-m-d', strtotime( '-7 days', $time ) ); $dates = $this->get_days_between_dates( $start_day, $end_day ); } else if( strcmp( 'last_week', $range ) === 0 ) { $previous_week = strtotime("-1 week +1 day"); // convert to datetime $previous_week = gmdate('Y-m-d', $previous_week); // respect the start of week day by wordpress $start_end_week = get_weekstartend($previous_week); $start_week = gmdate('Y-m-d', $start_end_week['start']); $end_week = gmdate('Y-m-d', $start_end_week['end']); $dates = $this->get_days_between_dates( $start_week, $end_week ); } else if( strcmp( 'this_month', $range ) === 0 ) { $start_day = gmdate('Y-m-01'); $end_day = gmdate('Y-m-d'); $dates = $this->get_days_between_dates( $start_day, $end_day ); } else if( strcmp( 'last_month', $range ) === 0 ) { $first_day_last_month = strtotime('first day of last month'); $last_day_last_month = strtotime('last day of last month'); $start_day = gmdate('Y-m-01', $first_day_last_month ); $end_day = gmdate('Y-m-t', $last_day_last_month ); $dates = $this->get_days_between_dates( $start_day, $end_day ); } else if( strcmp( 'this_year', $range ) === 0 ) { $start_day = gmdate('Y-01-01'); $end_day = gmdate('Y-m-d'); $dates = $this->get_days_between_dates( $start_day, $end_day ); } else if( strcmp( 'last_year', $range ) === 0 ) { $start_day = gmdate('Y-01-01', strtotime('-1 year')); $end_day = gmdate('Y-12-31', strtotime('-1 year')); $dates = $this->get_days_between_dates( $start_day, $end_day ); } else if( is_numeric($range) ) { // get dates for specific year like 2021 $start_day = intval( $range ) . '-01-01'; $end_day = intval( $range ) . '-12-31'; $dates = $this->get_days_between_dates( $start_day, $end_day ); } return $dates; } function get_all_years() { global $wpdb; $years = array(); $oldest_year = (int) $wpdb->get_var("SELECT YEAR(date) FROM {$wpdb->prefix}fv_player_stats ORDER BY id ASC LIMIT 1"); // add every year from oldest to current, when oldest is 2021 and current is 2025, it will add 2021, 2022, 2023, 2024, 2025 for( $i = $oldest_year; $i <= gmdate('Y'); $i++ ) { $j = strval($i); $years[$j] = $j; } // reorder years from newest to oldest $years = array_reverse( $years, true ); return $years; } private function get_days_between_dates( $start_day, $end_day ) { $dates = array(); $current = strtotime($start_day); $end = strtotime($end_day); while( $current <= $end ) { $dates[] = gmdate('Y-m-d', $current); $current = strtotime('+1 day', $current); } return $dates; } private function get_date_labels( $results ) { $date_labels = array(); foreach( $results as $row) { if( !in_array( $row['date'], $date_labels ) ) { $date_labels[strtotime($row['date'])] = $row['date']; } } ksort($date_labels); return array_values($date_labels); } /** * Group the database result rows by the video or post ID for the desired date range. * * @param array $raw_db_results Each item is array like: * array( * 'date' => '2024-09-03', * 'id_player' => '14', * 'id_video' => '912', * 'title' => 'My Video', * 'play' => '1', * ), * array( * 'date' => '2024-09-05', * 'id_player' => '171', * 'id_video' => '912', * 'title' => 'My Video', * 'play' => '1', * ), * array( * 'date' => '2024-09-07', * 'id_player' => '14', * 'id_video' => '912', * 'title' => 'My Video', * 'play' => '1', * ) * * @param mixed $top_ids_arr * @param string|int $range this_week, last_week, this_month, last_month, this_year, last_year or year number * @param string $type video or post * @param string $metric play or seconds or clicks * @param string $base_date (optional) The base date to use for $range * * @return array Summary of the daily video plays per video or post (see $type) by id_video or is_post: * 912 => array( * '2024-09-02' => array( 'play' => 0 ), * 'name' => 'My Video', * '2024-09-03' => array( 'play' => '1' ), * '2024-09-04' => array( 'play' => 0 ), * '2024-09-05' => array( 'play' => 1 ), * '2024-09-06' => array( 'play' => 0 ), * '2024-09-07' => array( 'play' => 1 ), * '2024-09-08' => array( 'play' => 0 ), * '2024-09-09' => array( 'play' => 0 ), * ), */ private function process_graph_data( $raw_db_results, $top_ids_arr, $range, $type, $metric = 'play', $base_date = false ) { $datasets = array(); $date_labels = $this->get_dates_in_range( $range, $base_date ); // order data for graph, foreach( $top_ids_arr as $id ) { foreach( $date_labels as $date ) { foreach( $raw_db_results as $row) { 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 ) ) { if( !isset($datasets[$id]) ) { $datasets[$id] = array(); } // aggregate data by date if( strcmp( $date, $row['date'] ) == 0 ) { // date row exists if( $metric === 'play' && isset($row['play']) ) { if( isset($datasets[$id][$date]['play']) ) { $datasets[$id][$date]['play'] += $row['play']; } else { $datasets[$id][$date]['play'] = $row['play']; } } if( $metric === 'seconds' && isset($row['seconds']) ) { if( isset($datasets[$id][$date]['seconds']) ) { $datasets[$id][$date]['seconds'] += $row['seconds']; } else { $datasets[$id][$date]['seconds'] = (int) $row['seconds']; } } if( $metric === 'click' && isset($row['click']) ) { if( isset($datasets[$id][$date]['click']) ) { $datasets[$id][$date]['click'] += $row['click']; } else { $datasets[$id][$date]['click'] = $row['click']; } } } else { // date row dont exists, add 0 plays/seconds - dont overwrite if value already set if( $metric === 'play' && !isset( $datasets[$id][$date]['play']) ) $datasets[$id][$date]['play'] = 0; if( $metric === 'seconds' && !isset( $datasets[$id][$date]['seconds']) ) $datasets[$id][$date]['seconds'] = 0; if( $metric === 'click' && !isset( $datasets[$id][$date]['click']) ) $datasets[$id][$date]['click'] = 0; } // add labels if( !isset($datasets[$id]['name']) ) { if( $type == 'video' || $type == 'player' ) { $datasets[$id]['name'] = $this->get_video_name( $row ); } else if( $type == 'post' ) { $datasets[$id]['name'] = !empty($row['post_title'] ) ? $row['post_title'] : 'id_post_' . $row['id_post'] ; } else if( $type == 'user' ) { $user_data = get_userdata( intval($row['user_id']) ); if( $user_data === false ) { $datasets[$id]['name'] = 'Guest Users'; } else { $datasets[$id]['name'] = $user_data->display_name; } } else if( $type == 'guest') { $datasets[$id]['name'] = 'Guest ' . $row['guest_user_id']; } } } } } } $datasets['date-labels'] = $date_labels; // date will be used as X axis label return $datasets; } function get_video_name( $row ) { if( ! empty( $row['title'] ) ) { return $row['title']; } $src = $row['src']; // check if youtube if( FV_Player_YouTube()->is_youtube( $src ) ) { // get youtube id preg_match( '/[\\?\\&]v=([^\\?\\&]+)/', $src, $matches ); if( isset($matches[1]) ) { $id = $matches[1]; $name = 'Youtube: ' . $id; return $name; } } // check if vimeo if( function_exists('FV_Player_Pro_Vimeo') && FV_Player_Pro_Vimeo()->is_vimeo($src) ) { // get vimeo id preg_match( '/vimeo\.com\/([0-9]+)/', $src, $matches ); if( isset($matches[1]) ) { $id = $matches[1]; $name = 'Vimeo: ' . $id; return $name; } } // parse title $name = flowplayer::get_title_from_src($src); return $name; } /** * Test the stats tracking by sending the ping request to the tracking endpoint and then checking if it got recorded * properly in the file. */ public function stats_test() { if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'fv_player_track' ) ) { wp_send_json_error( 'Invalid nonce' ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'You are not allowed to do this' ); } // Create nonce for non-logged in user as we want to test the tracking for non-logged in users $action = 'fv_player_track'; $i = wp_nonce_tick( $action ); $key = $i . '|' . $action . '|0|'; $nonce = substr( wp_hash( $key, 'nonce' ), -12, 10 ); // Send the ping request to the tracking endpoint $response = wp_remote_post( flowplayer::get_plugin_url() . '/controller/track.php', array( 'body' => array( 'action' => 'fv_player_track', 'blog_id' => get_current_blog_id(), 'tag' => 'ping', 'user_id' => 0, '_wpnonce' => $nonce, ), ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( 'HTTP Error: ' . $response->get_error_message() ); } if ( $response['response']['code'] !== 200 ) { wp_send_json_error( 'Unexpected HTTP response code: ' . $response['response']['code'] . ' ' . $response['response']['message'] ); } $response_body = wp_remote_retrieve_body( $response ); if ( ! empty( $response_body ) ) { wp_send_json_error( 'Unexpected response: ' . $response_body ); } // Check the stored value in the cache file $filename = "ping-" . absint( get_current_blog_id() ) . ".data"; $fp = fopen( $this->cache_directory . "/" . $filename, 'r+'); if ( ! $fp ) { wp_send_json_error( 'Failed to open cache file: ' . $filename ); } $encoded_data = fgets( $fp ); $data = json_decode( $encoded_data, true ); $json_error = json_last_error(); if( $json_error !== JSON_ERROR_NONE ) { wp_send_json_error( 'JSON decode error: ' . json_last_error_msg() ); } $current_time = time(); $tollerance = $current_time - 10; if ( ! isset( $data['pong'] ) ) { wp_send_json_error( 'Bad tracking data found: ' . var_export( $data, true ) ); } if ( $data['pong'] < $tollerance ) { wp_send_json_error( 'Tracking data is too old: ' . date( 'Y-m-d H:i:s', $data['pong'] ) . ' < ' . date( 'Y-m-d H:i:s', $tollerance ) ); } else if ( $data['pong'] > $current_time ) { wp_send_json_error( 'Tracking data is too new: ' . date( 'Y-m-d H:i:s', $data['pong'] ) . ' > ' . date( 'Y-m-d H:i:s', $current_time ) ); } $info = array(); $info_message = ''; $warnings = array(); $warnings_message = ''; foreach( array( "play-" . absint( get_current_blog_id() ) . ".data", "seconds-" . absint( get_current_blog_id() ) . ".data" ) as $filename ) { $fp = fopen( $this->cache_directory . "/" . $filename, 'r+'); if ( ! $fp ) { wp_send_json_error( 'Failed to open cache file: ' . $filename ); } $encoded_data = fgets( $fp ); if ( $encoded_data ) { $data = json_decode( $encoded_data, true ); $json_error = json_last_error(); if( $json_error !== JSON_ERROR_NONE ) { wp_send_json_error( 'JSON decode error for ' . $filename . ': ' . json_last_error_msg() ); } $info[] = '' . $filename . ' has data about ' . count( $data ) . ' videos'; } else { $warnings[] = '' . $filename . ' is empty'; } } if ( ! empty( $info ) ) { $info_message = ' ' . implode( ', ', $info ); } if ( ! empty( $warnings ) ) { $warnings_message = ' Warning: ' . implode( ', ', $warnings ) . '. If you did not play any video as a guest in last 5 minutes, this is normal. Try to play a video and re-test.'; } wp_send_json_success( 'Test successful' . $info_message . $warnings_message ); } function users_column( $columns ) { global $fv_fp; if ( $fv_fp->_get_option('video_stats_enable') ) { $columns['fv_player_stats_user_play_today'] = "Video Plays Today"; $columns['fv_player_stats_user_seconds_today'] = "Video Minutes Today"; } return $columns; } function users_column_content( $content, $column_name, $user_id ) { $field = false; if ( 'fv_player_stats_user_play_today' === $column_name ) { $field = 'play'; } else if ( 'fv_player_stats_user_seconds_today' === $column_name ) { $field = 'seconds'; } if( $field ) { // TODO: Preload to avoid too many SQL queries global $wpdb; if ( 'play' === $field ) { $val = $wpdb->get_var( $wpdb->prepare( "SELECT sum(play) FROM {$wpdb->prefix}fv_player_stats WHERE user_id = %d AND date = %s", $user_id, date_i18n( 'Y-m-d', false, true ) ) ); } else if ( 'seconds' === $field ) { $val = $wpdb->get_var( $wpdb->prepare( "SELECT sum(seconds) FROM {$wpdb->prefix}fv_player_stats WHERE user_id = %d AND date = %s", $user_id, date_i18n( 'Y-m-d', false, true ) ) ); } if ( $val ) { if( 'seconds' === $field ) { $val = ceil($val/60) . ' min'; } $url = add_query_arg( array( 'page' => 'fv_player_stats_users', 'user_id' => $user_id ), admin_url( 'admin.php' ) ); $content = '' . $val . ''; } } return $content; } function users_sortable_columns( $columns ) { $columns['fv_player_stats_user_play_today'] = 'fv_player_stats_user_play_today'; $columns['fv_player_stats_user_seconds_today'] = 'fv_player_stats_user_seconds_today'; return $columns; } function users_sort($userquery) { global $wpdb; $field = false; if ( 'fv_player_stats_user_play_today' === $userquery->query_vars['orderby'] ) { $field = 'play'; } else if ( 'fv_player_stats_user_seconds_today' === $userquery->query_vars['orderby'] ) { $field = 'seconds'; } if ( $field ) { $userquery->query_fields .= ", sum(" . $field . ") AS " . $field . " "; $userquery->query_from .= " LEFT OUTER JOIN {$wpdb->prefix}fv_player_stats AS stats ON ($wpdb->users.ID = stats.user_id) "; $userquery->query_where .= " AND stats.date = '" . date_i18n( 'Y-m-d', false, true ) . "' "; $userquery->query_orderby = " GROUP BY wp_users.ID ORDER BY " . $field . " ".($userquery->query_vars["order"] == "ASC" ? "ASC " : "DESC "); } } function user_stats_search() { 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']) ) { $search = sanitize_text_field( $_GET['q'] ); $date_range = sanitize_text_field( $_GET['date_range'] ); // search for users by login, nicename or email $users = get_users( array( 'search' => '*' . $search . '*', 'search_columns' => array( 'user_login', 'display_name' ,'user_nicename', 'user_email' ), ) ); $results = array(); foreach( $users AS $user ) { $data = $this->get_users_by_time_range( $date_range, $user->ID ); // check if user has any data in the selected date range if( $data ) { $plays = $data[0]['play'] ? $data[0]['play'] : 0; $item = array( 'id' => $user->ID, // used as value for option 'text' => $user->display_name . '-' . $user->user_email . ' ( ' . number_format_i18n( $plays, 0) . ' plays )' // used as label for option ); if( !$plays ) { $item['disabled'] = true; // disable option if user has no data in the selected date range } $results[] = $item; } } echo wp_json_encode( array( 'results' => $results ) ); } die(); } } global $FV_Player_Stats; $FV_Player_Stats = new FV_Player_Stats(); function fv_player_stats_top( $args = array() ) { $args = wp_parse_args( $args, array( 'taxonomy' => false, 'term' => false ) ); extract($args); global $wpdb; if( $taxonomy && $term ) { $raw = $wpdb->get_results( $wpdb->prepare(" SELECT p.id, vm.id_video, vm.meta_value AS stats_play, pm.meta_value AS post_id FROM {$wpdb->prefix}fv_player_videometa AS vm JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) > 0 JOIN {$wpdb->prefix}fv_player_playermeta AS pm ON p.id = pm.id_player INNER JOIN {$wpdb->prefix}term_relationships AS tr ON (pm.meta_value = tr.object_id) INNER JOIN {$wpdb->prefix}term_taxonomy AS tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id) INNER JOIN {$wpdb->prefix}terms AS t ON (t.term_id = tt.term_id) WHERE vm.meta_key = 'stats_play' AND pm.meta_key = 'post_id' AND tt.taxonomy = %s AND t.name = %s ORDER BY CAST(vm.meta_value AS unsigned) DESC", $taxonomy, $term ) ); } else { $raw = $wpdb->get_results( " SELECT p.id, vm.id_video, vm.meta_value AS stats_play, pm.meta_value AS post_id FROM {$wpdb->prefix}fv_player_videometa AS vm JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) > 0 JOIN {$wpdb->prefix}fv_player_playermeta AS pm ON p.id = pm.id_player WHERE vm.meta_key = 'stats_play' AND pm.meta_key = 'post_id' ORDER BY CAST(vm.meta_value AS unsigned) DESC" ); } // sice there might be multiple players for a single post_id we count these together $top = array(); foreach( $raw AS $record ) { if( empty($top[$record->post_id]) ) $top[$record->post_id] = 0; $top[$record->post_id] += $record->stats_play; } asort($top); $top = array_reverse($top,true); return $top; }