Player::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', ) ); $id = $first ? (int) $first[0] : 0; } return $id; } /** * Public URL. * * @return string */ public static function url() { if ( get_option( 'permalink_structure' ) ) { return home_url( user_trailingslashit( self::slug() ) ); } return add_query_arg( self::QUERY, '1', home_url( '/' ) ); } /** * Sections a listen page can show under the player (key → label). Core: the station switcher and * share buttons; add-ons add theirs and render them on `streamsera_listen_page_section`. * * @return array */ public static function sections() { /** * Filter the listen page sections. * * @param array $sections Key → label. */ return (array) apply_filters( 'streamsera_listen_page_sections', array( 'switcher' => __( 'Station switcher', 'streamsera' ), 'share' => __( 'Share buttons', 'streamsera' ), ) ); } /** * Sections shown, in order. Sites from before the setting keep what the page always showed. * * @return string[] */ public static function active_sections() { $saved = streamsera_get_setting( 'listen_sections', null ); $list = is_array( $saved ) ? $saved : array( 'schedule', 'share' ); return array_values( array_intersect( array_map( 'strval', $list ), array_keys( self::sections() ) ) ); } /** * Rewrite rule. */ public function rewrite() { if ( get_option( 'streamsera_flush_rewrite' ) ) { delete_option( 'streamsera_flush_rewrite' ); add_action( 'wp_loaded', 'flush_rewrite_rules' ); } // Only claim the slug while the page is on; otherwise a real page at /listen/ must keep working. if ( ! self::enabled() ) { return; } add_rewrite_rule( '^' . preg_quote( self::slug(), '/' ) . '/?$', 'index.php?' . self::QUERY . '=1', 'top' ); } /** * Query var. * * @param array $vars Vars. * @return array */ public function query_vars( $vars ) { $vars[] = self::QUERY; return $vars; } /** * Persist the new keys; flush rewrites when the slug changes. * * @param array $next Settings about to be saved. * @param array $input Raw input. * @return array */ public function save_settings( $next, $input ) { $current = (array) get_option( 'streamsera_settings', array() ); if ( isset( $input['listen_slug'] ) && is_scalar( $input['listen_slug'] ) ) { $slug = sanitize_title( (string) ( $next['listen_slug'] ?? $input['listen_slug'] ) ); $slug = $slug ? $slug : 'listen'; $previous = sanitize_title( (string) ( $current['listen_slug'] ?? 'listen' ) ); $previous = $previous ? $previous : 'listen'; $conflict = $slug !== $previous ? streamsera_slug_conflict( $slug, 'listen_slug' ) : ''; if ( $conflict ) { // A rewrite on this slug would hijack existing content: keep the old value and say why. streamsera_add_settings_error( 'listen_slug', sprintf( /* translators: 1: slug, 2: what already uses it */ __( '"/%1$s/" is already used by %2$s. The listen page slug was not changed.', 'streamsera' ), $slug, $conflict ) ); $slug = $previous; } $next['listen_slug'] = $slug; } streamsera_guard_page_enable( $next, $current, 'listen_enabled', 'listen_slug', 'listen', /* translators: 1: slug, 2: what already uses it */ __( '"/%1$s/" is already used by %2$s, so the listen page was not turned on. Change the listen page slug first.', 'streamsera' ) ); if ( array_key_exists( 'listen_sections', $input ) && is_array( $input['listen_sections'] ) ) { // Keys only; unknown ones are kept so an add-on's section survives while it is inactive. $keys = array_map( 'sanitize_key', array_filter( $input['listen_sections'], 'is_scalar' ) ); $next['listen_sections'] = array_values( array_unique( array_filter( $keys ) ) ); } if ( ( $current['listen_slug'] ?? 'listen' ) !== ( $next['listen_slug'] ?? 'listen' ) || (bool) ( $next['listen_enabled'] ?? false ) !== (bool) ( $current['listen_enabled'] ?? false ) ) { update_option( 'streamsera_flush_rewrite', 1, false ); } return $next; } /** * Expose the URL to the admin app. * * @param array $data Admin data. * @return array */ public function admin_data( $data ) { $data['listenUrl'] = self::url(); $data['homeUrl'] = home_url( '/' ); $data['permalinks'] = (bool) get_option( 'permalink_structure' ); $sections = array(); foreach ( self::sections() as $key => $label ) { $sections[] = array( 'key' => (string) $key, 'label' => (string) $label, ); } $data['listenSections'] = $sections; $data['listenSectionsActive'] = self::active_sections(); $data['blockTheme'] = function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(); return $data; } /** * Stations for the listen page's switcher: every published station, current one first marked. * * @param int $current Station on the page now. * @return array[] { id, name, logo, url, current } */ public static function switcher_stations( $current ) { $ids = get_posts( array( 'post_type' => Station::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 30, 'orderby' => array( 'menu_order' => 'ASC', 'title' => 'ASC', ), 'fields' => 'ids', ) ); $out = array(); foreach ( $ids as $id ) { $public = Station::to_public( (int) $id ); if ( ! $public ) { continue; } $out[] = array( 'id' => (int) $id, 'name' => (string) $public['name'], 'logo' => (string) ( $public['logo'] ?? '' ), 'url' => add_query_arg( 'station', (int) $id, self::url() ), 'current' => (int) $id === (int) $current, ); } return count( $out ) > 1 ? $out : array(); } /** * Advertise the listen page (lets browsers/apps discover it). */ public function head_link() { if ( self::enabled() && self::player_id() ) { echo '' . "\n"; } } /** * Serve the page. */ public function render() { if ( ! get_query_var( self::QUERY ) ) { return; } if ( ! self::enabled() ) { return; // Falls through to the theme's 404. } $player_id = self::player_id(); $config = $player_id ? Player::get_config( $player_id ) : null; if ( ! $config ) { return; } $config = Player::with_live_station( $config ); // Station switcher: ?station=ID plays another published station with the same player. $pick = isset( $_GET['station'] ) ? absint( wp_unslash( $_GET['station'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public, read-only. if ( $pick && $pick !== (int) $config['station'] && Station::to_public( $pick ) ) { $config['station'] = $pick; } $station = Station::to_public( $config['station'] ); if ( ! $station ) { return; } // template_redirect runs before wp_enqueue_scripts: register exactly the player handles // (core + add-ons) so the template's head/footer have something to print. Assets::register_standalone(); $markup = Assets::render_player( $player_id, array( 'layout' => 'hero', 'station' => (int) $config['station'], ), array( 'listen' => true ) ); if ( ! $markup ) { return; } $vars = array( 'player_id' => $player_id, 'config' => $config, 'station' => $station, 'markup' => $markup, 'url' => self::url(), 'site' => get_bloginfo( 'name' ), 'title' => sprintf( /* translators: %s station name */ __( 'Listen live — %s', 'streamsera' ), $station['name'] ), 'back' => home_url( '/' ), 'sections' => self::active_sections(), 'stations' => in_array( 'switcher', self::active_sections(), true ) ? self::switcher_stations( (int) $station['id'] ) : array(), 'share' => array( 'x' => 'https://twitter.com/intent/tweet?' . http_build_query( array( 'text' => sprintf( /* translators: %s station */ __( 'Listening to %s', 'streamsera' ), $station['name'] ), 'url' => self::url(), ) ), 'facebook' => 'https://www.facebook.com/sharer/sharer.php?u=' . rawurlencode( self::url() ), 'whatsapp' => 'https://wa.me/?text=' . rawurlencode( $station['name'] . ' ' . self::url() ), ), ); /** * Filter the variables handed to the listen template. * * @param array $vars Vars. */ $vars = apply_filters( 'streamsera_listen_vars', $vars ); $file = locate_template( array( 'streamsera/listen.php' ) ); if ( ! $file ) { $file = STREAMSERA_PATH . 'templates/listen.php'; } /** * Filter the listen page template path. * * @param string $file Template. */ $file = streamsera_safe_template( apply_filters( 'streamsera_listen_template', $file ), STREAMSERA_PATH . 'templates/listen.php' ); nocache_headers(); status_header( 200 ); header( 'Content-Type: text/html; charset=utf-8' ); $streamsera = $vars; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable include $file; exit; } }