Files
wp-fedistream/includes/Frontend/TemplateLoader.php

666 lines
24 KiB
PHP
Raw Normal View History

<?php
/**
* Template loader for frontend display.
*
* @package WP_FediStream
*/
namespace WP_FediStream\Frontend;
use WP_FediStream\Plugin;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* TemplateLoader class.
*
* Handles loading custom templates for FediStream post types.
*/
class TemplateLoader {
/**
* Recursion depth for get_post_data calls.
*
* @var int
*/
private static int $recursion_depth = 0;
/**
* Maximum allowed recursion depth.
*
* @var int
*/
private const MAX_RECURSION_DEPTH = 3;
/**
* Whether we're currently in a shortcode rendering context.
* When true, the_content filter is skipped to prevent recursive shortcode processing.
*
* @var bool
*/
private static bool $in_shortcode_context = false;
/**
* Enter shortcode rendering context.
* Call this before rendering shortcode content to prevent recursive shortcode processing.
*
* @return void
*/
public static function enter_shortcode_context(): void {
self::$in_shortcode_context = true;
}
/**
* Exit shortcode rendering context.
* Call this after shortcode rendering is complete.
*
* @return void
*/
public static function exit_shortcode_context(): void {
self::$in_shortcode_context = false;
}
/**
* Check if we're in a shortcode rendering context.
*
* @return bool
*/
public static function is_in_shortcode_context(): bool {
return self::$in_shortcode_context;
}
/**
* Constructor.
*/
public function __construct() {
add_filter( 'template_include', array( $this, 'template_include' ) );
add_filter( 'body_class', array( $this, 'body_class' ) );
}
/**
* Include custom templates for FediStream post types.
*
* @param string $template Template path.
* @return string Modified template path.
*/
public function template_include( string $template ): string {
// Check if we're on a FediStream page.
if ( is_singular( 'fedistream_artist' ) ) {
return $this->get_template( 'single-artist' );
}
if ( is_singular( 'fedistream_album' ) ) {
return $this->get_template( 'single-album' );
}
if ( is_singular( 'fedistream_track' ) ) {
return $this->get_template( 'single-track' );
}
if ( is_singular( 'fedistream_playlist' ) ) {
return $this->get_template( 'single-playlist' );
}
if ( is_post_type_archive( 'fedistream_artist' ) ) {
return $this->get_template( 'archive-artist' );
}
if ( is_post_type_archive( 'fedistream_album' ) ) {
return $this->get_template( 'archive-album' );
}
if ( is_post_type_archive( 'fedistream_track' ) ) {
return $this->get_template( 'archive-track' );
}
if ( is_post_type_archive( 'fedistream_playlist' ) ) {
return $this->get_template( 'archive-playlist' );
}
if ( is_tax( 'fedistream_genre' ) ) {
return $this->get_template( 'taxonomy-genre' );
}
if ( is_tax( 'fedistream_mood' ) ) {
return $this->get_template( 'taxonomy-mood' );
}
return $template;
}
/**
* Get template file path.
*
* First checks theme for override, then uses plugin template.
*
* @param string $template_name Template name without extension.
* @return string Template path.
*/
private function get_template( string $template_name ): string {
// Check theme for override.
$theme_template = locate_template(
array(
"fedistream/{$template_name}.php",
"fedistream/{$template_name}.twig",
)
);
if ( $theme_template ) {
return $theme_template;
}
// Use plugin template wrapper.
return WP_FEDISTREAM_PATH . 'includes/Frontend/template-wrapper.php';
}
/**
* Add FediStream classes to body.
*
* @param array $classes Body classes.
* @return array Modified classes.
*/
public function body_class( array $classes ): array {
if ( $this->is_fedistream_page() ) {
$classes[] = 'fedistream';
if ( is_singular( 'fedistream_artist' ) ) {
$classes[] = 'fedistream-artist';
$classes[] = 'fedistream-single';
} elseif ( is_singular( 'fedistream_album' ) ) {
$classes[] = 'fedistream-album';
$classes[] = 'fedistream-single';
} elseif ( is_singular( 'fedistream_track' ) ) {
$classes[] = 'fedistream-track';
$classes[] = 'fedistream-single';
} elseif ( is_singular( 'fedistream_playlist' ) ) {
$classes[] = 'fedistream-playlist';
$classes[] = 'fedistream-single';
} elseif ( is_post_type_archive( 'fedistream_artist' ) ) {
$classes[] = 'fedistream-archive';
$classes[] = 'fedistream-artists';
} elseif ( is_post_type_archive( 'fedistream_album' ) ) {
$classes[] = 'fedistream-archive';
$classes[] = 'fedistream-albums';
} elseif ( is_post_type_archive( 'fedistream_track' ) ) {
$classes[] = 'fedistream-archive';
$classes[] = 'fedistream-tracks';
} elseif ( is_post_type_archive( 'fedistream_playlist' ) ) {
$classes[] = 'fedistream-archive';
$classes[] = 'fedistream-playlists';
} elseif ( is_tax( 'fedistream_genre' ) || is_tax( 'fedistream_mood' ) ) {
$classes[] = 'fedistream-archive';
$classes[] = 'fedistream-taxonomy';
}
}
return $classes;
}
/**
* Check if current page is a FediStream page.
*
* @return bool
*/
public function is_fedistream_page(): bool {
return is_singular( array( 'fedistream_artist', 'fedistream_album', 'fedistream_track', 'fedistream_playlist' ) )
|| is_post_type_archive( array( 'fedistream_artist', 'fedistream_album', 'fedistream_track', 'fedistream_playlist' ) )
|| is_tax( array( 'fedistream_genre', 'fedistream_mood', 'fedistream_license' ) );
}
/**
* Get template context for current page.
*
* @return array Template context.
*/
public static function get_context(): array {
$context = array(
'site_name' => get_bloginfo( 'name' ),
'site_url' => home_url(),
'is_singular' => is_singular(),
'is_archive' => is_archive(),
'current_url' => get_permalink(),
);
if ( is_singular() ) {
global $post;
$context['post'] = self::get_post_data( $post );
}
if ( is_post_type_archive() || is_tax() ) {
$context['posts'] = self::get_archive_posts();
$context['pagination'] = self::get_pagination();
$context['archive_title'] = self::get_archive_title();
$context['archive_description'] = self::get_archive_description();
}
return $context;
}
/**
* Get post data for template.
*
* @param \WP_Post $post Post object.
* @param bool $skip_nested Whether to skip loading nested items (albums, tracks, etc.).
* @return array Post data.
*/
public static function get_post_data( \WP_Post $post, bool $skip_nested = false ): array {
// Track recursion to prevent infinite loops from shortcodes in content.
++self::$recursion_depth;
// Skip the_content filter if:
// 1. We're in a shortcode context (prevents recursive shortcode processing)
// 2. We're at depth > 1 (nested data loading)
$skip_content_filter = self::$in_shortcode_context || self::$recursion_depth > 1;
$data = array(
'id' => $post->ID,
'title' => get_the_title( $post ),
'content' => $skip_content_filter ? wp_kses_post( $post->post_content ) : apply_filters( 'the_content', $post->post_content ),
'excerpt' => get_the_excerpt( $post ),
'permalink' => get_permalink( $post ),
'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'large' ),
'date' => get_the_date( '', $post ),
'author' => get_the_author_meta( 'display_name', $post->post_author ),
);
// Add post type specific data (skip nested items if at max depth).
$load_nested = ! $skip_nested && self::$recursion_depth < self::MAX_RECURSION_DEPTH;
switch ( $post->post_type ) {
case 'fedistream_artist':
$data = array_merge( $data, self::get_artist_data( $post->ID, $load_nested ) );
break;
case 'fedistream_album':
$data = array_merge( $data, self::get_album_data( $post->ID, $load_nested ) );
break;
case 'fedistream_track':
$data = array_merge( $data, self::get_track_data( $post->ID ) );
break;
case 'fedistream_playlist':
$data = array_merge( $data, self::get_playlist_data( $post->ID, $load_nested ) );
break;
}
// Add taxonomies.
$data['genres'] = self::get_terms( $post->ID, 'fedistream_genre' );
$data['moods'] = self::get_terms( $post->ID, 'fedistream_mood' );
--self::$recursion_depth;
return $data;
}
/**
* Get artist-specific data.
*
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @param bool $load_nested Whether to load nested albums.
* @return array Artist data.
*/
public static function get_artist_data( int|\WP_Post $post_id, bool $load_nested = true ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
$type = get_post_meta( $post_id, '_fedistream_artist_type', true ) ?: 'solo';
$types = array(
'solo' => __( 'Solo Artist', 'wp-fedistream' ),
'band' => __( 'Band', 'wp-fedistream' ),
'duo' => __( 'Duo', 'wp-fedistream' ),
'collective' => __( 'Collective', 'wp-fedistream' ),
);
$albums = array();
$album_count = 0;
if ( $load_nested ) {
$album_posts = get_posts(
array(
'post_type' => 'fedistream_album',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_album_artist',
'meta_value' => $post_id,
'orderby' => 'meta_value',
'meta_query' => array(
array(
'key' => '_fedistream_album_release_date',
'compare' => 'EXISTS',
),
),
'order' => 'DESC',
)
);
$album_count = count( $album_posts );
$albums = array_map(
function ( $album ) {
return self::get_post_data( $album, true ); // Skip further nesting.
},
$album_posts
);
} else {
// Just get the count without loading full data.
$album_count = (int) get_posts(
array(
'post_type' => 'fedistream_album',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_album_artist',
'meta_value' => $post_id,
'fields' => 'ids',
)
);
$album_count = is_array( $album_count ) ? count( $album_count ) : 0;
}
return array(
'artist_type' => $type,
'artist_type_label' => $types[ $type ] ?? $types['solo'],
'formed_date' => get_post_meta( $post_id, '_fedistream_artist_formed_date', true ),
'location' => get_post_meta( $post_id, '_fedistream_artist_location', true ),
'website' => get_post_meta( $post_id, '_fedistream_artist_website', true ),
'social_links' => get_post_meta( $post_id, '_fedistream_artist_social_links', true ) ?: array(),
'members' => get_post_meta( $post_id, '_fedistream_artist_members', true ) ?: array(),
'albums' => $albums,
'album_count' => $album_count,
);
}
/**
* Get album-specific data.
*
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @param bool $load_nested Whether to load nested tracks.
* @return array Album data.
*/
public static function get_album_data( int|\WP_Post $post_id, bool $load_nested = true ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
$type = get_post_meta( $post_id, '_fedistream_album_type', true ) ?: 'album';
$types = array(
'album' => __( 'Album', 'wp-fedistream' ),
'ep' => __( 'EP', 'wp-fedistream' ),
'single' => __( 'Single', 'wp-fedistream' ),
'compilation' => __( 'Compilation', 'wp-fedistream' ),
'live' => __( 'Live Album', 'wp-fedistream' ),
'remix' => __( 'Remix Album', 'wp-fedistream' ),
);
$artist_id = get_post_meta( $post_id, '_fedistream_album_artist', true );
$tracks = array();
$total_tracks = 0;
if ( $load_nested ) {
$track_posts = get_posts(
array(
'post_type' => 'fedistream_track',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_track_album',
'meta_value' => $post_id,
'orderby' => 'meta_value_num',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_fedistream_track_number',
'compare' => 'EXISTS',
),
),
'order' => 'ASC',
)
);
$total_tracks = count( $track_posts );
$tracks = array_map(
function ( $track ) {
return self::get_post_data( $track, true ); // Skip further nesting.
},
$track_posts
);
} else {
// Just get the count without loading full data.
$track_ids = get_posts(
array(
'post_type' => 'fedistream_track',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_key' => '_fedistream_track_album',
'meta_value' => $post_id,
'fields' => 'ids',
)
);
$total_tracks = is_array( $track_ids ) ? count( $track_ids ) : 0;
}
return array(
'album_type' => $type,
'album_type_label' => $types[ $type ] ?? $types['album'],
'release_date' => get_post_meta( $post_id, '_fedistream_album_release_date', true ),
'release_year' => date( 'Y', strtotime( get_post_meta( $post_id, '_fedistream_album_release_date', true ) ?: 'now' ) ),
'artist_id' => $artist_id,
'artist_name' => $artist_id ? get_the_title( $artist_id ) : '',
'artist_url' => $artist_id ? get_permalink( $artist_id ) : '',
'upc' => get_post_meta( $post_id, '_fedistream_album_upc', true ),
'catalog_number' => get_post_meta( $post_id, '_fedistream_album_catalog_number', true ),
'total_tracks' => $total_tracks,
'total_duration' => (int) get_post_meta( $post_id, '_fedistream_album_total_duration', true ),
'tracks' => $tracks,
);
}
/**
* Get track-specific data.
*
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @return array Track data.
*/
public static function get_track_data( int|\WP_Post $post_id ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
$audio_file = get_post_meta( $post_id, '_fedistream_track_audio_file', true );
$artists = get_post_meta( $post_id, '_fedistream_track_artists', true ) ?: array();
$duration = (int) get_post_meta( $post_id, '_fedistream_track_duration', true );
$artist_data = array();
foreach ( $artists as $artist_id ) {
$artist = get_post( $artist_id );
if ( $artist ) {
$artist_data[] = array(
'id' => $artist_id,
'name' => $artist->post_title,
'url' => get_permalink( $artist_id ),
);
}
}
return array(
'track_number' => (int) get_post_meta( $post_id, '_fedistream_track_number', true ),
'disc_number' => (int) get_post_meta( $post_id, '_fedistream_track_disc_number', true ) ?: 1,
'duration' => $duration,
'duration_formatted' => $duration ? sprintf( '%d:%02d', floor( $duration / 60 ), $duration % 60 ) : '',
'audio_url' => $audio_file ? wp_get_attachment_url( $audio_file ) : '',
'audio_format' => get_post_meta( $post_id, '_fedistream_track_audio_format', true ),
'bpm' => (int) get_post_meta( $post_id, '_fedistream_track_bpm', true ),
'key' => get_post_meta( $post_id, '_fedistream_track_key', true ),
'explicit' => (bool) get_post_meta( $post_id, '_fedistream_track_explicit', true ),
'isrc' => get_post_meta( $post_id, '_fedistream_track_isrc', true ),
'album_id' => $album_id,
'album_title' => $album_id ? get_the_title( $album_id ) : '',
'album_url' => $album_id ? get_permalink( $album_id ) : '',
'album_artwork' => $album_id ? get_the_post_thumbnail_url( $album_id, 'medium' ) : '',
'artists' => $artist_data,
);
}
/**
* Get playlist-specific data.
*
* @param int|\WP_Post $post_id Post ID or WP_Post object.
* @param bool $load_nested Whether to load nested tracks.
* @return array Playlist data.
*/
public static function get_playlist_data( int|\WP_Post $post_id, bool $load_nested = true ): array {
// Support both post ID and WP_Post object.
if ( $post_id instanceof \WP_Post ) {
$post_id = $post_id->ID;
}
global $wpdb;
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
$duration = (int) get_post_meta( $post_id, '_fedistream_playlist_total_duration', true );
// Get track IDs.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$track_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT track_id FROM $table WHERE playlist_id = %d ORDER BY position ASC",
$post_id
)
);
$tracks = array();
$track_count = count( $track_ids );
if ( $load_nested && ! empty( $track_ids ) ) {
foreach ( $track_ids as $track_id ) {
$track = get_post( $track_id );
if ( $track && 'publish' === $track->post_status ) {
$tracks[] = self::get_post_data( $track, true ); // Skip further nesting.
}
}
}
return array(
'visibility' => get_post_meta( $post_id, '_fedistream_playlist_visibility', true ) ?: 'public',
'collaborative' => (bool) get_post_meta( $post_id, '_fedistream_playlist_collaborative', true ),
'federated' => (bool) get_post_meta( $post_id, '_fedistream_playlist_federated', true ),
'track_count' => $load_nested ? count( $tracks ) : $track_count,
'total_duration' => $duration,
'duration_formatted' => $duration >= 3600
? sprintf( '%d:%02d:%02d', floor( $duration / 3600 ), floor( ( $duration % 3600 ) / 60 ), $duration % 60 )
: sprintf( '%d:%02d', floor( $duration / 60 ), $duration % 60 ),
'tracks' => $tracks,
);
}
/**
* Get taxonomy terms for post.
*
* @param int $post_id Post ID.
* @param string $taxonomy Taxonomy name.
* @return array Terms with name and URL.
*/
private static function get_terms( int $post_id, string $taxonomy ): array {
$terms = get_the_terms( $post_id, $taxonomy );
if ( ! $terms || is_wp_error( $terms ) ) {
return array();
}
return array_map(
function ( $term ) {
return array(
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'url' => get_term_link( $term ),
);
},
$terms
);
}
/**
* Get archive posts.
*
* @return array Posts for archive.
*/
private static function get_archive_posts(): array {
global $wp_query;
$posts = array();
if ( $wp_query->have_posts() ) {
while ( $wp_query->have_posts() ) {
$wp_query->the_post();
$posts[] = self::get_post_data( get_post() );
}
wp_reset_postdata();
}
return $posts;
}
/**
* Get pagination data.
*
* @return array Pagination data.
*/
private static function get_pagination(): array {
global $wp_query;
$total_pages = $wp_query->max_num_pages;
$current = max( 1, get_query_var( 'paged' ) );
return array(
'total_pages' => $total_pages,
'current_page' => $current,
'has_prev' => $current > 1,
'has_next' => $current < $total_pages,
'prev_url' => $current > 1 ? get_pagenum_link( $current - 1 ) : '',
'next_url' => $current < $total_pages ? get_pagenum_link( $current + 1 ) : '',
'links' => paginate_links(
array(
'total' => $total_pages,
'current' => $current,
'type' => 'array',
'prev_text' => __( '&laquo; Previous', 'wp-fedistream' ),
'next_text' => __( 'Next &raquo;', 'wp-fedistream' ),
)
) ?: array(),
);
}
/**
* Get archive title.
*
* @return string Archive title.
*/
private static function get_archive_title(): string {
if ( is_post_type_archive( 'fedistream_artist' ) ) {
return __( 'Artists', 'wp-fedistream' );
}
if ( is_post_type_archive( 'fedistream_album' ) ) {
return __( 'Albums', 'wp-fedistream' );
}
if ( is_post_type_archive( 'fedistream_track' ) ) {
return __( 'Tracks', 'wp-fedistream' );
}
if ( is_post_type_archive( 'fedistream_playlist' ) ) {
return __( 'Playlists', 'wp-fedistream' );
}
if ( is_tax() ) {
return single_term_title( '', false );
}
return get_the_archive_title();
}
/**
* Get archive description.
*
* @return string Archive description.
*/
private static function get_archive_description(): string {
if ( is_tax() ) {
return term_description();
}
return get_the_archive_description();
}
}