#!/bin/sh # # Japan internet radio live streaming recoder # Copyright (C) 2019 uru (https://twitter.com/uru_2) # License is MIT (see LICENSE file) set -u ####################################### # Show usage # Arguments: # None # Returns: # None ####################################### show_usage() { cat << _EOT_ Usage: $(basename "$0") [options] Options: -t TYPE Record type nhk: NHK Radidu radiko: radiko lisradi: ListenRadio jcba: JCBA simul radio shiburadi: Shibuya no Radio -s STATION ID Station ID -d MINUTE Record minute(s) -o FILEPATH Output file path -i ADDRESS login mail address (radiko only) -p PASSWORD login password (radiko only) -l Show all station ID list _EOT_ } ####################################### # Show all station ID and name # Arguments: # None # Returns: # None ####################################### show_all_stations() { # Radiru echo "Record type: nhk" list=$(curl --silent "https://www.nhk.or.jp/radio/config/config_v5.7.3_radiru_and.xml") cnt=$(echo "${list}" | xmllint --xpath "count(/radiru_config/area)" - 2> /dev/null) cntarray=$(echo "${cnt}" | awk '{for (i=1; i<=$1; i++) print i}') for i in ${cntarray}; do echo " $(echo "${list}" | xmllint --xpath "concat(string((/radiru_config/area)[${i}]/@id), '-r1: ', string((/radiru_config/area)[${i}]/@name), ' R1')" - 2> /dev/null)" echo " $(echo "${list}" | xmllint --xpath "concat(string((/radiru_config/area)[${i}]/@id), '-fm: ', string((/radiru_config/area)[${i}]/@name), ' FM')" - 2> /dev/null)" done echo " r2: R2" echo "" # radiko echo "Record type: radiko" list=$(curl --silent "http://radiko.jp/v3/station/region/full.xml") cnt=$(echo "${list}" | xmllint --xpath "count(/region/stations/station)" - 2> /dev/null) cntarray=$(echo "${cnt}" | awk '{for (i=1; i<=$1; i++) print i}') for i in ${cntarray}; do echo " $(echo "${list}" | xmllint --xpath "concat((/region/stations/station)[${i}]/id/text(), ': ', (/region/stations/station)[${i}]/name/text())" - 2> /dev/null)" done echo "" # ListenRadio echo "Record site type: lisradi" curl --silent "http://listenradio.jp/service/channel.aspx" | jq -r '.Channel[] | " " + (.ChannelId | tostring) + ": " + .ChannelName' 2> /dev/null echo "" # JCBA echo "Record type: jcba" list=$(curl --silent "https://www.jcbasimul.com/") cnt=$(echo "${list}" | xmllint --html --xpath "count(//div[@class='stationList']//div[@class='areaList']/ul[@class='cf']/li)" - 2> /dev/null) cntarray=$(echo "${cnt}" | awk '{for (i=1; i<=$1; i++) print i}') for i in ${cntarray}; do echo " $(echo "${list}" | xmllint --html --xpath "concat(string((//div[@class='stationList']//div[@class='areaList']/ul[@class='cf']/li)[${i}]//div[@class='rplayer']/@id), ': ', string((//div[@class='stationList']//div[@class='areaList']/ul[@class='cf']/li)[${i}]/@value))" - 2> /dev/null)" done echo "" # Shibuya no Radio echo "Record type: shiburadi" echo " None" echo "" } ####################################### # Radiko Login # Arguments: # Mail address # Password # Returns: # 0: Success # 1: Failed ####################################### login_radiko() { mail=$1 password=$2 # Login login_json=$(curl \ --silent \ --request POST \ --data-urlencode "mail=${mail}" \ --data-urlencode "pass=${password}" \ --output - \ "https://radiko.jp/v4/api/member/login") # Extract login result radiko_session=$(echo "${login_json}" | jq -r ".radiko_session") areafree=$(echo "${login_json}" | jq -r ".areafree") # Check login if [ -z "${radiko_session}" ] || [ "${areafree}" != "1" ]; then return 1 fi echo "${radiko_session}" return 0 } ####################################### # Radiko Logout # Arguments: # radiko Session # Returns: # None ####################################### logout_radiko() { radiko_session=$1 # Logout curl \ --silent \ --request POST \ --data-urlencode "radiko_session=${radiko_session}" \ --output /dev/null \ "https://radiko.jp/v4/api/member/logout" } ####################################### # Authorize radiko # Arguments: # radiko Session # Returns: # 0: Success # 1: Failed ####################################### radiko_authorize() { radiko_session=$1 # Define authorize key value (from http://radiko.jp/apps/js/playerCommon.js) RADIKO_AUTHKEY_VALUE="bcd151073c03b352e1ef2fd66c32209da9ca0afa" # Authorize 1 auth1_res=$(curl \ --silent \ --header "X-Radiko-App: pc_html5" \ --header "X-Radiko-App-Version: 0.0.1" \ --header "X-Radiko-Device: pc" \ --header "X-Radiko-User: dummy_user" \ --dump-header - \ --output /dev/null \ "https://radiko.jp/v2/api/auth1") # Get partial key authtoken=$(echo "${auth1_res}" | awk 'tolower($0) ~/^x-radiko-authtoken: / {print substr($0,21,length($0)-21)}') keyoffset=$(echo "${auth1_res}" | awk 'tolower($0) ~/^x-radiko-keyoffset: / {print substr($0,21,length($0)-21)}') keylength=$(echo "${auth1_res}" | awk 'tolower($0) ~/^x-radiko-keylength: / {print substr($0,21,length($0)-21)}') if [ -z "${authtoken}" ] || [ -z "${keyoffset}" ] || [ -z "${keylength}" ]; then return 1 fi partialkey=$(echo "${RADIKO_AUTHKEY_VALUE}" | dd bs=1 "skip=${keyoffset}" "count=${keylength}" 2> /dev/null | base64) # Authorize 2 auth2_url_param="" if [ -n "${radiko_session}" ]; then auth2_url_param="?radiko_session=${radiko_session}" fi curl \ --silent \ --header "X-Radiko-Device: pc" \ --header "X-Radiko-User: dummy_user" \ --header "X-Radiko-AuthToken: ${authtoken}" \ --header "X-Radiko-PartialKey: ${partialkey}" \ --output /dev/null \ "https://radiko.jp/v2/api/auth2${auth2_url_param}" ret=$? if [ ${ret} -ne 0 ]; then return 1 fi echo "${authtoken}" return 0 } ####################################### # Get NHK Radiru HLS streaming URI # Arguments: # Station ID # Returns: # None ####################################### get_hls_uri_nhk() { station_id=$1 if [ "${station_id}" = "r2" ]; then # R2 curl --silent "https://www.nhk.or.jp/radio/config/config_v5.7.3_radiru_and.xml" | xmllint --xpath "string(/radiru_config/config[@key='url_stream_r2']/value[1]/@text)" - 2> /dev/null else # Split area and channel area="$(echo "${station_id}" | cut -d '-' -f 1)" channel="$(echo "${station_id}" | cut -d '-' -f 2)" curl --silent "https://www.nhk.or.jp/radio/config/config_v5.7.3_radiru_and.xml" | xmllint --xpath "string(/radiru_config/area[@id='${area}']/config[@key='url_stream_${channel}']/value[1]/@text)" - 2> /dev/null fi } ####################################### # Get radiko HLS streaming URI # Arguments: # Station ID # radiko login status # Returns: # None ####################################### get_hls_uri_radiko() { station_id=$1 radiko_login_status=$2 areafree="0" if [ "${radiko_login_status}" = "1" ]; then areafree="1" fi curl --silent "http://radiko.jp/v2/station/stream_smh_multi/${station_id}.xml" | xmllint --xpath "/urls/url[@areafree='${areafree}'][1]/playlist_create_url/text()" - 2> /dev/null } ####################################### # Get ListenRadio HLS streaming URI # Arguments: # Station ID # Returns: # None ####################################### get_hls_uri_lisradi() { station_id=$1 curl --silent "http://listenradio.jp/service/channel.aspx" | jq -r ".Channel[] | select(.ChannelId == ${station_id}) | .ChannelHls" 2> /dev/null } ####################################### # Get JCBA HLS streaming URI # Arguments: # Station ID # Returns: # None ####################################### get_hls_uri_jcba() { station_id=$1 echo "https://musicbird-hls.leanstream.co/musicbird/${station_id}.stream/playlist.m3u8" } ####################################### # Get Shibuya no Radio HLS streaming URI # Arguments: # None # Returns: # None ####################################### get_hls_uri_shiburadi() { curl --silent "https://shibuyanoradio.info/infoapi/?ver=1.1" | jq -r ".basicinfo.hls_playback" 2> /dev/null } ####################################### # Format time text # Arguments: # Time minute # Returns: # None ####################################### format_time() { minute=$1 hour=$((minute / 60)) minute=$((minute % 60)) printf "%02d:%02d:%02d" "${hour}" "${minute}" "0" } ##### Main routine start ##### # Argument none? if [ $# -lt 1 ]; then show_usage exit 1 fi # Parse argument type="" station_id="" duration=0 output="" login_id="" login_password="" while getopts t:s:d:o:i:p:l option; do case "${option}" in t) type="${OPTARG}" ;; s) station_id="${OPTARG}" ;; d) duration="${OPTARG}" ;; o) output="${OPTARG}" ;; i) login_id="${OPTARG}" ;; p) login_password="${OPTARG}" ;; l) show_all_stations exit 0 ;; \?) show_usage exit 1 ;; esac done # Set value from ENV if [ "${type}" = "radiko" ]; then if [ -z "${login_id}" ]; then env | grep -q -E "^RADIKO_MAIL=" ret=$? if [ ${ret} -eq 0 ]; then login_id="${RADIKO_MAIL}" fi fi if [ -z "${login_password}" ]; then env | grep -q -E "^RADIKO_PASSWORD=" ret=$? if [ ${ret} -eq 0 ]; then login_password="${RADIKO_PASSWORD}" fi fi fi # Check argument parameter if [ -z "${type}" ]; then # -t value is empty # echo "Require \"Type\"" >&2 # exit 1 type="radiko"; # radiko as default fi echo "${duration}" | grep -q -E "^[0-9]+$" ret=$? if [ ${ret} -ne 0 ]; then # -d value is invalid echo "Invalid \"Record minute\"" >&2 exit 1 fi if [ "${type}" = "shiburadi" ]; then station_id="shiburadi" else if [ -z "${station_id}" ]; then # -s value is empty echo "Require \"Station ID\"" >&2 exit 1 fi fi # Generate default file path file_ext="m4a" if [ "${type}" = "shiburadi" ]; then file_ext="mp3" fi if [ -z "${output}" ]; then output="${station_id}_$(date +%Y%m%d%H%M%S).${file_ext}" else # Fix file path extension echo "${output}" | grep -q -E "\\.${file_ext}$" ret=$? if [ ${ret} -ne 0 ]; then output="${output}.${file_ext}" fi fi playlist_uri="" radiko_session="" radiko_authtoken="" # Record type processes if [ "${type}" = "nhk" ]; then # NHK playlist_uri=$(get_hls_uri_nhk "${station_id}") elif [ "${type}" = "lisradi" ]; then # ListenRadio playlist_uri=$(get_hls_uri_lisradi "${station_id}") elif [ "${type}" = "jcba" ]; then # JCBA playlist_uri=$(get_hls_uri_jcba "${station_id}") elif [ "${type}" = "shiburadi" ]; then # Shibuya no Radio playlist_uri=$(get_hls_uri_shiburadi) elif [ "${type}" = "radiko" ]; then # radiko radiko_login_status="0" # Login radiko premium if [ -n "${login_id}" ]; then radiko_session=$(login_radiko "${login_id}" "${login_password}") ret=$? if [ ${ret} -ne 0 ]; then echo "Cannot login radiko premium" >&2 exit 1 fi radiko_login_status="1" fi # Authorize radiko_authtoken=$(radiko_authorize "${radiko_session}") ret=$? if [ ${ret} -ne 0 ]; then echo "radiko authorize failed" >&2 logout_radiko "${radiko_session}" exit 1 fi playlist_uri=$(get_hls_uri_radiko "${station_id}" "${radiko_login_status}") fi if [ -z "${playlist_uri}" ]; then echo "Cannot get playlist URI" >&2 if [ "${type}" = "radiko" ]; then logout_radiko "${radiko_session}" fi exit 1 fi # Play if [ "${type}" = "radiko" ]; then ffplay \ -loglevel error \ -fflags +discardcorrupt \ -headers "X-Radiko-Authtoken: ${radiko_authtoken}" \ -i "${playlist_uri}" \ -nodisp else ffplay \ -loglevel error \ -fflags +discardcorrupt \ -i "${playlist_uri}" \ -nodisp fi ret=$? if [ ${ret} -ne 0 ]; then echo "Record failed" >&2 if [ "${type}" = "radiko" ]; then logout_radiko "${radiko_session}" fi exit 1 fi # Finish if [ "${type}" = "radiko" ]; then logout_radiko "${radiko_session}" fi exit 0 ##### Main routine end #####