| 1 |
<?php |
| 2 |
/** |
| 3 |
* PowerPress Program Card |
| 4 |
* unified component that renders stats widget and show info card together |
| 5 |
* handles program data fetching, stats api calls, and rendering |
| 6 |
*/ |
| 7 |
|
| 8 |
class PowerPressProgramCard |
| 9 |
{ |
| 10 |
// CONFIGURATION |
| 11 |
private $general_settings; |
| 12 |
private $feed_settings; |
| 13 |
private $feed_slug; |
| 14 |
private $deferred = false; |
| 15 |
private $has_blubrry_auth = false; |
| 16 |
|
| 17 |
// PROGRAM DATA |
| 18 |
private $network_mode = false; |
| 19 |
private $program_keyword = ''; |
| 20 |
private $default_program_keyword = ''; |
| 21 |
private $programs = []; |
| 22 |
private $programs_full = []; |
| 23 |
private $program_info = []; |
| 24 |
|
| 25 |
// STATS DATA |
| 26 |
private $stats_content = []; |
| 27 |
// URL CONFIG |
| 28 |
private $stats_base_url = 'https://stats.blubrry.com/'; |
| 29 |
|
| 30 |
/* ============== |
| 31 |
INITIALIZATION |
| 32 |
============== */ |
| 33 |
|
| 34 |
public function __construct(string $feed_slug = 'podcast', string $override_program = '', bool $deferred = false) { |
| 35 |
// 1. LOAD SETTINGS |
| 36 |
$this->feed_slug = $feed_slug; |
| 37 |
$general = get_option('powerpress_general'); |
| 38 |
$this->general_settings = is_array($general) ? $general : []; |
| 39 |
|
| 40 |
if (defined('POWERPRESS_BLUBRRY_STATS_URL')) $this->stats_base_url = rtrim(POWERPRESS_BLUBRRY_STATS_URL, '/') . '/'; |
| 41 |
|
| 42 |
$this->network_mode = !empty($this->general_settings['network_mode']); |
| 43 |
$this->default_program_keyword = !empty($this->general_settings['blubrry_program_keyword']) |
| 44 |
? $this->general_settings['blubrry_program_keyword'] |
| 45 |
: ''; |
| 46 |
|
| 47 |
// 2. CHECK AUTH |
| 48 |
$creds = get_option('powerpress_creds'); |
| 49 |
$userpass = !empty($this->general_settings['blubrry_auth']) ? $this->general_settings['blubrry_auth'] : ''; |
| 50 |
$this->has_blubrry_auth = !empty($creds) || !empty($userpass); |
| 51 |
|
| 52 |
// 3. CHECK PERMISSIONS |
| 53 |
$stats_enabled = $this->should_show_stats(); |
| 54 |
$this->deferred = $deferred && $this->has_blubrry_auth && $stats_enabled; |
| 55 |
$this->load_feed_settings(); |
| 56 |
|
| 57 |
// 4. LOAD DATA |
| 58 |
if (!$this->has_blubrry_auth) { |
| 59 |
$this->program_keyword = $override_program ?: $this->default_program_keyword; |
| 60 |
} elseif (!$this->deferred) { |
| 61 |
$this->load_stats_data($override_program); |
| 62 |
} else { |
| 63 |
$this->program_keyword = $override_program ?: $this->default_program_keyword; |
| 64 |
} |
| 65 |
$this->program_info = $this->build_program_info(); |
| 66 |
} |
| 67 |
|
| 68 |
/** checks permission flags and user capabilities */ |
| 69 |
private function should_show_stats(): bool { |
| 70 |
if (!empty($this->general_settings['disable_dashboard_stats'])) return false; |
| 71 |
if (!empty($this->general_settings['use_caps']) && !current_user_can('view_podcast_stats')) return false; |
| 72 |
return true; |
| 73 |
} |
| 74 |
|
| 75 |
/** loads feed settings for current slug with defaults applied */ |
| 76 |
private function load_feed_settings(): void { |
| 77 |
$feed = get_option($this->feed_slug === 'podcast' ? 'powerpress_feed' : "powerpress_feed_{$this->feed_slug}"); |
| 78 |
$this->feed_settings = is_array($feed) ? $feed : []; |
| 79 |
|
| 80 |
if (function_exists('powerpress_default_settings')) { |
| 81 |
$this->feed_settings = powerpress_default_settings($this->feed_settings, 'editfeed'); |
| 82 |
} |
| 83 |
} |
| 84 |
|
| 85 |
public function get_program_keyword(): string { |
| 86 |
return $this->program_keyword; |
| 87 |
} |
| 88 |
|
| 89 |
/* ============ |
| 90 |
PROGRAM DATA |
| 91 |
============ */ |
| 92 |
|
| 93 |
/** fetches program list from api w/ transient caching */ |
| 94 |
private function fetch_programs(): array { |
| 95 |
$settings = $this->general_settings; |
| 96 |
$creds = get_option('powerpress_creds'); |
| 97 |
|
| 98 |
// 1. CHECK CACHE |
| 99 |
$cached_programs = get_transient('powerpress_programs_list'); |
| 100 |
if ($cached_programs !== false) { |
| 101 |
return $cached_programs; |
| 102 |
} |
| 103 |
|
| 104 |
// 2. FETCH API |
| 105 |
$programs = []; |
| 106 |
$api_error = null; |
| 107 |
|
| 108 |
require_once POWERPRESS_ABSPATH . '/powerpressadmin-auth.class.php'; |
| 109 |
$auth = new PowerPressAuth(); |
| 110 |
$api_url_array = powerpress_get_api_array(); |
| 111 |
|
| 112 |
$results = powerpress_api_request('/2/service/index.json', [], [], $settings, $creds, $auth, $api_url_array, 15); |
| 113 |
|
| 114 |
if ($results && is_array($results)) { |
| 115 |
if (isset($results['error'])) { |
| 116 |
$err = $results['message'] ?? $results['error']; |
| 117 |
if (!empty($err)) { |
| 118 |
$reset_url = wp_nonce_url( |
| 119 |
admin_url('admin.php?page=powerpress/powerpressadmin_tools.php&action=powerpress-reset-blubrry-connection'), |
| 120 |
'powerpress-reset-blubrry-connection' |
| 121 |
); |
| 122 |
$err = sprintf( |
| 123 |
__('Service unavailable (%1$s). <a href="%2$s">Reset and reconnect your Blubrry account</a>.', 'powerpress'), |
| 124 |
esc_html((string)$err), |
| 125 |
esc_url($reset_url) |
| 126 |
); |
| 127 |
} |
| 128 |
$api_error = $err; |
| 129 |
} else { |
| 130 |
foreach ($results as $row) { |
| 131 |
if (isset($row['program_keyword']) && isset($row['program_title'])) { |
| 132 |
$programs[$row['program_keyword']] = $row['program_title']; |
| 133 |
// store basic data for program_id lookup |
| 134 |
$this->programs_full[$row['program_keyword']] = $row; |
| 135 |
} |
| 136 |
} |
| 137 |
} |
| 138 |
} |
| 139 |
|
| 140 |
// 3. SAVE CACHE |
| 141 |
if (!empty($programs)) { |
| 142 |
set_transient('powerpress_programs_list', $programs, HOUR_IN_SECONDS); |
| 143 |
} else if ($api_error) { |
| 144 |
set_transient('powerpress_programs_api_error', $api_error, 5 * MINUTE_IN_SECONDS); |
| 145 |
} |
| 146 |
|
| 147 |
return $programs; |
| 148 |
} |
| 149 |
|
| 150 |
/** |
| 151 |
* fetches detailed program info from API (lazy-loaded per program) |
| 152 |
*/ |
| 153 |
private function fetch_program_info(string $keyword): ?array { |
| 154 |
if (empty($keyword)) return null; |
| 155 |
|
| 156 |
// 1. CHECK CACHE |
| 157 |
$cache_key = 'powerpress_program_info_' . md5($keyword); |
| 158 |
$cached_info = get_transient($cache_key); |
| 159 |
if ($cached_info !== false) { |
| 160 |
return $cached_info; |
| 161 |
} |
| 162 |
|
| 163 |
// 2. FETCH API |
| 164 |
$settings = $this->general_settings; |
| 165 |
$creds = get_option('powerpress_creds'); |
| 166 |
|
| 167 |
require_once POWERPRESS_ABSPATH . '/powerpressadmin-auth.class.php'; |
| 168 |
$auth = new PowerPressAuth(); |
| 169 |
$api_url_array = powerpress_get_api_array(); |
| 170 |
|
| 171 |
$enc_keyword = urlencode($keyword); |
| 172 |
$results = powerpress_api_request("/2/program/{$enc_keyword}/info.json", [], [], $settings, $creds, $auth, $api_url_array, 5); |
| 173 |
|
| 174 |
if (!$results || !is_array($results) || isset($results['error'])) { |
| 175 |
return null; |
| 176 |
} |
| 177 |
|
| 178 |
// 3. SAVE CACHE (30 min for program info) |
| 179 |
set_transient($cache_key, $results, 30 * MINUTE_IN_SECONDS); |
| 180 |
|
| 181 |
return $results; |
| 182 |
} |
| 183 |
|
| 184 |
/** |
| 185 |
* builds program info for display |
| 186 |
* default program: WP settings first, API as fallback |
| 187 |
* non-default programs: API only (no fallback to default's WP data) |
| 188 |
*/ |
| 189 |
private function build_program_info(): array { |
| 190 |
// 1. FETCH API DATA (skip when deferred - will load via AJAX later) |
| 191 |
$api = []; |
| 192 |
if (!$this->deferred && !empty($this->program_keyword) && $this->has_blubrry_auth) { |
| 193 |
$program = $this->fetch_program_info($this->program_keyword); |
| 194 |
if ($program) { |
| 195 |
$api = $program; |
| 196 |
// store in programs_full for program_id lookups elsewhere |
| 197 |
$this->programs_full[$this->program_keyword] = array_merge( |
| 198 |
$this->programs_full[$this->program_keyword] ?? [], |
| 199 |
$program |
| 200 |
); |
| 201 |
} |
| 202 |
} |
| 203 |
|
| 204 |
// 2. BASE: start with API values |
| 205 |
$default_image = powerpress_get_root_url() . 'images/pts_cover.jpg'; |
| 206 |
|
| 207 |
$title = $api['program_title'] ?? ''; |
| 208 |
$author = $api['author'] ?? ''; |
| 209 |
$description = $api['description'] ?? ''; |
| 210 |
$category = $api['category'] ?? ''; |
| 211 |
$feed_url = $api['feed_url'] ?? ''; |
| 212 |
$episode_count = isset($api['episode_count']) ? intval($api['episode_count']) : 0; |
| 213 |
$created_date = $api['created_date'] ?? ''; |
| 214 |
$last_update = $api['last_update'] ?? ''; |
| 215 |
|
| 216 |
$api_image = $api['artwork_url'] ?? ''; |
| 217 |
$has_real_api_image = !$this->is_placeholder_image($api_image); |
| 218 |
$image = $has_real_api_image ? $api_image : $default_image; |
| 219 |
|
| 220 |
// 3. OVERLAY: default program overrides with WP settings |
| 221 |
$is_default = ($this->program_keyword === $this->default_program_keyword); |
| 222 |
if ($is_default) { |
| 223 |
$wp_settings = $this->feed_settings; |
| 224 |
|
| 225 |
if (!empty($wp_settings['title'])) $title = $wp_settings['title']; |
| 226 |
if (!empty($wp_settings['itunes_talent_name'])) $author = $wp_settings['itunes_talent_name']; |
| 227 |
if (!empty($wp_settings['description'])) $description = $wp_settings['description']; |
| 228 |
|
| 229 |
// wp image if real -> api image if real -> wp default |
| 230 |
$wp_image = !empty($wp_settings['itunes_image']) ? $wp_settings['itunes_image'] : ($wp_settings['rss2_image'] ?? ''); |
| 231 |
if (!$this->is_placeholder_image($wp_image)) { |
| 232 |
$image = $wp_image; |
| 233 |
} elseif ($has_real_api_image) { |
| 234 |
$image = $api_image; |
| 235 |
} |
| 236 |
|
| 237 |
if (!empty($wp_settings['apple_cat_1'])) { |
| 238 |
$categories = powerpress_apple_categories(true); |
| 239 |
$category = $categories[$wp_settings['apple_cat_1']] ?? $wp_settings['apple_cat_1']; |
| 240 |
} |
| 241 |
|
| 242 |
$feed_url = $this->network_mode ? '' : get_feed_link($this->feed_slug); |
| 243 |
} |
| 244 |
|
| 245 |
if (empty($episode_count)) { |
| 246 |
$episode_count = powerpress_admin_episodes_per_feed($this->feed_slug); |
| 247 |
} |
| 248 |
|
| 249 |
return [ |
| 250 |
'title' => $title, |
| 251 |
'author' => $author, |
| 252 |
'description' => $description, |
| 253 |
'image' => $image, |
| 254 |
'category' => $category, |
| 255 |
'feed_url' => $feed_url, |
| 256 |
'episode_count' => $episode_count, |
| 257 |
'created_date' => $created_date, |
| 258 |
'last_update' => $last_update |
| 259 |
]; |
| 260 |
} |
| 261 |
|
| 262 |
/* ========== |
| 263 |
STATS DATA |
| 264 |
========== */ |
| 265 |
|
| 266 |
private function load_stats_data(string $override_program = ''): void { |
| 267 |
$settings = $this->general_settings; |
| 268 |
$creds = get_option('powerpress_creds'); |
| 269 |
|
| 270 |
// 1. VALIDATE PERMISSIONS |
| 271 |
if (!$this->should_show_stats()) return; |
| 272 |
|
| 273 |
$userpass = !empty($settings['blubrry_auth']) ? $settings['blubrry_auth'] : ''; |
| 274 |
|
| 275 |
// 2. CHECK AUTH |
| 276 |
if (!$userpass && !$creds) { |
| 277 |
$this->program_keyword = $this->default_program_keyword; |
| 278 |
return; |
| 279 |
} |
| 280 |
|
| 281 |
// 3. LOAD PROGRAMS |
| 282 |
$this->programs = $this->fetch_programs(); |
| 283 |
|
| 284 |
// 4. RESOLVE KEYWORD |
| 285 |
$keyword = ''; |
| 286 |
|
| 287 |
if ($this->network_mode) { |
| 288 |
$program_keys = array_map('strval', array_keys($this->programs)); |
| 289 |
|
| 290 |
if ($override_program && in_array((string)$override_program, $program_keys, true)) { |
| 291 |
$keyword = (string)$override_program; |
| 292 |
} else { |
| 293 |
$default_kw = $this->default_program_keyword ? (string)$this->default_program_keyword : ''; |
| 294 |
if ($default_kw && in_array($default_kw, $program_keys, true)) { |
| 295 |
$keyword = $default_kw; |
| 296 |
} |
| 297 |
} |
| 298 |
|
| 299 |
$this->program_keyword = $keyword; |
| 300 |
|
| 301 |
if (empty($this->programs)) { |
| 302 |
$api_error = get_transient('powerpress_programs_api_error'); |
| 303 |
$stats_url = esc_url($this->stats_base_url); |
| 304 |
if ($api_error) { |
| 305 |
$error_msg = $api_error; |
| 306 |
} else { |
| 307 |
$visit = __('Multi-program mode is enabled but no programs were found. Please visit', 'powerpress'); |
| 308 |
$suffix = __('to see your statistics.', 'powerpress'); |
| 309 |
$error_msg = "{$visit} <a href=\"{$stats_url}\" target=\"_blank\">Blubrry Stats</a> {$suffix}"; |
| 310 |
} |
| 311 |
$this->stats_content = ['error' => $error_msg]; |
| 312 |
return; |
| 313 |
} |
| 314 |
} else { |
| 315 |
$keyword = $this->default_program_keyword; |
| 316 |
$this->program_keyword = $keyword; |
| 317 |
} |
| 318 |
|
| 319 |
if (empty($keyword)) { |
| 320 |
$stats_url = esc_url($this->stats_base_url); |
| 321 |
$visit = __('No program selected. Please visit', 'powerpress'); |
| 322 |
$suffix = __('to see your statistics.', 'powerpress'); |
| 323 |
$this->stats_content = ['error' => "{$visit} <a href=\"{$stats_url}\" target=\"_blank\">Blubrry Stats</a> {$suffix}"]; |
| 324 |
return; |
| 325 |
} |
| 326 |
|
| 327 |
// 5. LOAD CACHE |
| 328 |
$days_key = "{$keyword}_days"; |
| 329 |
$meta_key = "{$keyword}_meta"; |
| 330 |
|
| 331 |
$stats_cached = get_option('powerpress_stats'); |
| 332 |
if (!is_array($stats_cached)) $stats_cached = []; |
| 333 |
|
| 334 |
$stats_cached = $this->migrate_stats_cache($stats_cached, $keyword); |
| 335 |
|
| 336 |
$cached_days = isset($stats_cached[$days_key]) && is_array($stats_cached[$days_key]) |
| 337 |
? $stats_cached[$days_key] |
| 338 |
: []; |
| 339 |
$cached_meta = isset($stats_cached[$meta_key]) && is_array($stats_cached[$meta_key]) |
| 340 |
? $stats_cached[$meta_key] |
| 341 |
: []; |
| 342 |
|
| 343 |
// fetch window |
| 344 |
$max_retention = 60; |
| 345 |
|
| 346 |
foreach (array_keys($cached_days) as $date) { |
| 347 |
if (!$this->is_valid_cache_date($date)) { |
| 348 |
unset($cached_days[$date]); |
| 349 |
} |
| 350 |
} |
| 351 |
|
| 352 |
// 6. CALCULATE DELTA |
| 353 |
$today = date('Y-m-d'); |
| 354 |
$today_ttl = 30 * 60; // 30 min ttl for today's data |
| 355 |
$last_fetch = isset($cached_meta['last_fetch']) ? (int)$cached_meta['last_fetch'] : 0; |
| 356 |
$today_needs_refresh = (time() - $last_fetch) > $today_ttl; |
| 357 |
|
| 358 |
$delta = $this->calculate_stats_delta($cached_days, $today, $max_retention); |
| 359 |
$fetch_days = $delta['fetch_days']; |
| 360 |
$is_full_fetch = $delta['is_full_fetch']; |
| 361 |
|
| 362 |
// use cached data if fresh enough |
| 363 |
if (!empty($cached_days) && !$today_needs_refresh && !$is_full_fetch) { |
| 364 |
$this->stats_content = $this->build_stats_content_from_cache($cached_days, $cached_meta, $max_retention); |
| 365 |
return; |
| 366 |
} |
| 367 |
|
| 368 |
// 7. FETCH API |
| 369 |
require_once(POWERPRESS_ABSPATH . '/powerpressadmin-auth.class.php'); |
| 370 |
$auth = new PowerPressAuth(); |
| 371 |
$api_url_array = powerpress_get_api_array(); |
| 372 |
|
| 373 |
$enc_keyword = urlencode($keyword); |
| 374 |
$new_content = powerpress_api_request( |
| 375 |
"/2/stats/{$enc_keyword}/data.json?days={$fetch_days}&include=daily,totals,averages,trends", |
| 376 |
[], |
| 377 |
[], |
| 378 |
$settings, |
| 379 |
$creds, |
| 380 |
$auth, |
| 381 |
$api_url_array, |
| 382 |
2 |
| 383 |
); |
| 384 |
|
| 385 |
// 8. PROCESS RESPONSE |
| 386 |
if (!$new_content) { |
| 387 |
if (!empty($cached_days)) { |
| 388 |
$this->stats_content = $this->build_stats_content_from_cache($cached_days, $cached_meta, $max_retention); |
| 389 |
} else { |
| 390 |
$this->stats_content = ['error' => 'Unable to retrieve statistics']; |
| 391 |
} |
| 392 |
return; |
| 393 |
} |
| 394 |
|
| 395 |
if (isset($new_content['error'])) { |
| 396 |
if (strpos($new_content['error'], 'Unable to locate program') !== false || |
| 397 |
strpos($new_content['error'], 'No statistics') !== false) { |
| 398 |
$this->stats_content = $this->get_empty_stats_content(); |
| 399 |
$no_stats_programs = get_transient('powerpress_no_stats_programs'); |
| 400 |
if (!is_array($no_stats_programs)) $no_stats_programs = []; |
| 401 |
$no_stats_programs[$keyword] = true; |
| 402 |
set_transient('powerpress_no_stats_programs', $no_stats_programs, DAY_IN_SECONDS); |
| 403 |
} else { |
| 404 |
$err = $new_content['error']; |
| 405 |
if (!empty($err)) { |
| 406 |
$reset_url = wp_nonce_url( |
| 407 |
admin_url('admin.php?page=powerpress/powerpressadmin_tools.php&action=powerpress-reset-blubrry-connection'), |
| 408 |
'powerpress-reset-blubrry-connection' |
| 409 |
); |
| 410 |
$err = sprintf( |
| 411 |
__('Statistics unavailable (%1$s). <a href="%2$s">Reset and reconnect your Blubrry account</a>.', 'powerpress'), |
| 412 |
esc_html((string)$err), |
| 413 |
esc_url($reset_url) |
| 414 |
); |
| 415 |
} |
| 416 |
$this->stats_content = ['error' => $err]; |
| 417 |
} |
| 418 |
return; |
| 419 |
} |
| 420 |
|
| 421 |
// 9. UPDATE CACHE (map data API response to internal format) |
| 422 |
$new_day_data = isset($new_content['daily']) ? $new_content['daily'] : []; |
| 423 |
$merged_days = $this->merge_stats_days_from_data_api($cached_days, $new_day_data); |
| 424 |
$merged_days = $this->prune_stats_days($merged_days, $today, $max_retention); |
| 425 |
|
| 426 |
$stats_cached[$days_key] = $merged_days; |
| 427 |
$stats_cached[$meta_key] = [ |
| 428 |
'last_fetch' => time(), |
| 429 |
'last_date' => $today, |
| 430 |
'stats_tier' => $new_content['stats_tier'] ?? 'basic', |
| 431 |
'program_total' => $new_content['totals']['all_time'] ?? 0, |
| 432 |
'month_average' => $new_content['averages']['thirty_day'] ?? 0, |
| 433 |
'month_average_change' => $new_content['trends']['thirty_day_vs_prior'] ?? 'same', |
| 434 |
]; |
| 435 |
update_option('powerpress_stats', $stats_cached); |
| 436 |
|
| 437 |
// clear no-stats flag if we got data |
| 438 |
$no_stats_programs = get_transient('powerpress_no_stats_programs'); |
| 439 |
if (is_array($no_stats_programs) && isset($no_stats_programs[$keyword])) { |
| 440 |
unset($no_stats_programs[$keyword]); |
| 441 |
set_transient('powerpress_no_stats_programs', $no_stats_programs, DAY_IN_SECONDS); |
| 442 |
} |
| 443 |
|
| 444 |
$this->stats_content = $this->build_stats_content_from_cache($merged_days, $stats_cached[$meta_key], $max_retention); |
| 445 |
} |
| 446 |
|
| 447 |
/** converts cached days data to stats_content format for rendering */ |
| 448 |
private function build_stats_content_from_cache(array $days_data, array $meta, int $window = 60): array { |
| 449 |
$today = date('Y-m-d'); |
| 450 |
$days_data = $this->fill_missing_dates($days_data, $today, $window); |
| 451 |
|
| 452 |
return [ |
| 453 |
'day_total_data' => $this->days_to_api_format($days_data), |
| 454 |
'stats_tier' => $meta['stats_tier'] ?? 'basic', |
| 455 |
'program_total' => $meta['program_total'] ?? 0, |
| 456 |
'month_average' => $meta['month_average'] ?? 0, |
| 457 |
'month_average_change' => $meta['month_average_change'] ?? 'same', |
| 458 |
]; |
| 459 |
} |
| 460 |
|
| 461 |
/** transforms stats_content into chart-ready data with scale and labels */ |
| 462 |
private function get_chart_data(int $day_count = 7): ?array { |
| 463 |
// 1. VALIDATE |
| 464 |
if (empty($this->stats_content) || isset($this->stats_content['error'])) |
| 465 |
return null; |
| 466 |
|
| 467 |
// 2. BUILD DAYS |
| 468 |
$days = []; |
| 469 |
$max_val = 0; |
| 470 |
$day_total_data = $this->stats_content['day_total_data'] ?? []; |
| 471 |
|
| 472 |
$total_entries = count($day_total_data); |
| 473 |
$start_index = max(0, $total_entries - $day_count); |
| 474 |
$is_month_view = $day_count > 7; |
| 475 |
$comparison_offset = $is_month_view ? 30 : 7; |
| 476 |
|
| 477 |
for ($i = $start_index; $i < $total_entries; $i++) { |
| 478 |
$day_data = $day_total_data[$i] ?? []; |
| 479 |
$total = (int)($day_data['trending_day_total'] ?? 0); |
| 480 |
$day_date = $day_data['day_date'] ?? date('Y-m-d'); |
| 481 |
|
| 482 |
// period-over-period comparison for tooltip |
| 483 |
$comparison_index = $i - $comparison_offset; |
| 484 |
$comparison_total = null; |
| 485 |
if ($comparison_index >= 0 && isset($day_total_data[$comparison_index]['trending_day_total'])) { |
| 486 |
$comparison_total = (int)$day_total_data[$comparison_index]['trending_day_total']; |
| 487 |
} |
| 488 |
|
| 489 |
$day_entry = [ |
| 490 |
'total' => $total, |
| 491 |
'date' => $is_month_view ? date("M d", strtotime($day_date)) : date("D d", strtotime($day_date)), |
| 492 |
'full_date' => date("l M d", strtotime($day_date)), |
| 493 |
]; |
| 494 |
|
| 495 |
if ($is_month_view) { |
| 496 |
$day_entry['last_month_total'] = $comparison_total; |
| 497 |
} else { |
| 498 |
$day_entry['last_week_total'] = $comparison_total; |
| 499 |
} |
| 500 |
|
| 501 |
$days[] = $day_entry; |
| 502 |
if ($total > $max_val) $max_val = $total; |
| 503 |
} |
| 504 |
|
| 505 |
// 3. CALCULATE SCALE |
| 506 |
$total_sum = array_sum(array_column($days, 'total')); |
| 507 |
$display_average = count($days) > 0 ? (int)round($total_sum / count($days)) : 0; |
| 508 |
|
| 509 |
$scale_min = 0; |
| 510 |
$nice_steps = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000]; |
| 511 |
|
| 512 |
$target_max = $max_val * 1.2; // 20% headroom |
| 513 |
if ($target_max < 5) $target_max = 5; |
| 514 |
|
| 515 |
$best_step = 1; |
| 516 |
$best_max = 5; |
| 517 |
foreach ($nice_steps as $step) { |
| 518 |
$potential_max = ceil($target_max / $step) * $step; |
| 519 |
$num_steps = $potential_max / $step; |
| 520 |
if ($num_steps >= 3 && $num_steps <= 6) { |
| 521 |
$best_step = $step; |
| 522 |
$best_max = $potential_max; |
| 523 |
break; |
| 524 |
} elseif ($num_steps < 3) { |
| 525 |
// step too big, use previous |
| 526 |
break; |
| 527 |
} |
| 528 |
$best_step = $step; |
| 529 |
$best_max = $potential_max; |
| 530 |
} |
| 531 |
|
| 532 |
$scale_max = $best_max; |
| 533 |
$scale_step = $best_step; |
| 534 |
|
| 535 |
$scale_range = $scale_max - $scale_min; |
| 536 |
if ($scale_range <= 0) $scale_range = 5; |
| 537 |
|
| 538 |
$num_lines = (int)($scale_range / $scale_step); |
| 539 |
if ($num_lines <= 0) $num_lines = 5; |
| 540 |
|
| 541 |
// 4. BUILD LABELS |
| 542 |
$scale_labels = []; |
| 543 |
for ($x = 0; $x < $num_lines; $x++) { |
| 544 |
$scale_labels[] = $scale_min + ($x + 1) * $scale_step; |
| 545 |
} |
| 546 |
|
| 547 |
$title_suffix = $day_count > 7 ? ' (30 Days)' : ' (7 Days)'; |
| 548 |
$base_title = !empty($this->stats_content['widget_title']) ? $this->stats_content['widget_title'] : 'Podcast Statistics'; |
| 549 |
|
| 550 |
// 5. RESOLVE URLS |
| 551 |
$tier = $this->get_stats_tier(); |
| 552 |
if (!$this->has_blubrry_auth) { |
| 553 |
$upgrade_url = admin_url('admin.php?page=powerpressadmin_basic&step=blubrrySignup&onboarding_type=stats'); |
| 554 |
} else if ($tier === 'basic') { |
| 555 |
$upgrade_url = 'https://secure.blubrry.com/checkout/manage-subscriptions/'; |
| 556 |
} else { |
| 557 |
// /s-{program_id}/ for specific show |
| 558 |
$program_id = !empty($this->programs_full[$this->program_keyword]['program_id']) |
| 559 |
? $this->programs_full[$this->program_keyword]['program_id'] |
| 560 |
: ''; |
| 561 |
|
| 562 |
$upgrade_url = $program_id |
| 563 |
? rtrim($this->stats_base_url, '/') . '/s-' . $program_id . '/' |
| 564 |
: $this->stats_base_url; |
| 565 |
} |
| 566 |
|
| 567 |
return [ |
| 568 |
'scale_min' => $scale_min, |
| 569 |
'scale_max' => $scale_max, |
| 570 |
'scale_range' => $scale_range, |
| 571 |
'scale_step' => $scale_step, |
| 572 |
'num_lines' => $num_lines, |
| 573 |
'scale_labels' => $scale_labels, |
| 574 |
'days' => $days, |
| 575 |
'widget_title' => "{$base_title}{$title_suffix}", |
| 576 |
'tier' => $this->get_stats_tier(), |
| 577 |
'has_auth' => $this->has_blubrry_auth, |
| 578 |
'upgrade_url' => $upgrade_url, |
| 579 |
'display_average' => $display_average |
| 580 |
]; |
| 581 |
} |
| 582 |
|
| 583 |
public function get_week_chart_data(): ?array { |
| 584 |
return $this->get_chart_data(7); |
| 585 |
} |
| 586 |
|
| 587 |
public function get_month_chart_data(): ?array { |
| 588 |
return $this->get_chart_data(30); |
| 589 |
} |
| 590 |
|
| 591 |
public function get_week_average(): int { |
| 592 |
$chart_data = $this->get_week_chart_data(); |
| 593 |
return $chart_data['display_average'] ?? 0; |
| 594 |
} |
| 595 |
|
| 596 |
public function get_month_average(): int { |
| 597 |
$chart_data = $this->get_month_chart_data(); |
| 598 |
return $chart_data['display_average'] ?? 0; |
| 599 |
} |
| 600 |
|
| 601 |
/** |
| 602 |
* empty stats placeholder for new programs |
| 603 |
*/ |
| 604 |
private function get_empty_stats_content(): array { |
| 605 |
$days = []; |
| 606 |
for ($i = 6; $i >= 0; $i--) { |
| 607 |
$date = date('Y-m-d', strtotime("-$i days")); |
| 608 |
$days[] = [ |
| 609 |
'day_date' => $date, |
| 610 |
'trending_day_total' => 0 |
| 611 |
]; |
| 612 |
} |
| 613 |
|
| 614 |
return [ |
| 615 |
'widget_title' => __('Podcast Statistics', 'powerpress'), |
| 616 |
'day_total_data' => $days, |
| 617 |
'scale_min' => 0, |
| 618 |
'scale_max' => 5, |
| 619 |
'scale_step' => 1, |
| 620 |
'month_average' => 0, |
| 621 |
'month_average_change' => '', |
| 622 |
'program_total' => 0, |
| 623 |
'is_empty' => true |
| 624 |
]; |
| 625 |
} |
| 626 |
|
| 627 |
/** tier comes directly from api response, default to basic */ |
| 628 |
public function get_stats_tier(): string { |
| 629 |
return $this->stats_content['stats_tier'] ?? 'basic'; |
| 630 |
} |
| 631 |
|
| 632 |
/* =========== |
| 633 |
MAIN RENDER |
| 634 |
=========== */ |
| 635 |
|
| 636 |
public function render(string $new_post_query_string = ''): void { |
| 637 |
?> |
| 638 |
<div class="pp-program-summary" id="pp-program-card" |
| 639 |
data-feed-slug="<?php echo esc_attr($this->feed_slug); ?>" |
| 640 |
data-program-keyword="<?php echo esc_attr($this->program_keyword); ?>" |
| 641 |
data-default-program="<?php echo esc_attr($this->default_program_keyword); ?>"> |
| 642 |
|
| 643 |
<div class="pp-settings-program-summary"> |
| 644 |
<?php $this->render_stats_widget(); ?> |
| 645 |
|
| 646 |
<div class="pp-program-row row"> |
| 647 |
<div class="col-md-12"> |
| 648 |
<?php $this->render_show_info_card(); ?> |
| 649 |
</div> |
| 650 |
</div> |
| 651 |
</div> |
| 652 |
</div> |
| 653 |
<?php |
| 654 |
} |
| 655 |
|
| 656 |
/* ====================== |
| 657 |
STATS WIDGET RENDERING |
| 658 |
====================== */ |
| 659 |
|
| 660 |
public function render_stats_widget(bool $stacked = false): void { |
| 661 |
// no widget when not connected |
| 662 |
if (empty($this->stats_content) && !$this->deferred) return; |
| 663 |
|
| 664 |
// template variables |
| 665 |
$nonce = wp_create_nonce('powerpress_stats_program'); |
| 666 |
$ajax_url = admin_url('admin-ajax.php'); |
| 667 |
$chart_data = $this->deferred ? null : $this->get_week_chart_data(); |
| 668 |
$deferred = $this->deferred; |
| 669 |
$stats_content = $this->stats_content; |
| 670 |
$program_card = $this; |
| 671 |
|
| 672 |
// load shared template |
| 673 |
include(POWERPRESS_ABSPATH . '/views/stats-widget.php'); |
| 674 |
} |
| 675 |
|
| 676 |
public function render_stats_header(): void { |
| 677 |
$current_program_title = ''; |
| 678 |
if ($this->network_mode && !empty($this->programs)) { |
| 679 |
$current_program_title = $this->programs[$this->program_keyword] ?? ''; |
| 680 |
} |
| 681 |
?> |
| 682 |
<div class="pp-stats-header"> |
| 683 |
<div class="pp-stats-header-left"> |
| 684 |
<h2 class="pp-stats-title"><span class="pp-stats-title-text"><?php _e('Podcast Statistics', 'powerpress'); ?><?php echo ($this->network_mode && !empty($this->programs)) ? ':' : ''; ?></span></h2><?php if ($this->network_mode && !empty($this->programs)) : ?><button type="button" class="pp-program-selector-trigger" id="pp-program-selector-trigger" aria-haspopup="listbox" aria-expanded="false" aria-label="<?php printf(esc_attr__('Select podcast program. Currently showing: %s', 'powerpress'), esc_attr($current_program_title)); ?>"> |
| 685 |
<span class="pp-program-selector-name"><?php echo esc_html($current_program_title); ?></span> |
| 686 |
<span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span> |
| 687 |
</button> |
| 688 |
<div class="pp-program-selector-dropdown" id="pp-program-selector-dropdown" role="listbox" aria-label="<?php esc_attr_e('Podcast programs', 'powerpress'); ?>"> |
| 689 |
<?php $this->render_program_selector_list(); ?> |
| 690 |
</div> |
| 691 |
<?php endif; ?> |
| 692 |
</div> |
| 693 |
<div class="pp-stats-header-controls"> |
| 694 |
<button type="button" class="pp-stats-btn pp-stats-cog-btn" id="pp-stats-cog-btn" data-tier="<?php echo esc_attr($this->get_stats_tier()); ?>" title="<?php esc_attr_e('Chart options', 'powerpress'); ?>" aria-label="<?php esc_attr_e('Open chart options', 'powerpress'); ?>" aria-expanded="false"> |
| 695 |
<span class="dashicons dashicons-admin-generic pp-cog-icon" aria-hidden="true"></span> |
| 696 |
<span class="dashicons dashicons-yes-alt pp-save-icon" style="display:none;" aria-hidden="true"></span> |
| 697 |
</button> |
| 698 |
</div> |
| 699 |
</div> |
| 700 |
<?php |
| 701 |
if ($this->get_stats_tier() !== 'basic') { |
| 702 |
$this->render_stats_settings_row(); |
| 703 |
} |
| 704 |
} |
| 705 |
|
| 706 |
/** |
| 707 |
* only for non-basic tier users |
| 708 |
*/ |
| 709 |
private function render_stats_settings_row(): void { |
| 710 |
?> |
| 711 |
<div class="pp-stats-settings-row" id="pp-stats-settings-row" style="display:none;"> |
| 712 |
<div class="pp-stats-settings-line"> |
| 713 |
<span class="pp-stats-setting-label"><?php esc_html_e('View:', 'powerpress'); ?></span> |
| 714 |
<div class="pp-stats-toggle-group"> |
| 715 |
<label class="pp-stats-toggle" title="<?php esc_attr_e('Show last 7 days', 'powerpress'); ?>"> |
| 716 |
<input type="radio" name="pp_stats_view" value="week" checked> |
| 717 |
<span><?php esc_html_e('Week', 'powerpress'); ?></span> |
| 718 |
</label> |
| 719 |
<label class="pp-stats-toggle" title="<?php esc_attr_e('Show last 30 days', 'powerpress'); ?>"> |
| 720 |
<input type="radio" name="pp_stats_view" value="month"> |
| 721 |
<span><?php esc_html_e('Month', 'powerpress'); ?></span> |
| 722 |
</label> |
| 723 |
</div> |
| 724 |
</div> |
| 725 |
<div class="pp-stats-settings-line"> |
| 726 |
<span class="pp-stats-setting-label"><?php esc_html_e('Scale:', 'powerpress'); ?></span> |
| 727 |
<div class="pp-stats-toggle-group"> |
| 728 |
<label class="pp-stats-toggle" title="<?php esc_attr_e('Standard view - equal spacing between values', 'powerpress'); ?>"> |
| 729 |
<input type="radio" name="pp_stats_scale" value="linear" checked> |
| 730 |
<span><?php esc_html_e('Linear', 'powerpress'); ?></span> |
| 731 |
</label> |
| 732 |
<label class="pp-stats-toggle" title="<?php esc_attr_e('Better for viral spikes - compresses large values', 'powerpress'); ?>"> |
| 733 |
<input type="radio" name="pp_stats_scale" value="log"> |
| 734 |
<span><?php esc_html_e('Log', 'powerpress'); ?></span> |
| 735 |
</label> |
| 736 |
</div> |
| 737 |
</div> |
| 738 |
<div class="pp-stats-settings-line"> |
| 739 |
<span class="pp-stats-setting-label"><?php esc_html_e('Show:', 'powerpress'); ?></span> |
| 740 |
<div class="pp-stats-toggle-group pp-stats-toggle-group--checkboxes"> |
| 741 |
<label class="pp-stats-toggle" title="<?php esc_attr_e('Growth direction over time', 'powerpress'); ?>"> |
| 742 |
<input type="checkbox" id="pp-stats-opt-trendline" checked> |
| 743 |
<span><?php esc_html_e('Trend', 'powerpress'); ?></span> |
| 744 |
</label> |
| 745 |
<label class="pp-stats-toggle" title="<?php esc_attr_e('Daily average reference line', 'powerpress'); ?>"> |
| 746 |
<input type="checkbox" id="pp-stats-opt-avgline" checked> |
| 747 |
<span><?php esc_html_e('Avg', 'powerpress'); ?></span> |
| 748 |
</label> |
| 749 |
<label class="pp-stats-toggle" id="pp-stats-opt-values-label" title="<?php esc_attr_e('Download counts on bars (week view only)', 'powerpress'); ?>"> |
| 750 |
<input type="checkbox" id="pp-stats-opt-values" checked> |
| 751 |
<span><?php esc_html_e('Values', 'powerpress'); ?></span> |
| 752 |
</label> |
| 753 |
</div> |
| 754 |
</div> |
| 755 |
<div class="pp-stats-settings-line"> |
| 756 |
<button type="button" class="pp-stats-refresh-btn" id="pp-stats-refresh-btn" title="<?php esc_attr_e('Clear cached data and fetch fresh stats', 'powerpress'); ?>"> |
| 757 |
<span class="dashicons dashicons-update"></span> |
| 758 |
<span><?php _e('Refresh Stats', 'powerpress'); ?></span> |
| 759 |
</button> |
| 760 |
</div> |
| 761 |
</div> |
| 762 |
<?php |
| 763 |
} |
| 764 |
|
| 765 |
public function render_stats_chart(): void { |
| 766 |
// build screen reader description from current data |
| 767 |
$day_total_data = $this->stats_content['day_total_data'] ?? []; |
| 768 |
$num_days = count($day_total_data); |
| 769 |
|
| 770 |
// get todays value |
| 771 |
$today_data = !empty($day_total_data) ? end($day_total_data) : []; |
| 772 |
$today_total = (int)($today_data['trending_day_total'] ?? 0); |
| 773 |
|
| 774 |
// calculate average |
| 775 |
$total_downloads = 0; |
| 776 |
foreach ($day_total_data as $day) { |
| 777 |
$total_downloads += (int)($day['trending_day_total'] ?? 0); |
| 778 |
} |
| 779 |
$avg_downloads = $num_days > 0 ? round($total_downloads / $num_days) : 0; |
| 780 |
|
| 781 |
// calculate velocity (trend direction) |
| 782 |
$velocity_pct = 0; |
| 783 |
if ($num_days >= 2 && $avg_downloads > 0) { |
| 784 |
$sum_x = 0; $sum_y = 0; $sum_xy = 0; $sum_x2 = 0; |
| 785 |
$i = 0; |
| 786 |
foreach ($day_total_data as $day) { |
| 787 |
$val = (int)($day['trending_day_total'] ?? 0); |
| 788 |
$sum_x += $i; |
| 789 |
$sum_y += $val; |
| 790 |
$sum_xy += $i * $val; |
| 791 |
$sum_x2 += $i * $i; |
| 792 |
$i++; |
| 793 |
} |
| 794 |
$denom = $num_days * $sum_x2 - $sum_x * $sum_x; |
| 795 |
if ($denom != 0) { |
| 796 |
$slope = ($num_days * $sum_xy - $sum_x * $sum_y) / $denom; |
| 797 |
$velocity_pct = round(($slope / $avg_downloads) * 100 * $num_days); |
| 798 |
} |
| 799 |
} |
| 800 |
|
| 801 |
// sentiment based on velocity |
| 802 |
if ($velocity_pct > 5) { |
| 803 |
$sentiment = __("Your podcast is growing - keep posting!", 'powerpress'); |
| 804 |
} elseif ($velocity_pct < -5) { |
| 805 |
$sentiment = __("Downloads are slowing down. Time for fresh content!", 'powerpress'); |
| 806 |
} else { |
| 807 |
$sentiment = __("Your podcast is holding steady.", 'powerpress'); |
| 808 |
} |
| 809 |
|
| 810 |
// build description |
| 811 |
$sr_parts = []; |
| 812 |
if ($today_total == 0) { |
| 813 |
$sr_parts[] = __("No downloads today", 'powerpress'); |
| 814 |
} else { |
| 815 |
$sr_parts[] = sprintf(__("You had %s downloads today", 'powerpress'), number_format($today_total)); |
| 816 |
} |
| 817 |
$sr_parts[] = sprintf(__("with a daily average of %s", 'powerpress'), number_format($avg_downloads)); |
| 818 |
if ($this->get_stats_tier() !== 'basic' && $velocity_pct != 0) { |
| 819 |
$direction = $velocity_pct > 0 ? __('up', 'powerpress') : __('down', 'powerpress'); |
| 820 |
$sr_parts[] = sprintf(__("Trend is %s %d percent", 'powerpress'), $direction, abs($velocity_pct)); |
| 821 |
} |
| 822 |
$sr_parts[] = $sentiment; |
| 823 |
|
| 824 |
$sr_description = implode(' ', $sr_parts); |
| 825 |
?> |
| 826 |
<span id="pp-stats-chart-description" class="pp-screen-reader-text"><?php echo esc_html($sr_description); ?></span> |
| 827 |
<div class="pp-stats-chart" role="img" aria-label="<?php esc_attr_e('Podcast download statistics', 'powerpress'); ?>" aria-describedby="pp-stats-chart-description"> |
| 828 |
<canvas id="pp-stats-widget__canvas" aria-hidden="true"></canvas> |
| 829 |
</div> |
| 830 |
<?php |
| 831 |
} |
| 832 |
|
| 833 |
public function render_stats_summary(): void { |
| 834 |
?> |
| 835 |
<div class="pp-stats-summary" id="pp-stats-summary"> |
| 836 |
<table class="pp-stats-summary__table" role="table" aria-label="<?php esc_attr_e('Download statistics summary', 'powerpress'); ?>"> |
| 837 |
<tr class="pp-stats-summary__item--first"> |
| 838 |
<td class="pp-stats-summary__label">Today</td> |
| 839 |
<?php |
| 840 |
$day_total_data = $this->stats_content['day_total_data'] ?? []; |
| 841 |
$today_data = end($day_total_data) ?: []; |
| 842 |
$yesterday_data = prev($day_total_data) ?: []; |
| 843 |
$today_total = (int)($today_data['trending_day_total'] ?? 0); |
| 844 |
$yesterday_total = (int)($yesterday_data['trending_day_total'] ?? 0); |
| 845 |
|
| 846 |
if ($today_total < $yesterday_total) { |
| 847 |
$day_img_src = powerpress_get_root_url() . 'images/down_arrow_pink.svg'; |
| 848 |
$day_change_text = "Decreased from yesterday"; |
| 849 |
} elseif ($today_total > $yesterday_total) { |
| 850 |
$day_img_src = powerpress_get_root_url() . 'images/up_arrow_pink.svg'; |
| 851 |
$day_change_text = "Increased from yesterday"; |
| 852 |
} else { |
| 853 |
$day_img_src = powerpress_get_root_url() . 'images/audio_lines_pink.svg'; |
| 854 |
$day_change_text = "Unchanged from yesterday"; |
| 855 |
} |
| 856 |
?> |
| 857 |
<td class="pp-stats-summary__data" aria-label="<?php echo number_format($today_total) . " downloads today. $day_change_text"; ?>"> |
| 858 |
<?php echo number_format($today_total); ?> |
| 859 |
<div class="pp-stats-summary__icon" title="<?php echo $day_change_text; ?>"> |
| 860 |
<img alt="today" src="<?php echo esc_url($day_img_src); ?>"/> |
| 861 |
</div> |
| 862 |
</td> |
| 863 |
</tr> |
| 864 |
<tr class="pp-stats-summary__item"> |
| 865 |
<td class="pp-stats-summary__label" id="pp-avg-label">7 Day Average</td> |
| 866 |
<?php |
| 867 |
$week_average = $this->get_week_average(); |
| 868 |
// icon based on week-over-week trend (compare to previous 7 days) |
| 869 |
$week_change = $this->stats_content['month_average_change'] ?? ''; |
| 870 |
switch ($week_change) { |
| 871 |
case 'up': |
| 872 |
$avg_img_src = powerpress_get_root_url() . 'images/up_arrow.svg'; |
| 873 |
$avg_change_text = "Increased from last week"; |
| 874 |
break; |
| 875 |
case 'down': |
| 876 |
$avg_img_src = powerpress_get_root_url() . 'images/down_arrow.svg'; |
| 877 |
$avg_change_text = "Decreased from last week"; |
| 878 |
break; |
| 879 |
default: |
| 880 |
$avg_img_src = powerpress_get_root_url() . 'images/audio_lines.svg'; |
| 881 |
$avg_change_text = "Unchanged from last week"; |
| 882 |
break; |
| 883 |
} |
| 884 |
?> |
| 885 |
<td class="pp-stats-summary__data" id="pp-avg-value" aria-label="<?php echo number_format($week_average) . " average downloads for the past week. $avg_change_text"; ?>"> |
| 886 |
<?php echo number_format($week_average); ?> |
| 887 |
<div class="pp-stats-summary__icon" title="<?php echo $avg_change_text; ?>"> |
| 888 |
<img alt="average change" src="<?php echo esc_url($avg_img_src); ?>"/> |
| 889 |
</div> |
| 890 |
</td> |
| 891 |
</tr> |
| 892 |
<tr class="pp-stats-summary__item"> |
| 893 |
<td class="pp-stats-summary__label">Total Downloads</td> |
| 894 |
<?php |
| 895 |
if ($today_total > 0) { |
| 896 |
$total_img_src = powerpress_get_root_url() . 'images/up_arrow.svg'; |
| 897 |
$total_change_text = "Increased from yesterday"; |
| 898 |
} else { |
| 899 |
$total_img_src = powerpress_get_root_url() . 'images/audio_lines.svg'; |
| 900 |
$total_change_text = "Unchanged from yesterday"; |
| 901 |
} |
| 902 |
$program_total = $this->stats_content['program_total'] ?? 0; |
| 903 |
?> |
| 904 |
<td class="pp-stats-summary__data" aria-label="<?php echo number_format($program_total) . " total downloads for program. $total_change_text"; ?>"> |
| 905 |
<?php echo number_format($program_total); ?> |
| 906 |
<div class="pp-stats-summary__icon" title="<?php echo $total_change_text; ?>"> |
| 907 |
<img alt="total" src="<?php echo esc_url($total_img_src); ?>"/> |
| 908 |
</div> |
| 909 |
</td> |
| 910 |
</tr> |
| 911 |
</table> |
| 912 |
<?php if (!empty($this->stats_content['is_empty'])) : ?> |
| 913 |
<p class="pp-stats-empty-message"> |
| 914 |
<?php _e('Your podcast is just getting started! Downloads will appear here once listeners tune in.', 'powerpress'); ?> |
| 915 |
</p> |
| 916 |
<?php endif; ?> |
| 917 |
<?php |
| 918 |
$program_id = !empty($this->programs_full[$this->program_keyword]['program_id']) |
| 919 |
? $this->programs_full[$this->program_keyword]['program_id'] |
| 920 |
: ''; |
| 921 |
$stats_link = $program_id |
| 922 |
? rtrim($this->stats_base_url, '/') . '/s-' . $program_id . '/' |
| 923 |
: $this->stats_base_url; |
| 924 |
?> |
| 925 |
<div class="pp-stats-footer-actions"> |
| 926 |
<a class="pp-stats-btn pp-stats-btn--primary" href="<?php echo esc_url(admin_url('post-new.php')); ?>"> |
| 927 |
<?php esc_html_e('New Episode', 'powerpress'); ?> |
| 928 |
</a> |
| 929 |
<a id="pp-stats-advanced-link" class="pp-stats-btn pp-stats-btn--secondary" href="<?php echo esc_url($stats_link); ?>" target="_blank"> |
| 930 |
<?php esc_html_e('See all statistics', 'powerpress'); ?> |
| 931 |
</a> |
| 932 |
</div> |
| 933 |
</div> |
| 934 |
<?php |
| 935 |
} |
| 936 |
|
| 937 |
|
| 938 |
/** splits programs into with/without stats for selector ordering */ |
| 939 |
private function get_sorted_programs(): array { |
| 940 |
$no_stats_programs = get_transient('powerpress_no_stats_programs'); |
| 941 |
if (!is_array($no_stats_programs)) $no_stats_programs = []; |
| 942 |
|
| 943 |
$with_stats = []; |
| 944 |
$without_stats = []; |
| 945 |
foreach ($this->programs as $keyword => $title) { |
| 946 |
if (isset($no_stats_programs[$keyword])) { |
| 947 |
$without_stats[$keyword] = $title; |
| 948 |
} else { |
| 949 |
$with_stats[$keyword] = $title; |
| 950 |
} |
| 951 |
} |
| 952 |
|
| 953 |
ksort($with_stats); |
| 954 |
ksort($without_stats); |
| 955 |
|
| 956 |
return ['with_stats' => $with_stats, 'without_stats' => $without_stats]; |
| 957 |
} |
| 958 |
|
| 959 |
/** renders <select> dropdown for program switching */ |
| 960 |
private function render_program_selector_options(): void { |
| 961 |
$sorted = $this->get_sorted_programs(); |
| 962 |
?> |
| 963 |
<select id="pp-stats-program-select" name="pp_stats_program"> |
| 964 |
<?php foreach ($sorted['with_stats'] as $keyword => $title) : ?> |
| 965 |
<option value="<?php echo esc_attr($keyword); ?>"<?php echo ((string)$this->program_keyword === (string)$keyword) ? ' selected' : ''; ?>><?php echo esc_html($title); ?></option> |
| 966 |
<?php endforeach; ?> |
| 967 |
<?php foreach ($sorted['without_stats'] as $keyword => $title) : ?> |
| 968 |
<option value="<?php esc_attr_e($keyword); ?>" disabled><?php echo esc_html($title); ?> (<?php _e('stats not enabled', 'powerpress'); ?>)</option> |
| 969 |
<?php endforeach; ?> |
| 970 |
</select> |
| 971 |
<?php |
| 972 |
} |
| 973 |
|
| 974 |
/** renders accessible listbox for inline program selector */ |
| 975 |
private function render_program_selector_list(): void { |
| 976 |
$sorted = $this->get_sorted_programs(); |
| 977 |
?> |
| 978 |
<ul class="pp-program-selector-list" role="presentation"> |
| 979 |
<?php foreach ($sorted['with_stats'] as $keyword => $title) : |
| 980 |
$is_selected = ((string)$this->program_keyword === (string)$keyword); |
| 981 |
?> |
| 982 |
<li role="option" aria-selected="<?php echo $is_selected ? 'true' : 'false'; ?>"> |
| 983 |
<button type="button" class="pp-program-selector-item<?php echo $is_selected ? ' pp-program-selector-item--active' : ''; ?>" data-keyword="<?php echo esc_attr($keyword); ?>"> |
| 984 |
<?php echo esc_html($title); ?> |
| 985 |
</button> |
| 986 |
</li> |
| 987 |
<?php endforeach; ?> |
| 988 |
<?php if (!empty($sorted['without_stats'])) : ?> |
| 989 |
<li class="pp-program-selector-divider" role="separator" aria-hidden="true"></li> |
| 990 |
<?php foreach ($sorted['without_stats'] as $keyword => $title) : ?> |
| 991 |
<li role="option" aria-disabled="true" aria-selected="false"> |
| 992 |
<button type="button" class="pp-program-selector-item pp-program-selector-item--disabled" disabled aria-label="<?php printf(esc_attr__('%s - stats not enabled', 'powerpress'), esc_attr($title)); ?>"> |
| 993 |
<?php echo esc_html($title); ?> |
| 994 |
<span class="pp-program-selector-item-note"><?php _e('stats not enabled', 'powerpress'); ?></span> |
| 995 |
</button> |
| 996 |
</li> |
| 997 |
<?php endforeach; ?> |
| 998 |
<?php endif; ?> |
| 999 |
</ul> |
| 1000 |
<?php |
| 1001 |
} |
| 1002 |
|
| 1003 |
/* ======================== |
| 1004 |
SHOW INFO CARD RENDERING |
| 1005 |
======================== */ |
| 1006 |
|
| 1007 |
private function render_show_info_card(): void { |
| 1008 |
$podcast_title = $this->program_info['title'] ?: __('Podcast', 'powerpress'); |
| 1009 |
$episode_count = intval($this->program_info['episode_count']); |
| 1010 |
$img_alt = sprintf(__('Cover art for %s', 'powerpress'), $podcast_title); |
| 1011 |
?> |
| 1012 |
<div class="prog-sum-head" id="pp-show-info-card" role="region" aria-labelledby="welcome-title"> |
| 1013 |
<div class="pp-program-card"> |
| 1014 |
<div class="pp-program-card__header"> |
| 1015 |
<h2 class="pp-heading" id="welcome-title"><?php echo esc_html($podcast_title); ?></h2> |
| 1016 |
<div class="pp-program-card__episode-count" aria-label="<?php printf(esc_attr__('%d episodes published', 'powerpress'), $episode_count); ?>"> |
| 1017 |
<span class="pp-program-card__label" aria-hidden="true"><?php _e('Episodes', 'powerpress'); ?>:</span> |
| 1018 |
<span class="pp-episode-count" aria-hidden="true"><?php echo $episode_count; ?></span> |
| 1019 |
</div> |
| 1020 |
</div> |
| 1021 |
<div class="pp-program-card__body"> |
| 1022 |
<img id="welcome-preview-image" src="<?php echo esc_url($this->program_info['image']); ?>" alt="<?php echo esc_attr($img_alt); ?>" onerror="this.onerror=null; this.src='<?php echo esc_url(powerpress_get_root_url() . 'images/pts_cover.jpg'); ?>';" /> |
| 1023 |
<div class="pp-program-card__content"> |
| 1024 |
<p class="pp-program-card__author"><span class="pp-program-card__label"><?php _e('By', 'powerpress'); ?></span> <?php echo esc_html($this->program_info['author']); ?></p> |
| 1025 |
|
| 1026 |
<div class="pp-program-card__main"> |
| 1027 |
<p class="pp-program-card__description"><span class="pp-program-card__label"><?php _e('Description', 'powerpress'); ?>:</span> <?php echo esc_html($this->program_info['description']); ?></p> |
| 1028 |
</div> |
| 1029 |
|
| 1030 |
<div class="pp-program-card__footer"> |
| 1031 |
<p class="pp-program-card__category"<?php echo empty($this->program_info['category']) ? ' style="display: none;"' : ''; ?>><span class="pp-program-card__label"><?php _e('Category', 'powerpress'); ?>:</span> <?php echo esc_html($this->program_info['category']); ?></p> |
| 1032 |
<div class="pp-program-card__dates"> |
| 1033 |
<span class="pp-program-card__last-update"<?php echo empty($this->program_info['last_update']) ? ' style="display: none;"' : ''; ?>><span class="pp-program-card__label"><?php _e('Last Upload', 'powerpress'); ?>:</span> <?php echo esc_html($this->format_date($this->program_info['last_update'])); ?></span> |
| 1034 |
<span class="pp-program-card__date-separator" aria-hidden="true"<?php echo (empty($this->program_info['last_update']) || empty($this->program_info['created_date'])) ? ' style="display: none;"' : ''; ?>>|</span> |
| 1035 |
<span class="pp-program-card__created-date"<?php echo empty($this->program_info['created_date']) ? ' style="display: none;"' : ''; ?>><span class="pp-program-card__label"><?php _e('Started Publishing', 'powerpress'); ?>:</span> <?php echo esc_html($this->format_date($this->program_info['created_date'])); ?></span> |
| 1036 |
</div> |
| 1037 |
</div> |
| 1038 |
</div> |
| 1039 |
</div> |
| 1040 |
</div> |
| 1041 |
</div> |
| 1042 |
<?php |
| 1043 |
} |
| 1044 |
|
| 1045 |
/** checks if url matches any known default/placeholder artwork */ |
| 1046 |
private function is_placeholder_image(string $url): bool { |
| 1047 |
if (empty($url)) return true; |
| 1048 |
// api default, pp grey logo, pp blue logo |
| 1049 |
return strpos($url, 'default.jpg') !== false |
| 1050 |
|| strpos($url, 'pts_cover.jpg') !== false |
| 1051 |
|| strpos($url, 'itunes_default.jpg') !== false; |
| 1052 |
} |
| 1053 |
|
| 1054 |
/** formats unix timestamp or date string using WP date format */ |
| 1055 |
private function format_date($timestamp): string { |
| 1056 |
if (empty($timestamp)) return ''; |
| 1057 |
if (is_numeric($timestamp)) |
| 1058 |
return date_i18n(get_option('date_format'), $timestamp); |
| 1059 |
$ts = strtotime($timestamp); |
| 1060 |
if ($ts === false || $ts < 0) return ''; |
| 1061 |
return date_i18n(get_option('date_format'), $ts); |
| 1062 |
} |
| 1063 |
|
| 1064 |
/* ===================== |
| 1065 |
AJAX RESPONSE METHODS |
| 1066 |
===================== */ |
| 1067 |
|
| 1068 |
public function get_stats_widget_html(bool $stacked = false): string { |
| 1069 |
ob_start(); |
| 1070 |
$this->render_stats_widget($stacked); |
| 1071 |
return ob_get_clean(); |
| 1072 |
} |
| 1073 |
|
| 1074 |
public function get_show_info_card_html(): string { |
| 1075 |
ob_start(); |
| 1076 |
$this->render_show_info_card(); |
| 1077 |
return ob_get_clean(); |
| 1078 |
} |
| 1079 |
|
| 1080 |
public function get_program_info_data(): array { |
| 1081 |
return $this->program_info; |
| 1082 |
} |
| 1083 |
|
| 1084 |
/* ========= |
| 1085 |
ACCESSORS |
| 1086 |
========= */ |
| 1087 |
|
| 1088 |
public function is_network_mode(): bool { return $this->network_mode; } |
| 1089 |
public function get_programs(): array { return $this->programs; } |
| 1090 |
public function get_current_program_keyword(): string { return $this->program_keyword; } |
| 1091 |
|
| 1092 |
/* =================== |
| 1093 |
DELTA CACHE HELPERS |
| 1094 |
=================== */ |
| 1095 |
|
| 1096 |
/** determines how many days to fetch based on cache gap */ |
| 1097 |
private function calculate_stats_delta(array $cached_days, string $today, int $max_days): array { |
| 1098 |
if (empty($cached_days)) { |
| 1099 |
return ['fetch_days' => $max_days, 'is_full_fetch' => true]; |
| 1100 |
} |
| 1101 |
|
| 1102 |
$last_date = max(array_keys($cached_days)); |
| 1103 |
$last_ts = strtotime($last_date); |
| 1104 |
$today_ts = strtotime($today); |
| 1105 |
|
| 1106 |
$gap_days = (int)(($today_ts - $last_ts) / 86400); |
| 1107 |
|
| 1108 |
// same day = still fetch 1 day for today refresh |
| 1109 |
if ($gap_days <= 0) { |
| 1110 |
return ['fetch_days' => 1, 'is_full_fetch' => false]; |
| 1111 |
} |
| 1112 |
|
| 1113 |
// gap exceeds retention = full fetch |
| 1114 |
if ($gap_days >= $max_days) { |
| 1115 |
return ['fetch_days' => $max_days, 'is_full_fetch' => true]; |
| 1116 |
} |
| 1117 |
|
| 1118 |
return ['fetch_days' => $gap_days, 'is_full_fetch' => false]; |
| 1119 |
} |
| 1120 |
|
| 1121 |
/** |
| 1122 |
* merge daily data from data API format (date/downloads keys) |
| 1123 |
*/ |
| 1124 |
private function merge_stats_days_from_data_api(array $existing, array $new_data): array { |
| 1125 |
foreach ($new_data as $day) { |
| 1126 |
if (isset($day['date']) && isset($day['downloads'])) { |
| 1127 |
$existing[$day['date']] = (int)$day['downloads']; |
| 1128 |
} |
| 1129 |
} |
| 1130 |
return $existing; |
| 1131 |
} |
| 1132 |
|
| 1133 |
/** drops dates older than retention window */ |
| 1134 |
private function prune_stats_days(array $days_data, string $today, int $retention_days): array { |
| 1135 |
$cutoff_ts = strtotime($today) - ($retention_days * 86400); |
| 1136 |
|
| 1137 |
return array_filter($days_data, function($date) use ($cutoff_ts) { |
| 1138 |
return strtotime($date) >= $cutoff_ts; |
| 1139 |
}, ARRAY_FILTER_USE_KEY); |
| 1140 |
} |
| 1141 |
|
| 1142 |
/** |
| 1143 |
* fills gaps with zeros to prevent unnecessary refetches |
| 1144 |
*/ |
| 1145 |
private function fill_missing_dates(array $days_data, string $today, int $window): array { |
| 1146 |
$today_ts = strtotime($today); |
| 1147 |
for ($i = 0; $i < $window; $i++) { |
| 1148 |
$date = date('Y-m-d', $today_ts - ($i * 86400)); |
| 1149 |
if (!isset($days_data[$date])) { |
| 1150 |
$days_data[$date] = 0; |
| 1151 |
} |
| 1152 |
} |
| 1153 |
return $days_data; |
| 1154 |
} |
| 1155 |
|
| 1156 |
/** converts date=>total map to [{day_date, trending_day_total}] */ |
| 1157 |
private function days_to_api_format(array $days_data): array { |
| 1158 |
ksort($days_data); |
| 1159 |
$result = []; |
| 1160 |
foreach ($days_data as $date => $total) { |
| 1161 |
$result[] = ['day_date' => $date, 'trending_day_total' => $total]; |
| 1162 |
} |
| 1163 |
return $result; |
| 1164 |
} |
| 1165 |
|
| 1166 |
/** validate YYYY-MM-DD format */ |
| 1167 |
private function is_valid_cache_date($date): bool { |
| 1168 |
if (!is_string($date) || empty($date)) { |
| 1169 |
return false; |
| 1170 |
} |
| 1171 |
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1; |
| 1172 |
} |
| 1173 |
|
| 1174 |
/** |
| 1175 |
* clears legacy cache formats (v1/v2/v3) |
| 1176 |
*/ |
| 1177 |
private function migrate_stats_cache(array $stats_cached, string $keyword): array { |
| 1178 |
$modified = false; |
| 1179 |
|
| 1180 |
// clear old v1 format (simple updated/content keys) |
| 1181 |
if (isset($stats_cached['updated']) || isset($stats_cached['content'])) { |
| 1182 |
unset($stats_cached['updated'], $stats_cached['content'], $stats_cached['retry_count']); |
| 1183 |
$modified = true; |
| 1184 |
} |
| 1185 |
|
| 1186 |
// clear old v2/v3 format keys |
| 1187 |
foreach (array_keys($stats_cached) as $key) { |
| 1188 |
if (preg_match('/_v[23]$/', $key)) { |
| 1189 |
unset($stats_cached[$key]); |
| 1190 |
$modified = true; |
| 1191 |
} |
| 1192 |
} |
| 1193 |
|
| 1194 |
if ($modified) { |
| 1195 |
update_option('powerpress_stats', $stats_cached); |
| 1196 |
} |
| 1197 |
|
| 1198 |
return $stats_cached; |
| 1199 |
} |
| 1200 |
} |
| 1201 |
|