fix_single_pagination();
$this->redirect_custom_archives();
}
/**
* Register rewrite rules.
*
* @since 1.0.0
*/
public function init() {
$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
$site_url = home_url();
// Rewrite tags
add_rewrite_tag( '%aiopod_podcast%', '([^/]+)' );
add_rewrite_tag( '%aiopod_episode%', '([^/]+)' );
add_rewrite_tag( '%aiopod_category%', '([^/]+)' );
add_rewrite_tag( '%aiopod_tag%', '([^/]+)' );
add_rewrite_tag( '%aiopod_participant%', '([^/]+)' );
add_rewrite_tag( '%aiopod_feed%', '([^/]+)' );
add_rewrite_tag( '%aiopod_download%', '([^/]+)' );
// RSS feed page
$podcast_post_type = get_post_type_object( 'aiopod_podcasts' );
if ( $podcast_post_type && ! empty( $podcast_post_type->rewrite['slug'] ) ) {
$podcast_base = trim( $podcast_post_type->rewrite['slug'], '/' );
add_rewrite_rule( "^{$podcast_base}/([^/]+)/feed/?$", 'index.php?aiopod_podcast=$matches[1]&aiopod_feed=podcast', 'top' );
}
// Episode download tracking — matches:
// /{base}/{slug}/file/{filename}.{ext} (new format, with extension)
// /{base}/{slug}/file/ (legacy format, no filename)
$episode_post_type = get_post_type_object( 'aiopod_episodes' );
if ( $episode_post_type && ! empty( $episode_post_type->rewrite['slug'] ) ) {
$episode_base = trim( $episode_post_type->rewrite['slug'], '/' );
add_rewrite_rule( "^{$episode_base}/([^/]+)/file(?:/[^/]+)?/?$", 'index.php?aiopod_episode=$matches[1]&aiopod_download=1', 'top' );
}
// Single category page
if ( ! empty( $pages_settings['category'] ) ) {
$id = (int) $pages_settings['category'];
$post = get_post( $id );
if ( $post && 'publish' === $post->post_status ) {
$permalink = get_permalink( $id );
if ( $permalink ) {
$slug = str_replace( $site_url, '', $permalink );
$slug = trim( $slug, '/' );
$slug = urldecode( $slug );
add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_category=$matches[1]&paged=$matches[2]', 'top' );
add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_category=$matches[1]', 'top' );
}
}
}
// Single tag page
if ( ! empty( $pages_settings['tag'] ) ) {
$id = (int) $pages_settings['tag'];
$post = get_post( $id );
if ( $post && 'publish' === $post->post_status ) {
$permalink = get_permalink( $id );
if ( $permalink ) {
$slug = str_replace( $site_url, '', $permalink );
$slug = trim( $slug, '/' );
$slug = urldecode( $slug );
add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_tag=$matches[1]&paged=$matches[2]', 'top' );
add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_tag=$matches[1]', 'top' );
}
}
}
// Single participant page
if ( ! empty( $pages_settings['participant'] ) ) {
$id = (int) $pages_settings['participant'];
$post = get_post( $id );
if ( $post && 'publish' === $post->post_status ) {
$permalink = get_permalink( $id );
if ( $permalink ) {
$slug = str_replace( $site_url, '', $permalink );
$slug = trim( $slug, '/' );
$slug = urldecode( $slug );
add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_participant=$matches[1]&paged=$matches[2]', 'top' );
add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_participant=$matches[1]', 'top' );
}
}
}
// Podcast archive page
if ( ! empty( $pages_settings['podcast_archive_page'] ) ) {
$id = (int) $pages_settings['podcast_archive_page'];
$post = get_post( $id );
if ( $post && 'publish' === $post->post_status ) {
$permalink = get_permalink( $id );
if ( $permalink ) {
$slug = str_replace( $site_url, '', $permalink );
$slug = trim( $slug, '/' );
$slug = urldecode( $slug );
add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?post_type=aiopod_podcasts&name=$matches[1]&paged=$matches[2]', 'top' );
add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?post_type=aiopod_podcasts&name=$matches[1]', 'top' );
add_rewrite_rule( "^$slug/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&paged=$matches[1]', 'top' );
}
}
}
// Episode archive page
if ( ! empty( $pages_settings['episode_archive_page'] ) ) {
$id = (int) $pages_settings['episode_archive_page'];
$post = get_post( $id );
if ( $post && 'publish' === $post->post_status ) {
$permalink = get_permalink( $id );
if ( $permalink ) {
$slug = str_replace( $site_url, '', $permalink );
$slug = trim( $slug, '/' );
$slug = urldecode( $slug );
add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?post_type=aiopod_episodes&name=$matches[1]&paged=$matches[2]', 'top' );
add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?post_type=aiopod_episodes&name=$matches[1]', 'top' );
add_rewrite_rule( "^$slug/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&paged=$matches[1]', 'top' );
}
}
}
}
/**
* Register custom query vars.
*
* @since 1.0.0
*/
public function query_vars( $vars ) {
$vars[] = 'view';
$vars[] = 'aiopod_podcast';
$vars[] = 'aiopod_episode';
$vars[] = 'aiopod_category';
$vars[] = 'aiopod_tag';
$vars[] = 'aiopod_participant';
$vars[] = 'aiopod_feed';
$vars[] = 'aiopod_download';
$vars[] = 'aiopod_transcript';
$vars[] = 'aiopod_format';
return $vars;
}
/**
* Resolve pages that sit underneath a custom archive page.
*
* The archive rules map `{archive-page}/{segment}` to a single podcast or episode. A child page
* of that same archive page has the identical shape, and because these rules are registered at
* the top of the list they are consulted before WordPress reaches its own page rules — so the
* page is looked up as a podcast, is not found, and returns 404.
*
* This reaches any page nested under an archive page: the category, tag and participant pages
* when they are children of it, and the episode archive page itself when it is a child of the
* podcast one.
*
* An existing podcast or episode always keeps the address, so the requests these rules were
* written to serve are untouched. Only a slug matching no post of that type, but matching a
* published page in the archive page's own branch, is handed back to WordPress as a page.
*
* @since 1.0.4
* @param array $query_vars Query variables built from the matched rewrite rule.
* @return array $query_vars Query variables for this request to use.
*/
public function resolve_archive_child_pages( $query_vars ) {
if ( empty( $query_vars['name'] ) || empty( $query_vars['post_type'] ) ) {
return $query_vars;
}
$archive_pages = array(
'aiopod_podcasts' => 'podcast_archive_page',
'aiopod_episodes' => 'episode_archive_page'
);
if ( ! isset( $archive_pages[ $query_vars['post_type'] ] ) ) {
return $query_vars;
}
$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
$setting = $archive_pages[ $query_vars['post_type'] ];
if ( empty( $pages_settings[ $setting ] ) ) {
return $query_vars;
}
$archive_uri = get_page_uri( (int) $pages_settings[ $setting ] );
if ( empty( $archive_uri ) ) {
return $query_vars;
}
// Tested first because it fails for every ordinary podcast and episode, which is the
// overwhelming majority of what reaches here, and settles them in a single lookup
$page = get_page_by_path( trailingslashit( $archive_uri ) . $query_vars['name'] );
if ( empty( $page ) || 'publish' !== $page->post_status ) {
return $query_vars;
}
// Whatever its status, a post of this type owns the address, so a page only takes it over
// where no such post exists
if ( get_page_by_path( $query_vars['name'], OBJECT, $query_vars['post_type'] ) ) {
return $query_vars;
}
$resolved = array( 'page_id' => $page->ID );
// Carried over so a paginated child page still lands on the right page of its own loop
if ( ! empty( $query_vars['paged'] ) ) {
$resolved['paged'] = $query_vars['paged'];
}
// `WP_Query::parse_query()` sets `is_preview` from this var alone, and `the_preview` — the
// filter that swaps the revision in — runs only when it is set. Dropping it would show the
// published page instead of the unsaved changes when a nested page is previewed
if ( ! empty( $query_vars['preview'] ) ) {
$resolved['preview'] = $query_vars['preview'];
}
return $resolved;
}
/**
* Add podcasts and episodes to the core category and tag archives.
*
* `/category/foo/` and `/tag/bar/` set no `post_type` of their own, so `WP_Query` falls back
* to `post` alone. Without this, a core term link on a podcast leads to an archive that does
* not contain the podcast the visitor came from — it shows the wrong posts, or nothing.
*
* Those archives stay the theme's: our posts are rendered by `archive.php` alongside blog
* posts, with no player. That is the intended result of the setting, and the reason the whole
* feature is off by default.
*
* Registered at priority 99 and merges rather than replaces, so a plugin setting `post_type`
* at the usual priority 10 cannot drop us. `aiopod_core_archive_post_types` opts back out.
*
* @since 1.0.4
* @param WP_Query $query The query object, before it runs.
*/
public function include_in_core_archives( $query ) {
// Never the admin list tables, and never a theme's or a plugin's secondary loop
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// Deliberately not guarded against `is_feed()`. `/category/foo/feed/` is the core feed of
// a core archive: if the archive lists our episodes, its feed lists them too. This does
// not touch the plugin's own podcast feeds
if ( ! $query->is_category() && ! $query->is_tag() ) {
return;
}
$taxonomies_settings = aiopod_get_option( 'aiopod_taxonomies_settings' );
if ( empty( $taxonomies_settings['wp_taxonomies'] ) || empty( $taxonomies_settings['include_in_core_archives'] ) ) {
return;
}
// The archive being viewed has to belong to a taxonomy that is actually enabled. A site
// running tags only must find its category archives exactly as they were
$setting = $query->is_category() ? 'categories' : 'tags';
if ( ! isset( $taxonomies_settings['wp_taxonomies'][ $setting ] ) ) {
return;
}
$existing = $query->get( 'post_type' );
// `any` is a keyword, not a list, and already covers both post types
if ( 'any' === $existing ) {
return;
}
// Additive, never a replacement - another plugin's post type stays. An empty value is
// the `post` that WP_Query would have fallen back to on its own
$post_types = empty( $existing ) ? array( 'post' ) : (array) $existing;
$post_types = array_unique( array_merge( $post_types, array( 'aiopod_podcasts', 'aiopod_episodes' ) ) );
$query->set( 'post_type', apply_filters( 'aiopod_core_archive_post_types', $post_types, $query ) );
}
/**
* Register the stylesheets for the public-facing side of the site.
*
* @since 1.0.0
*/
public function register_styles() {
$performance_settings = aiopod_get_option( 'aiopod_performance_settings' );
$appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' );
wp_register_style(
AIOPOD_PLUGIN_SLUG . '-public',
AIOPOD_PLUGIN_URL . 'public/assets/css/public.min.css',
array(),
AIOPOD_PLUGIN_VERSION,
'all'
);
$primary_color = ! empty( $appearance_settings['primary_color'] ) ? esc_attr( $appearance_settings['primary_color'] ) : '#00b2ff';
$inline_css = '.aiopod { --aiopod-color-main: ' . $primary_color . '; --plyr-color-main: ' . $primary_color . '; --swiper-theme-color: ' . $primary_color . '; }';
wp_add_inline_style( AIOPOD_PLUGIN_SLUG . '-public', $inline_css );
// Force load the plugin's CSS file
if ( isset( $performance_settings['force_load_assets']['css'] ) ) {
wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' );
}
}
/**
* Register the JavaScript for the public-facing side of the site.
*
* @since 1.0.0
*/
public function register_scripts() {
$performance_settings = aiopod_get_option( 'aiopod_performance_settings' );
$episodes_settings = aiopod_get_option( 'aiopod_episodes_settings' );
$scroll_offset = ! empty( $episodes_settings['scroll_offset'] ) ? (int) $episodes_settings['scroll_offset'] : 0;
if ( $scroll_offset && is_admin_bar_showing() ) $scroll_offset += 32;
$scroll_offset = apply_filters( 'aiopod_scroll_offset', $scroll_offset );
// Register the dependencies
wp_register_script(
AIOPOD_PLUGIN_SLUG . '-public',
AIOPOD_PLUGIN_URL . 'public/assets/js/public.min.js',
array( 'jquery' ),
AIOPOD_PLUGIN_VERSION,
array( 'strategy' => 'defer' )
);
wp_localize_script(
AIOPOD_PLUGIN_SLUG . '-public',
'aiopod',
array(
'plugin_url' => AIOPOD_PLUGIN_URL,
'ajax_url' => admin_url( 'admin-ajax.php' ),
'ajax_nonce' => wp_create_nonce( 'aiopod_ajax_nonce' ),
'scroll_offset' => $scroll_offset,
'can_play_video' => apply_filters( 'aiopod_player_can_play_video', false ),
)
);
// Force load the plugin's JavaScript file
if ( isset( $performance_settings['force_load_assets']['js'] ) ) {
wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' );
}
}
/**
* Set MySQL's RAND function seed value in a cookie.
*
* @since 1.0.0
*/
public function set_mysql_rand_seed_value() {
$privacy_settings = aiopod_get_option( 'aiopod_privacy_settings' );
if ( isset( $privacy_settings['disable_cookies'] ) && isset( $privacy_settings['disable_cookies']['aiopod_rand_seed'] ) ) {
unset( $_COOKIE['aiopod_rand_seed'] );
return false;
}
if ( headers_sent() ) {
return false;
}
$paged = aiopod_get_page_number();
if ( ! isset( $_COOKIE['aiopod_rand_seed'] ) || 1 === $paged ) {
$seed = wp_rand();
setcookie( 'aiopod_rand_seed', $seed, time() + ( 86400 * 1 ), COOKIEPATH, COOKIE_DOMAIN );
// Update $_COOKIE for immediate use in this request
$_COOKIE['aiopod_rand_seed'] = $seed;
}
}
/**
* Enqueue block assets inside the block editor (iframe).
*
* Hooked to enqueue_block_assets with an is_admin() guard so styles and scripts
* are injected inside the iframed block editor (WP 6.3+ / WP 7.0 always) only,
* and not duplicated on the front end where wp_enqueue_scripts already handles them.
*
* @since 1.0.0
*/
public function enqueue_block_assets() {
if ( ! is_admin() ) {
return;
}
$this->enqueue_editor_assets();
}
/**
* Enqueue the plugin's public styles and scripts in any editor context.
*
* Called by enqueue_block_assets() (WordPress block editor, guarded by is_admin())
* and hooked directly to Elementor actions so assets are also available in the
* Elementor editor panel and its frontend live-preview iframe:
* - elementor/editor/after_enqueue_scripts (admin context)
* - elementor/preview/enqueue_scripts (frontend context, is_admin() = false)
*
* @since 1.0.0
*/
public function enqueue_editor_assets() {
// Styles
$this->register_styles();
wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' );
// Scripts
$this->register_scripts();
wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' );
}
/**
* Flush rewrite rules when it's necessary.
*
* @since 1.0.0
*/
public function maybe_flush_rules() {
// One-time delayed flush after activation/update/settings change.
// Runs on wp_loaded (after init), so all CPTs, taxonomies and rewrite
// rules are registered. Use a hard flush here so the server-level
// permalink config (e.g. .htaccess) is rebuilt too — a soft flush only
// updates the rewrite_rules option and can leave feed/CPT URLs 404ing
// on a fresh install until permalinks are saved manually.
if ( ! get_option( 'aiopod_rewrite_rules_flushed' ) ) {
flush_rewrite_rules( true );
update_option( 'aiopod_rewrite_rules_flushed', 1 );
return;
}
$performance_settings = aiopod_get_option( 'aiopod_performance_settings' );
if ( empty( $performance_settings['maybe_flush_rewrite_rules'] ) ) {
return;
}
// Allow developers to restrict the check to admin only
$admin_only = apply_filters( 'aiopod_flush_rewrite_rules_admin_only', false );
if ( $admin_only && ! is_admin() ) {
return;
}
$stored_rules = get_option( 'rewrite_rules' );
if ( empty( $stored_rules ) || ! is_array( $stored_rules ) ) {
return;
}
global $wp_rewrite;
if ( empty( $wp_rewrite ) ) {
return;
}
$generated_rules = $wp_rewrite->rewrite_rules();
if ( empty( $generated_rules ) || ! is_array( $generated_rules ) ) {
return;
}
foreach ( $generated_rules as $rule => $rewrite ) {
if ( ! array_key_exists( $rule, $stored_rules ) ) {
flush_rewrite_rules( false );
return;
}
}
}
/**
* Override the default page/post title.
*
* @since 1.0.0
* @param string $title The document title.
* @param string $sep Title separator.
* @param string $seplocation Location of the separator (left or right).
* @return string The filtered title.
*/
public function wp_title( $title, $sep, $seplocation ) {
global $post;
if ( ! isset( $post ) ) return $title;
$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
$site_name = sanitize_text_field( get_bloginfo( 'name' ) );
$custom_title = '';
// Get category page title
if ( $post->ID === (int) $pages_settings['category'] ) {
if ( $slug = get_query_var( 'aiopod_category' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) {
$custom_title = $term->name;
}
}
}
// Get tag page title
if ( $post->ID === (int) $pages_settings['tag'] ) {
if ( $slug = get_query_var( 'aiopod_tag' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) {
$custom_title = $term->name;
}
}
}
// Get participant page title
if ( $post->ID === (int) $pages_settings['participant'] ) {
if ( $slug = get_query_var( 'aiopod_participant' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) {
$custom_title = $term->name;
}
}
}
// Set custom title
if ( ! empty( $custom_title ) ) {
$title = ( 'left' === $seplocation ) ? "$site_name $sep $custom_title" : "$custom_title $sep $site_name";
}
return $title;
}
/**
* Override the default post/page title depending on the AIOPOD view.
*
* @since 1.0.0
* @param array $title The document title parts.
* @return Filtered title parts.
*/
public function document_title_parts( $title ) {
global $post;
if ( ! isset( $post ) ) return $title;
$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
// Get category page title
if ( $post->ID === (int) $pages_settings['category'] ) {
if ( $slug = get_query_var( 'aiopod_category' ) ) {
$term = get_term_by( 'slug', $slug, 'aiopod_categories' );
$title['title'] = $term->name;
}
}
// Get tag page title
if ( $post->ID === (int) $pages_settings['tag'] ) {
if ( $slug = get_query_var( 'aiopod_tag' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) {
$title['title'] = $term->name;
}
}
}
// Get participant page title
if ( $post->ID === (int) $pages_settings['participant'] ) {
if ( $slug = get_query_var( 'aiopod_participant' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) {
$title['title'] = $term->name;
}
}
}
// Return
return $title;
}
/**
* Adds Facebook Open Graph tags and Twitter Cards to the page head.
*
* @since 1.0.0
*/
public function wp_head() {
$seo_settings = aiopod_get_option( 'aiopod_seo_settings' );
// Facebook Open Graph tags and Twitter Cards.
global $post;
$content_type = 'page';
$term_slug = '';
if ( is_singular( 'aiopod_podcasts' ) ) {
$content_type = 'podcast';
} elseif ( is_singular( 'aiopod_episodes' ) ) {
$content_type = 'episode';
} elseif ( $term_slug = get_query_var( 'aiopod_category' ) ) {
$content_type = 'category';
} elseif ( $term_slug = get_query_var( 'aiopod_tag' ) ) {
$content_type = 'tag';
} elseif ( $term_slug = get_query_var( 'aiopod_participant' ) ) {
$content_type = 'participant';
}
// Check if OG tags are enabled for this content type.
if ( empty( $seo_settings['open_graph_tags'] ) || ! isset( $seo_settings['open_graph_tags'][ $content_type ] ) ) {
return;
}
$site_name = get_bloginfo( 'name' );
$twitter_username = ! empty( $seo_settings['twitter_username'] ) ? trim( $seo_settings['twitter_username'] ) : '';
$page_url = '';
$page_title = '';
$page_description = '';
$image_url = '';
$media_url = '';
$media_type = '';
$is_video = 0;
/**
* Category / Tag / Participant pages (custom query vars)
*/
if ( in_array( $content_type, array( 'category', 'tag', 'participant' ) ) ) {
$taxonomy = 'aiopod_categories';
if ( 'tag' === $content_type ) {
$taxonomy = 'aiopod_tags';
} elseif ( 'participant' === $content_type ) {
$taxonomy = 'aiopod_participants';
}
$term = get_term_by( 'slug', $term_slug, $taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
return;
}
$page_url = get_term_link( $term );
$page_title = trim( $term->name );
$page_description = aiopod_truncate_text( $term->description, 160, '' );
$image_data = aiopod_get_image( array(
'term_id' => $term->term_id,
'size' => 'large',
'alt' => ''
) );
$image_url = ! empty( $image_data['src'] ) ? $image_data['src'] : '';
/**
* Singular post pages
*/
} else {
if ( empty( $post ) || ! isset( $post->ID ) ) {
return;
}
$page_url = get_permalink( $post );
$page_title = trim( get_the_title( $post ) );
$page_description = aiopod_get_excerpt( $post->ID, 160, '', false );
$image_data = aiopod_get_image( array(
'post_id' => $post->ID,
'size' => 'large',
'alt' => ''
) );
$image_url = ! empty( $image_data['src'] ) ? $image_data['src'] : '';
$media_url = get_post_meta( $post->ID, 'episode_file', true );
if ( ! empty( $media_url ) ) {
$media_type = aiopod_get_episode_file_type( $post->ID, $media_url );
$is_video = false !== strpos( $media_type, 'audio/' );
}
}
/**
* Open Graph type mapping
*/
$og_type = 'website';
switch ( $content_type ) {
case 'podcast':
$og_type = 'music.album';
break;
case 'episode':
$og_type = $is_video ? 'video.episode' : 'music.song';
break;
case 'participant':
$og_type = 'profile';
break;
}
// Fallback title
if ( ! $page_title ) {
$page_title = $site_name;
}
/**
* Output tags
*/
echo PHP_EOL . '' . PHP_EOL;
echo sprintf( '' . PHP_EOL, esc_attr( $site_name ) );
echo sprintf( '' . PHP_EOL, esc_url( $page_url ) );
echo sprintf( '' . PHP_EOL, esc_attr( $og_type ) );
echo sprintf( '' . PHP_EOL, esc_attr( $page_title ) );
if ( $page_description ) {
echo sprintf( '' . PHP_EOL, esc_attr( $page_description ) );
}
if ( $image_url ) {
echo sprintf( '' . PHP_EOL, esc_url( $image_url ) );
}
/**
* Media tags (Open Graph)
*/
if ( $media_url ) {
if ( $is_video ) {
echo sprintf( '' . PHP_EOL, esc_url( $media_url ) );
if ( $media_type ) {
echo sprintf( '' . PHP_EOL, esc_attr( $media_type ) );
}
if ( 0 === strpos( $media_url, 'https://' ) ) {
echo sprintf( '' . PHP_EOL, esc_url( $media_url ) );
}
echo '' . PHP_EOL;
echo '' . PHP_EOL;
} else {
echo sprintf( '' . PHP_EOL, esc_url( $media_url ) );
if ( $media_type ) {
echo sprintf( '' . PHP_EOL, esc_attr( $media_type ) );
}
if ( 0 === strpos( $media_url, 'https://' ) ) {
echo sprintf( '' . PHP_EOL, esc_url( $media_url ) );
}
}
}
/**
* Twitter Cards (summary only – no player yet)
*/
echo '' . PHP_EOL;
if ( $twitter_username ) {
if ( '@' !== $twitter_username[0] ) $twitter_username = '@' . $twitter_username;
echo sprintf( '' . PHP_EOL, esc_attr( $twitter_username ) );
}
echo sprintf( '' . PHP_EOL, esc_attr( $page_title ) );
if ( $page_description ) {
echo sprintf( '' . PHP_EOL, esc_attr( $page_description ) );
}
if ( $image_url ) {
echo sprintf( '' . PHP_EOL, esc_url( $image_url ) );
}
}
/**
* Change the current page title if applicable.
*
* @since 1.0.0
* @param string $title Current page title.
* @param int $post_id The post ID.
* @return string $title Filtered page title.
*/
public function the_title( $title, $id = 0 ) {
if ( ! in_the_loop() || ! is_main_query() ) {
return $title;
}
$post_id = get_the_ID();
if ( ! empty( $id ) ) {
if ( $post_id !== $id ) {
return $title;
}
}
$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
// Change category page title
if ( $post_id === (int) $pages_settings['category'] ) {
if ( $slug = get_query_var( 'aiopod_category' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) {
$title = $term->name;
}
}
}
// Change tag page title
if ( $post_id === (int) $pages_settings['tag'] ) {
if ( $slug = get_query_var( 'aiopod_tag' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) {
$title = $term->name;
}
}
}
// Change participant page title
if ( $post_id === (int) $pages_settings['participant'] ) {
if ( $slug = get_query_var( 'aiopod_participant' ) ) {
if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) {
$title = $term->name;
}
}
}
return $title;
}
/**
* Filters whether a podcast-related post has a thumbnail.
*
* This ensures that AiO Podcast Studio posts can report a thumbnail
* even when the image is provided via plugin-specific image settings,
* while still respecting user options to hide featured images.
*
* @since 1.0.0
* @param bool $has_thumbnail Whether the post has a thumbnail.
* @param int|WP_Post|null $post Post ID or WP_Post object.
* @param int|string $thumbnail_id Post thumbnail ID or empty string.
* @return bool Whether the post should be treated as having a thumbnail.
*/
public function has_post_thumbnail( $has_thumbnail, $post, $thumbnail_id ) {
$post = get_post( $post );
// Bail early if the post object is invalid.
if ( ! $post ) {
return $has_thumbnail;
}
// Apply only on single podcast-related post types.
if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) {
return $has_thumbnail;
}
// Ensure this runs only for the main queried object.
global $wp_the_query;
if ( (int) $post->ID !== (int) $wp_the_query->get_queried_object_id() ) {
return $has_thumbnail;
}
// Respect plugin image visibility settings.
$appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' );
if ( ! empty( $appearance_settings['hide_on_single_post_pages'] ) ) {
return false;
}
// If a native featured image exists, honor WordPress behavior.
if ( ! empty( $thumbnail_id ) ) {
return $has_thumbnail;
}
// Fallback: Check for plugin-managed images.
$image_data = aiopod_get_image( array(
'post_id' => $post->ID,
'size' => 'large',
'alt' => ''
) );
if ( ! empty( $image_data['src'] ) ) {
return true;
}
return $has_thumbnail;
}
/**
* Filters the podcast post thumbnail HTML.
*
* Allows AiO Podcast Studio posts to render thumbnails provided
* via plugin-specific image settings when a native featured image
* is not available, while still respecting user visibility options.
*
* @since 1.0.0
* @param string $html The post thumbnail HTML.
* @param int $post_id The post ID.
* @param int|string $post_thumbnail_id The post thumbnail ID or empty string.
* @param string|array $size The post thumbnail size.
* @param array $attr Image attributes.
* @return string The filtered post thumbnail HTML.
*/
public function post_thumbnail_html( $html, $post_id, $post_thumbnail_id, $size, $attr ) {
// Apply only on single podcast-related post types.
if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) {
return $html;
}
// Ensure this runs only for the main queried object.
global $wp_the_query;
if ( (int) $post_id !== (int) $wp_the_query->get_queried_object_id() ) {
return $html;
}
// Respect plugin image visibility settings.
$appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' );
if ( ! empty( $appearance_settings['hide_on_single_post_pages'] ) ) {
return '';
}
// If a native featured image exists, honor WordPress behavior.
if ( ! empty( $post_thumbnail_id ) ) {
return $html;
}
$thumbnail_html = '';
/**
* 1. Plugin-managed attachment image (preferred)
*/
$image_id = get_post_meta( $post_id, 'image_id', true );
if ( $image_id ) {
$thumbnail_html = wp_get_attachment_image( $image_id, $size, false, $attr );
}
/**
* 2. External image URL fallback
*/
if ( ! $thumbnail_html ) {
$image_url = get_post_meta( $post_id, 'image', true );
if ( $image_url ) {
$attributes = wp_parse_args( (array) $attr, array( 'alt' => get_the_title( $post_id ) ) );
/**
* Match WordPress image attribute filtering behavior.
*/
$attributes = apply_filters( 'wp_get_attachment_image_attributes', $attributes, null, $size );
$attributes = array_map( 'esc_attr', $attributes );
$thumbnail_html = sprintf( '
$value ) {
$thumbnail_html .= sprintf( ' %s="%s"', $name, $value );
}
$thumbnail_html .= ' />';
}
}
return $thumbnail_html ? $thumbnail_html : $html;
}
/**
* Always use our custom page for AIOPOD categories & tags.
*
* @since 1.0.0
* @param string $url The term URL.
* @param object $term The term object.
* @param string $taxonomy The taxonomy slug.
* @return string $url Filtered term URL.
*/
public function term_link( $url, $term, $taxonomy ) {
if ( 'aiopod_categories' === $taxonomy ) {
$url = aiopod_get_category_page_url( $term );
}
if ( 'aiopod_tags' === $taxonomy ) {
$url = aiopod_get_tag_page_url( $term );
}
if ( 'aiopod_participants' === $taxonomy ) {
$url = aiopod_get_participant_page_url( $term );
}
return $url;
}
/**
* Fix pagination on single podcast / episode / participant pages.
*
* @access private
* @since 1.0.0
*/
private function fix_single_pagination() {
if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) {
return;
}
global $wp_query;
if ( ! $wp_query->is_main_query() ) {
return;
}
$page = (int) $wp_query->get( 'page' );
if ( $page > 1 ) {
// Convert `page` to `paged`
$wp_query->set( 'page', 1 );
$wp_query->set( 'paged', $page );
}
// Prevent canonical redirect breaking pagination
remove_action( 'template_redirect', 'redirect_canonical' );
}
/**
* Redirect default CPT archives to custom archive pages.
*
* @access private
* @since 1.0.0
*/
private function redirect_custom_archives() {
if ( ! is_post_type_archive() ) {
return;
}
$pages_settings = aiopod_get_option( 'aiopod_pages_settings' );
// Explicit CPT → option key mapping (future-proof)
$archive_map = array(
'aiopod_podcasts' => 'podcast_archive_page',
'aiopod_episodes' => 'episode_archive_page'
);
$post_type = get_query_var( 'post_type' );
if ( empty( $archive_map[ $post_type ] ) ) {
return;
}
$option_key = $archive_map[ $post_type ];
if ( empty( $pages_settings[ $option_key ] ) ) {
return;
}
$page_id = (int) $pages_settings[ $option_key ];
$page_url = get_permalink( $page_id );
if ( ! $page_url ) {
return;
}
$current_url = home_url( add_query_arg( null, null ) );
// Avoid redirect loop
if ( untrailingslashit( $current_url ) === untrailingslashit( $page_url ) ) {
return;
}
wp_safe_redirect( $page_url, 301 );
exit;
}
}