24. 前回のラジオプレーヤーで曲名表示

前回作成したラジオプレーヤーで、放送中の曲名が分かると良いなと思いネットで調べたら、PHPでの方法とpythonでの方法が見つかり、両方共試したがメインのプレーヤーがPHPなのでPHP方式にした。
ただ、フランスのFIP局や、ストリーミングがflacの局はタイトル取得がpython式/PHP式共に出来なかった。

上図の矢印部分に放送中のアーチスト名と曲名を表示する。

●PHP方式
参考サイトのサンプルのままでは、ネットラジオ局URLにport指定が無いとエラーになるので、無い場合はport:80を付加するようにした。

<?php define('CRLF', "\r\n"); class streaminfo{ public $valid = false; public $useragent = 'Winamp 2.81'; protected $headers = array(); protected $metadata = array(); public function __construct($location){ $errno = $errstr = ''; $t = parse_url($location); if(isset($t['port'])) $sock = fsockopen($t['host'], $t['port'], $errno, $errstr, 5); else $sock = fsockopen($t['host'], 80, $errno, $errstr, 5); $path = isset($t['path'])?$t['path']:'/'; if ($sock){ $request = 'GET '.$path.' HTTP/1.0' . CRLF . 'Host: ' . $t['host'] . CRLF . 'Connection: Close' . CRLF . 'User-Agent: ' . $this->useragent . CRLF . 'Accept: */*' . CRLF . 'icy-metadata: 1'.CRLF. 'icy-prebuffer: 65536'.CRLF. (isset($t['user'])?'Authorization: Basic '.base64_encode($t['user'].':'.$t['pass']).CRLF:''). 'X-TipOfTheDay: Winamp "Classic" rulez all of them.' . CRLF . CRLF; if (fwrite($sock, $request)){ $theaders = $line = ''; while (!feof($sock)){ $line = fgets($sock, 4096); if('' == trim($line)){ break; } $theaders .= $line; } $theaders = explode(CRLF, $theaders); foreach ($theaders as $header){ $t = explode(':', $header); if (isset($t[0]) && trim($t[0]) != ''){ $name = preg_replace('/[^a-z][^a-z0-9]*/i','', strtolower(trim($t[0]))); array_shift($t); $value = trim(implode(':', $t)); if ($value != ''){ if (is_numeric($value)){ $this->headers[$name] = (int)$value; }else{ $this->headers[$name] = $value; } } } } if (!isset($this->headers['icymetaint'])){ $data = ''; $metainterval = 512; while(!feof($sock)){ $data .= fgetc($sock); if (strlen($data) >= $metainterval) break; } $matches = array(); preg_match_all('/([\x00-\xff]{2})\x0\x0([a-z]+)=/i', $data, $matches, PREG_OFFSET_CAPTURE); preg_match_all('/([a-z]+)=([a-z0-9\(\)\[\]., ]+)/i', $data, $matches, PREG_SPLIT_NO_EMPTY); $title = $artist = ''; foreach ($matches[0] as $nr => $values){ $offset = $values[1]; $length = ord($values[0]{0}) + (ord($values[0]{1}) * 256)+ (ord($values[0]{2}) * 256*256)+ (ord($values[0]{3}) * 256*256*256); $info = substr($data, $offset + 4, $length); $seperator = strpos($info, '='); $this->metadata[substr($info, 0, $seperator)] = substr($info, $seperator + 1); if (substr($info, 0, $seperator) == 'title') $title = substr($info, $seperator + 1); if (substr($info, 0, $seperator) == 'artist') $artist = substr($info, $seperator + 1); } $this->metadata['streamtitle'] = $artist . ' - ' . $title; }else{ $metainterval = $this->headers['icymetaint']; $intervals = 0; $metadata = ''; while(1){ $data = ''; while(!feof($sock)){ $data .= fgetc($sock); if (strlen($data) >= $metainterval) break; } $len = join(unpack('c', fgetc($sock))) * 16; if ($len > 0){ $metadata = str_replace("\0", '', fread($sock, $len)); break; }else{ $intervals++; if ($intervals > 100) break; } } $metarr = explode(';', $metadata); foreach ($metarr as $meta){ $t = explode('=', $meta); if (isset($t[0]) && trim($t[0]) != ''){ $name = preg_replace('/[^a-z][^a-z0-9]*/i','', strtolower(trim($t[0]))); array_shift($t); $value = trim(implode('=', $t)); if (substr($value, 0, 1) == '"' || substr($value, 0, 1) == "'"){ $value = substr($value, 1); } if (substr($value, -1) == '"' || substr($value, -1) == "'"){ $value = substr($value, 0, -1); } if ($value != ''){ $this->metadata[$name] = $value; } } } } fclose($sock); $this->valid = true; }else echo 'unable to write.'; }else echo 'no socket '.$errno.' - '.$errstr.'.'; } public function __get($name){ if (isset($this->metadata[$name])){ return $this->metadata[$name]; } if (isset($this->headers[$name])){ return $this->headers[$name]; } return null; } } ?>

●python方式
python方式も参考サイトのサンプルのままではステータスに"ICY 200 OK"を返すラジオ局はエラーになってしまうので、別のネット情報に有った"ICY 200 OK"を"HTTP/1.0 200 OK"に替える仕組みを追加した。

#!/usr/bin/env python3 from __future__ import print_function import re import struct import sys from pprint import pprint try: import urllib2 except ImportError: # Python 3 import urllib.request as urllib2 # 'ICY 200 OK' status対応('HTTP/1.0 200 OK'に変換) def NiceToICY(self): class InterceptedHTTPResponse(): pass import io line = self.fp.readline().replace(b"ICY 200 OK\r\n", b"HTTP/1.0 200 OK\r\n") InterceptedSelf = InterceptedHTTPResponse() InterceptedSelf.fp = io.BufferedReader(io.BytesIO(line)) InterceptedSelf.debuglevel = self.debuglevel InterceptedSelf._close_conn = self._close_conn return ORIGINAL_HTTP_CLIENT_READ_STATUS(InterceptedSelf) # 'ICY 200 OK' status対応 ORIGINAL_HTTP_CLIENT_READ_STATUS = urllib2.http.client.HTTPResponse._read_status urllib2.http.client.HTTPResponse._read_status = NiceToICY if len(sys.argv) == 2: url = sys.argv[1] else: url = 'http://94.23.201.38:8020/stream' # radio stream for test #encoding = 'latin1' # default: iso-8859-1 for mp3 and utf-8 for ogg streams encoding = 'utf8' # default: iso-8859-1 for mp3 and utf-8 for ogg streams try: request = urllib2.Request(url, headers={'Icy-MetaData': 1}) # request metadata response = urllib2.urlopen(request) metaint = int(response.headers['icy-metaint']) except Exception as e: sys.exit('no icy-metaint found') for _ in range(5): # # title may be empty initially, try several times response.read(metaint) # skip to metadata metadata_length = struct.unpack('B', response.read(1))[0] * 16 # length byte metadata = response.read(metadata_length).rstrip(b'\0\n') # extract title from the metadata if metadata.find(b"StreamTitle=") != -1: title = metadata[metadata.find(b"='")+2:metadata.find(b"';")] if title != b"": # available title break else: sys.exit('no title found') print(title.decode(encoding, errors='replace')) sys.exit(0)

●raspi側再生プレーヤー修正
ラジオの放送中曲名が分かるようになったのでプレーヤーの機能追加修正を行った。また、ブラウザからPulseAudioで出力先をDAC等に切り替えても音声出力されなかったので、直にmpvコマンド実行からソケット通信で実行させる方法に変更した。

<?php require "radio_meta.php"; function execPlay($song){ require_once('./shelper1.php'); $sockethelper = new sockethelper('localhost',5557); $sockethelper->send_data("m$song"); $spkrs = $sockethelper->read_data() . PHP_EOL; $sockethelper->close_socket(); } function playReq($req_id) { if(is_numeric($req_id)){ // idが数値なら該当曲再生 $fullpath = getMusicPath($req_id); # play中のものは中止して新たに再生 if(!empty(exec('pgrep mpv'))){exec('sudo pkill -15 mpv');} exec('sudo -u pi rm ./tmp/album.jpg'); if(strpos($fullpath,'.flac') !== false){ $cmd = 'sudo -u pi metaflac "'.$fullpath.'" --export-picture-to=./tmp/album.jpg'; }else{ $cmd = 'sudo -u pi /usr/bin/ffmpeg -i "'.$fullpath.'" -an -s 150x150 -scodec copy ./tmp/album.jpg'; } exec($cmd . ' 2>&1', $array, $st); if($st==1){ $st_e = strrpos($fullpath, '/'); $st_path = substr($fullpath, 0, $st_e); $arr = glob($st_path.'/*.jpg'); exec('sudo -u pi cp "'.$arr[0].'" ./tmp/album.jpg'); } execPlay($fullpath); $msg = '再生開始'; }elseif(strpos($req_id,'http') == 0){ // radio play if(!empty(exec('pgrep mpv'))){exec('sudo pkill -15 mpv');} // mpvが起動していたらkill execPlay($req_id); $msg = 'RADIO開始'; }elseif(strpos($req_id,'rhttp') == 0){ // radio meta info if(!empty(exec('pgrep mpv'))){ // mpvが起動していたらkill $url = substr($req_id, 1); $t = new streaminfo($url); // get metadata if($t->valid){ if($t->streamtitle == ' - ') $msg = 'no title found'; else $msg = $t->streamtitle; } else $msg = 'invalid address'; }else{ $msg = 'no run mpv'; } }elseif(!empty(exec('pgrep mpv'))){ // mpvが起動している  :  :中略  : }else{ return 0; } return $msg; } function getMusicPath($id) { $attr = json_decode(file_get_contents('./tmp/sql-music.json'),true); $playlist = $attr["playlist"]; try{ # sqlite3 DBアクセス $db = new PDO('sqlite:./db/music.sqlite3'); # fetchモード設定 $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_COLUMN); $sql = "SELECT fpath FROM ".$playlist." WHERE id IN (0,".$id.");"; $mpath = $db->query($sql)->fetchAll(); $fullpath = $mpath[0].'/'.$mpath[1]; # base-path + music-path } catch (Exception $e) { echo $e->getMessage() . PHP_EOL; } $db = null; return $fullpath; } $response = array(); if($_SERVER['REQUEST_METHOD'] == 'POST') { // always return true if you save the contact data ok or false if it fails $tmp = playReq($_POST['id']); $response['status'] = $tmp!==0 ? 'success' : 'AP error'; $response['message'] = $response['status'] == 'success' ? $tmp : 'Your Requested ID:'.$_POST['id'].' is incorrect.'; header('Content-type: application/json'); echo json_encode($response); exit; } ?>

【参考】

  1. Shoutcast/Icecastストリームからアーティストとタイトルを抽出するためのPHPスクリプト
  2. Python 3 get song name from internet radio stream
  3. Record streaming and saving internet radio in python
このページは"Vue.js"を利用してみました。
2019/11/20