#!/bin/sh
#
# Radiko Live recorder
# Copyright (C) 2017-2019 uru (https://twitter.com/uru_2)
# License is MIT (see LICENSE file)
set -u
radiko_session=""
# Define authorize key value (from http://radiko.jp/apps/js/playerCommon.js)
readonly AUTHKEY_VALUE="bcd151073c03b352e1ef2fd66c32209da9ca0afa"
#######################################
# Show usage
# Arguments:
# None
# Returns:
# None
#######################################
show_usage() {
cat << _EOT_
Usage: $(basename "$0") [options]
Options:
-s STATION Station ID (see http://radiko.jp/v3/station/region/full.xml)
-d MINUTE Record minute
-m ADDRESS Radiko premium mail address
-p PASSWORD Radiko premium password
-o FILEPATH Output file path
_EOT_
}
#######################################
# Radiko Premium Login
# Arguments:
# Mail address
# Password
# Returns:
# 0: Success
# 1: Failed
#######################################
login() {
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" \
| tr -d "\r" \
| tr -d "\n")
# Extract login result
radiko_session=$(echo "${login_json}" | extract_login_value "radiko_session")
areafree=$(echo "${login_json}" | extract_login_value "areafree")
# Check login
if [ -z "${radiko_session}" ] || [ "${areafree}" != "1" ]; then
return 1
fi
return 0
}
#######################################
# Extract login JSON value
# Arguments:
# (pipe)Login result JSON
# Key
# Returns:
# None
#######################################
extract_login_value() {
name=$1
# for gawk
#value=$(cat - | gawk -v "name=${name}" 'BEGIN { FS = "\n"; } { regex = "\""name"\"[ ]*:[ ]*(\"[0-9a-zA-Z]+\"|[0-9]*)"; if (!match($0, regex, v)) { exit 0; } val=v[1]; if (match(val, /\"([0-9a-zA-Z]*)\"/, v)) { val=v[1]; } print val; }')
value=$(cat - \
| awk -v "name=${name}" '
BEGIN {
FS = "\n";
}
{
# Extract key and value
regex = "\""name"\"[ ]*:[ ]*(\"[0-9a-zA-Z]+\"|[0-9]*)";
if (!match($1, regex)) {
exit 0;
}
str = substr($0, RSTART, RLENGTH);
# Extract value
regex = "\""name"\"[ ]*:[ ]*";
match(str, regex);
str = substr(str, RSTART + RLENGTH);
# String value
if (match(str, /^\"[0-9a-zA-Z]+\"/)) {
print substr(str, RSTART + 1, RLENGTH - 2);
exit 0;
}
# Numeric value
if (match(str, /^[0-9]*/)) {
print substr(str, RSTART, RLENGTH);
exit 0;
}
}')
echo "${value}"
return 0
}
#######################################
# Radiko Premium Logout
# Arguments:
# None
# Returns:
# None
#######################################
logout() {
# Logout
curl \
--silent \
--request POST \
--data-urlencode "radiko_session=${radiko_session}" \
--output /dev/null \
"https://radiko.jp/v4/api/member/logout"
radiko_session=""
return 0
}
#######################################
# Finalize program
# Arguments:
# None
# Returns:
# None
#######################################
finalize() {
if [ -n "${radiko_session}" ]; then
logout
fi
return 0
}
#######################################
# 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"
}
# Define argument values
station_id=
duration=
mail=
password=
output=
# Argument none?
if [ $# -lt 1 ]; then
show_usage
finalize
exit 1
fi
# Parse argument
while getopts s:d:m:p:o: option; do
case "${option}" in
s)
station_id="${OPTARG}"
;;
d)
duration="${OPTARG}"
;;
m)
mail="${OPTARG}"
;;
p)
password="${OPTARG}"
;;
o)
output="${OPTARG}"
;;
\?)
show_usage
finalize
exit 1
;;
esac
done
# Check argument parameter
if [ -z "${station_id}" ]; then
# -s value is empty
echo "Require \"Station ID\"" >&2
finalize
exit 1
fi
if [ -z "$(echo "${duration}" | awk '/^[0-9]+$/ {print $0}')" ]; then
# -d value is invalid
echo "Invalid \"Record minute\"" >&2
finalize
exit 1
fi
# Login premium
if [ -n "${mail}" ]; then
login "${mail}" "${password}"
ret=$?
if [ ${ret} -ne 0 ]; then
echo "Cannot login Radiko premium" >&2
finalize
exit 1
fi
fi
# 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
echo "auth1 failed" >&2
finalize
exit 1
fi
partialkey=$(echo "${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
echo "auth2 failed" >&2
finalize
exit 1
fi
# Get playlist URL
areafree="0"
if [ -n "${radiko_session}" ]; then
areafree="1"
fi
playlist=$(curl \
--silent \
"http://radiko.jp/v2/station/stream_smh_multi/${station_id}.xml" \
| xmllint --xpath "/urls/url[@areafree='${areafree}'][1]/playlist_create_url/text()" -)
if [ -z "${playlist}" ]; then
echo "Cannot get playlist URL" >&2
finalize
exit 1
fi
# Generate default file path
if [ -z "${output}" ]; then
output="${station_id}_$(date +%Y%m%d%H%M%S).m4a"
else
# Fix file path extension
echo "${output}" | grep -q "\\.m4a$"
ret=$?
if [ ${ret} -ne 0 ]; then
# Add .m4a
output="${output}_$(date +%Y%m%d%H%M%S).m4a"
fi
fi
# Record
ffmpeg \
-loglevel error \
-fflags +discardcorrupt \
-headers "X-Radiko-Authtoken: ${authtoken}" \
-i "${playlist}" \
-acodec copy \
-vn \
-bsf:a aac_adtstoasc \
-y \
-t "$(format_time "${duration}")" \
"${output}"
# Finish
finalize
exit 0