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' => __( '« Previous', 'wp-fedistream' ), 'next_text' => __( 'Next »', '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(); } }