(int) $station_id, 'format' => 'pls' === $format ? 'pls' : 'm3u', ), home_url( '/' ) ); } /** * URL of the OBS overlay page for a station. * * @param int $station_id Station. * @return string */ public static function overlay_url( $station_id ) { return add_query_arg( self::QUERY_OVERLAY, (int) $station_id, home_url( '/' ) ); } /** * Add the external-player links to the public station payload. * * @param array $data Payload. * @param int $station_id Station. * @return array */ public function station_links( $data, $station_id ) { if ( ! empty( $data['streams'] ) ) { $data['m3u'] = self::playlist_url( $station_id, 'm3u' ); $data['pls'] = self::playlist_url( $station_id, 'pls' ); } return $data; } /** * Expose the URL patterns to the admin app (Settings → Feeds & overlays). * * @param array $data Admin bootstrap. * @return array */ public function admin_data( $data ) { $data['feeds'] = array( 'm3u' => self::playlist_url( 0, 'm3u' ), 'pls' => self::playlist_url( 0, 'pls' ), 'overlay' => self::overlay_url( 0 ), 'text' => rest_url( self::NS . '/now-playing/0/text' ), 'json' => rest_url( self::NS . '/now-playing/0' ), ); return $data; } /* -------------------------------------------------------- front-end pages */ /** * Serve the playlist file or the overlay page. */ public function render() { $station_id = (int) get_query_var( self::QUERY_PLAYLIST ); if ( $station_id ) { $this->render_playlist( $station_id ); } $station_id = (int) get_query_var( self::QUERY_OVERLAY ); if ( $station_id ) { $this->render_overlay( $station_id ); } } /** * .m3u / .pls download. * * @param int $station_id Station. */ private function render_playlist( $station_id ) { $station = Station::to_public( $station_id ); if ( ! $station || empty( $station['streams'] ) ) { status_header( 404 ); exit; } $format = 'pls' === self::query_arg( 'format' ) ? 'pls' : 'm3u'; $slug = sanitize_title( $station['name'] ); $slug = $slug ? $slug : 'station'; nocache_headers(); header( 'X-Content-Type-Options: nosniff' ); header( 'Content-Disposition: attachment; filename="' . $slug . '.' . $format . '"' ); // Only the station's own stream URLs (sanitized as URLs) and its name/labels (sanitized as text) // go into the file, and every line is escaped again when it is printed. $name = sanitize_text_field( (string) $station['name'] ); $entries = array(); foreach ( $station['streams'] as $stream ) { $url = empty( $stream['url'] ) ? '' : esc_url_raw( (string) $stream['url'], array( 'http', 'https' ) ); if ( '' === $url ) { continue; } $label = empty( $stream['label'] ) ? '' : sanitize_text_field( (string) $stream['label'] ); $entries[] = array( 'url' => $url, 'title' => $name . ( '' !== $label ? ' (' . $label . ')' : '' ), ); } if ( 'pls' === $format ) { header( 'Content-Type: audio/x-scpls; charset=utf-8' ); echo "[playlist]\r\n"; foreach ( $entries as $i => $entry ) { $n = $i + 1; echo 'File' . absint( $n ) . '=' . esc_url_raw( $entry['url'] ) . "\r\n"; echo 'Title' . absint( $n ) . '=' . esc_html( $entry['title'] ) . "\r\n"; echo 'Length' . absint( $n ) . "=-1\r\n"; } echo 'NumberOfEntries=' . absint( count( $entries ) ) . "\r\n"; echo "Version=2\r\n"; } else { header( 'Content-Type: audio/x-mpegurl; charset=utf-8' ); echo "#EXTM3U\r\n"; foreach ( $entries as $entry ) { echo '#EXTINF:-1,' . esc_html( $entry['title'] ) . "\r\n"; echo esc_url_raw( $entry['url'] ) . "\r\n"; } } exit; } /** * Transparent now-playing card for OBS / vMix browser sources. * * Query options: `theme=light|dark` (default dark text on transparent), `art=0` hides artwork, * `size=sm|md|lg`, `align=left|center`. * * @param int $station_id Station. */ private function render_overlay( $station_id ) { $station = Station::to_public( $station_id ); if ( ! $station ) { status_header( 404 ); exit; } $np = Resolver::now_playing( $station_id ); $theme = 'light' === self::query_arg( 'theme' ) ? 'light' : 'dark'; $show_art = '0' !== self::query_arg( 'art' ); $size = self::query_arg( 'size' ); $size = in_array( $size, array( 'sm', 'lg' ), true ) ? $size : 'md'; $align = 'center' === self::query_arg( 'align' ) ? 'center' : 'left'; $interval = max( 5, (int) streamsera_get_setting( 'cache_ttl', 15 ) ); $endpoint = rest_url( self::NS . '/now-playing/' . $station_id ); $classes = array( 'ss-ov-theme-' . $theme, 'ss-ov-size-' . $size, 'ss-ov-align-' . $align ); nocache_headers(); header( 'Content-Type: text/html; charset=utf-8' ); ?> >