public function maybe_render_episode(){
if(!$this->is_episode_request()){ return; }
global $wpdb;
$show = sanitize_title(get_query_var('pp_tv_show'));
$s = (int) get_query_var('pp_tv_s');
$e = (int) get_query_var('pp_tv_e');
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->tbl_eps} WHERE show_slug=%s AND season_number=%d AND episode_number=%d LIMIT 1",
$show, $s, $e
), ARRAY_A
);
if(!$row){
status_header(404);
nocache_headers();
get_header();
echo '
';
echo '
← Back to show';
echo '
'.esc_html($title).'
';
$hero_url = '';
$hero_is_local = false;
if(!empty($row['still_attachment_id'])){
$hero_url = wp_get_attachment_image_url((int)$row['still_attachment_id'],'large');
$hero_is_local = true;
}
if(!$hero_url && !empty($row['still_path'])){
$hero_url = 'https://image.tmdb.org/t/p/w780'.$row['still_path'];
}
$hero_alt = $this->episode_alt_text($row['show_title'], (int)$row['season_number'], (int)$row['episode_number'], $row['title']);
if($hero_url){
echo '
.')
';
if(!empty($row['overview'])){
echo '
'.esc_html($row['overview']).'
';
}
if(!$hero_is_local){
echo '
Image temporarily served from TMDB until local copy is saved.
';
}
} elseif(!empty($row['overview'])){
echo '
'.esc_html($row['overview']).'
';
}
echo '
';
$yt_raw = $this->val($row, array('youtube_video_id','youtube_id','yt_id','youtube','trailer_youtube_id'));
$yt = $this->extract_youtube_id($yt_raw);
if($yt){
echo '
';
}
if ( function_exists('pp_tv_render_episode_sections') ) {
echo pp_tv_render_episode_sections( $row );
} elseif ( function_exists('pp_tv_render_episode_ai_sections') ) {
echo pp_tv_render_episode_ai_sections( $row );
}
// Credits render
$main_cast=[]; $writing=[]; $directing=[]; $buckets=[];
foreach($credits as $c){
$dept = $c['department'] ? $c['department'] : ($c['credit_type']==='cast' ? 'Acting' : '');
$job = $c['job'] ? $c['job'] : ($c['credit_type']==='cast' ? 'Cast' : '');
$pid = (int)$c['tmdb_person_id'];
$person = isset($people[$pid]) ? $people[$pid] : null;
// PERSON PHOTO POLICY:
// Only use LOCAL photo_id; never hotlink TMDB and never queue downloads.
$photo = '';
if($person && !empty($person->photo_id)){
$photo = wp_get_attachment_image_url((int)$person->photo_id, 'medium');
}
$pname = $person ? $person->name : $c['person_name'];
$item = array(
'name'=>$pname,
'photo'=>$photo,
'character'=> isset($c['character_name']) ? $c['character_name'] : '',
'job'=>$job,
'dept'=>$dept,
'order'=> isset($c['order_num']) && $c['order_num']!==null ? (int)$c['order_num'] : 9999,
);
if($c['credit_type']==='cast'){
$main_cast[] = $item;
} elseif(strtolower($dept)==='writing' || stripos($job,'writer')!==false || stripos($job,'script')!==false){
$writing[] = $item;
} elseif(strtolower($dept)==='directing' || stripos($job,'director')!==false){
$directing[] = $item;
} else {
$key = $dept ? $dept : 'Other';
if(!isset($buckets[$key])) $buckets[$key] = array();
$buckets[$key][] = $item;
}
}
usort($main_cast, function($a,$b){ return $a['order'] - $b['order']; });
usort($writing, function($a,$b){ return strcmp($a['name'],$b['name']); });
usort($directing, function($a,$b){ return strcmp($a['name'],$b['name']); });
ksort($buckets);
if($main_cast){
echo '
Cast
';
$max = min(5, count($main_cast));
for($i=0; $i<$max; $i++){ $this->render_person_card($main_cast[$i], true); }
echo '
';
if(count($main_cast)>5){
echo '
Show '.(count($main_cast)-5).' more cast
';
for($i=5; $irender_person_card($main_cast[$i], true); }
echo '
';
}
}
echo '
';
echo '
Writing
';
if($writing){ echo '
'; foreach($writing as $w){ echo '- '.$this->crew_line($w).'
'; } echo '
'; } else { echo '
—
'; }
echo '
';
echo '
Directing
';
if($directing){ echo '
'; foreach($directing as $d){ echo '- '.$this->crew_line($d).'
'; } echo '
'; } else { echo '
—
'; }
echo '
';
echo '
';
echo $this->render_season_browser($row['show_slug'], (int)$row['season_number']);
echo '
This product uses the TMDB API but is not endorsed or certified by TMDB.
';
echo '
';
get_footer();
exit;
}
/* ==================== SEO ==================== */
public function maybe_disable_seo_plugins(){
if(!$this->is_episode_request()) return;
if(has_action('wpseo_head')) remove_all_actions('wpseo_head'); // Yoast
add_filter('rank_math/frontend/disable', '__return_true', 99);
add_filter('aioseo_disable', '__return_true', 99);
}
public function print_episode_seo_head(){
if(!$this->is_episode_request()) return;
global $wpdb;
$show = sanitize_title(get_query_var('pp_tv_show'));
$s = (int) get_query_var('pp_tv_s');
$e = (int) get_query_var('pp_tv_e');
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT show_slug, show_title, season_number, episode_number, episode_slug, title, overview, air_date, still_attachment_id, still_path
FROM {$this->tbl_eps} WHERE show_slug=%s AND season_number=%d AND episode_number=%d LIMIT 1",
$show, $s, $e
), ARRAY_A
);
if(!$row) return;
$title = ($row['show_title'] ?: ucfirst($row['show_slug'])) . ' - ' . ($row['title'] ?: 'Episode') . sprintf(' (S%dE%d)', $s, $e);
$desc = trim(wp_strip_all_tags((string)$row['overview']));
if($desc==='') $desc = $title;
$img = '';
if(!empty($row['still_attachment_id'])){
$img = wp_get_attachment_image_url((int)$row['still_attachment_id'], 'large');
} elseif(!empty($row['still_path'])) {
$img = 'https://image.tmdb.org/t/p/w780'.$row['still_path'];
}
echo "\n".''."\n";
echo '
'.esc_html($title)."\n";
echo '
'."\n";
echo '
'."\n";
echo '
'."\n";
if($img) echo '
'."\n";
echo '
'."\n";
echo '
'."\n";
echo '
'."\n";
if($img) echo '
'."\n";
$ld = array(
'@context'=>'https://schema.org',
'@type'=>'TVEpisode',
'name'=> (string)$row['title'],
'description'=> $desc,
'episodeNumber'=>(int)$row['episode_number'],
'datePublished'=> (string)$row['air_date'],
'partOfSeason'=> array('@type'=>'TVSeason','seasonNumber'=>(int)$row['season_number'],'name'=>'Season '.(int)$row['season_number']),
'partOfSeries'=> array('@type'=>'TVSeries','name'=>(string)$row['show_title']),
);
if($img) $ld['image']=$img;
echo '\n";
echo ''."\n";
}
/* ==================== Helpers ==================== */
private function row_kv($k,$v){ if($v==='' || $v===null){ $v='—'; } echo '
'.esc_html($k).' | '.esc_html($v).' |
---|
'; }
private function crew_line($it){
$edit = current_user_can('manage_options') ? '
(edit)' : '';
$right = $it['job'] ? $it['job'] : $it['character'];
return '
'.esc_html($it['name']).''.$edit.'
— '.esc_html($right ? $right : 'Crew').'';
}
private function render_person_card($p, $showRole){
$edit = current_user_can('manage_options') ? '
(edit)' : '';
echo '
';
// Only show local photo if present; otherwise placeholder. Never hotlink TMDB.
if(!empty($p['photo'])){
echo '
!['.esc_attr($p['name']).']('.esc_url($p['photo']).')
';
} else {
echo '
👤
';
}
echo '
';
}
private function extract_youtube_id($in){
$in = trim((string)$in);
if($in==='') return '';
if(preg_match('~^[A-Za-z0-9_-]{6,}$~', $in)) return $in;
$parts = wp_parse_url($in);
if(!$parts) return '';
$host = isset($parts['host']) ? $parts['host'] : '';
$path = isset($parts['path']) ? trim($parts['path'], '/') : '';
if(strpos($host,'youtu.be') !== false && $path) return $path;
if(strpos($host,'youtube.com') !== false){
if(!empty($parts['query'])){ parse_str($parts['query'], $q); if(!empty($q['v'])) return $q['v']; }
if(strpos($path,'embed/')===0) return substr($path,6);
}
return '';
}
private function val($row, $candidates){
foreach((array)$candidates as $c){ if(isset($row[$c]) && $row[$c]!=='') return $row[$c]; }
return '';
}
/* ==================== Admin UI ==================== */
public function admin_menu(){
add_menu_page('TV Library','TV Library','manage_options','pp-tv-library',[$this,'page_library'],'dashicons-format-video',56);
add_submenu_page('pp-tv-library','Settings','Settings','manage_options','pp-tv-settings',[$this,'page_settings']);
add_submenu_page(null, 'Edit Episode', 'Edit Episode', 'manage_options', 'pp-tv-edit-episode', [$this,'page_edit_episode']);
}