Newer
Older
radiko_rec_server / radi.sh
@Kurokawa Kurokawa on 16 Dec 2024 11 KB version 1.1
#!/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
                    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.8.0_radiru_and.xml")
  cnt=$(echo "${list}" | xmllint --xpath "count(/radiru_config/area)" - 2> /dev/null)
  for i in $(awk "BEGIN { for (i = 1; i <= ${cnt}; i++) { print i } }"); 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 "https://radiko.jp/v3/station/region/full.xml")
  cnt=$(echo "${list}" | xmllint --xpath "count(/region/stations/station)" - 2> /dev/null)
  for i in $(awk "BEGIN { for (i = 1; i <= ${cnt}; i++) { print i } }"); 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 ""

  # 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/ap/member/webapi/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 https://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.8.0_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.8.0_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 "https://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 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
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_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}" = "shiburadi" ]; then
  # Shibuya no Radio
  playlist_uri=$(get_hls_uri_shiburadi)
elif [ "${type}" = "radiko" ]; then
  # radiko
  radiko_session=""
  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

    # Register radiko logout handler
    trap "logout_radiko ""${radiko_session}""" EXIT HUP INT QUIT TERM

    radiko_login_status="1"
  fi

  # Authorize
  radiko_authtoken=$(radiko_authorize "${radiko_session}")
  ret=$?
  if [ ${ret} -ne 0 ]; then
    echo "radiko authorize failed" >&2
    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
  exit 1
fi

# Record
if [ "${type}" = "radiko" ]; then
  ffmpeg \
      -loglevel error \
      -fflags +discardcorrupt \
      -headers "X-Radiko-Authtoken: ${radiko_authtoken}" \
      -i "${playlist_uri}" \
      -acodec copy \
      -vn \
      -bsf:a aac_adtstoasc \
      -y \
      -t "$(format_time "${duration}")" \
      "${output}"
elif [ "${type}" = "shiburadi" ]; then
  ffmpeg \
      -loglevel error \
      -fflags +discardcorrupt \
      -i "${playlist_uri}" \
      -acodec copy \
      -vn \
      -y \
      -t "$(format_time "${duration}")" \
      "${output}"
else
  ffmpeg \
      -loglevel error \
      -fflags +discardcorrupt \
      -i "${playlist_uri}" \
      -acodec copy \
      -vn \
      -bsf:a aac_adtstoasc \
      -y \
      -t "$(format_time "${duration}")" \
      "${output}"
fi
ret=$?
if [ ${ret} -ne 0 ]; then
  echo "Record failed" >&2
  exit 1
fi

# Finish
exit 0
##### Main routine end #####