__( 'In-stream (ICY) metadata', 'streamsera' ), 'description' => __( 'Reads the title the stream itself carries (Zeno and most hosted streams). No status page needed.', 'streamsera' ), 'usesUrl' => false, 'usesMount' => false, ); } /** * {@inheritdoc} * * @param array $source Source definition (url, mount, stream_url). */ public function fetch( array $source ) { $url = ! empty( $source['stream_url'] ) ? (string) $source['stream_url'] : ''; if ( ! $url ) { return null; } /** * Seconds allowed for reading the first in-stream metadata block. * * @param int $timeout Default 6. */ $response = Resolver::probe_headers( $url, (int) apply_filters( 'streamsera_icy_timeout', 6 ), true ); if ( ! is_array( $response ) || empty( $response['icy_meta'] ) ) { return null; } $raw = self::stream_title( (string) $response['icy_meta'] ); if ( null === $raw ) { return null; } // Hosts often send "- Title" or "Artist -" when one part is missing. // trim() works on bytes, which would cut multibyte dashes (and any title ending in a // character that shares their bytes) apart: strip them as characters. $raw = (string) preg_replace( '/^[\s\-–—]+|[\s\-–—]+$/u', '', $raw ); $parts = Resolver::split_title( $raw ); $h = (array) $response['headers']; return array( 'title' => $parts['title'], 'artist' => $parts['artist'], 'raw' => $raw, 'listeners' => null, 'live' => true, 'server' => 'icy', 'server_name' => isset( $h['icy-name'] ) ? (string) $h['icy-name'] : '', 'bitrate' => isset( $h['icy-br'] ) ? (int) $h['icy-br'] : null, ); } /** * Convert a Latin-1 (ISO-8859-1) string to UTF-8. * * @param string $text Latin-1 text. * @return string */ private static function latin1_to_utf8( $text ) { if ( function_exists( 'mb_convert_encoding' ) ) { return (string) mb_convert_encoding( $text, 'UTF-8', 'ISO-8859-1' ); } // Every Latin-1 byte is the Unicode code point of the same value. return (string) preg_replace_callback( '/[\x80-\xFF]/', static function ( $m ) { $b = ord( $m[0] ); return chr( 0xC0 | ( $b >> 6 ) ) . chr( 0x80 | ( $b & 0x3F ) ); }, $text ); } /** * StreamTitle from a metadata block (`StreamTitle='…';StreamUrl='…';`). * * @param string $meta Raw block. * @return string|null Title (may be empty when the station sends none), null if absent. */ public static function stream_title( $meta ) { if ( ! preg_match( "/StreamTitle='(.*?)';(?:Stream|$)/s", $meta, $m ) ) { return null; } $title = $m[1]; if ( ! preg_match( '//u', $title ) ) { // Not valid UTF-8: older encoders send Latin-1. $title = self::latin1_to_utf8( $title ); } return trim( wp_strip_all_tags( $title ) ); } }