あなたの天然記念物
ホーム更新雑談Perl鉄ゲタランドナーコースガイド自転車Linuxリンク経歴連絡先

Radiko録音プログラム (2016.04.11)

背景

聞きたいラジオ放送が深夜で、加齢で夜更かしできなくなったので録音したい。

用意する物

実際の処理をするプログラムを入手して同じフォルダに置いてください。
  1. rtmpdump
  2. swfextract

使い方

書式
perl rec_radiko.pl --tune_id=≪放送局ID≫ --rec_minute=≪録音分数≫ --save_file=≪保存ファイル名≫ --save_base=≪保存先パス名≫
省略したパラメタはそれぞれ、 となります。あとはスクリプトをタスクスケジューラから起動すれば留守録できます。

実行例

**********>perl rec_radiko.pl --rec_minute=1
起動。
オプション:rec_minute = 1
オプション:save_base = **********\Desktop

  1> AIR-G
  2> HBC
  3> HOUSOU-DAIGAKU
  4> NORTHWAVE
  5> RN1
  6> RN2
  7> STV


録音する放送局は? [1]: 1
録音する放送局:AIR-G
録音開始。
RTMPDump v2.3
(c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team; license: GPL
Connecting ...
WARNING: Trying different position for server digest!
INFO: Connected...
Starting Live Stream
For duration: 60.000 sec
INFO: Metadata:
INFO:   StreamTitle
365.687 kB / 60.03 sec
Download complete
録音終了:**********\Desktop/20160411_224426_AIR-G.flv

**********>

スクリプト

ファイル名はrec_radiko.pl、文字コードはutf8、BOMなしで。
#!/usr/local/bin/perl -w 

use utf8;
use strict;
use warnings;
use open IO => ":utf8";

use Encode::Locale;
use File::HomeDir;
use Getopt::Long;
use Term::UI;
use Term::ReadLine;

use Radiko;

$| = 1;

binmode STDOUT, ":encoding(console_out)";
binmode STDERR, ":encoding(console_out)";

print "起動。\n";

my %opts = (
	save_base => File::HomeDir->my_desktop,
);
GetOptions(\%opts, qw/ tune_id=s rec_minute=i save_file=s save_base=s /) or die "オプションが違います。\n";
if (0 < @ARGV) {
	die "オプションが多いです:'@ARGV'\n";
}
for (sort keys %opts) {
	my $v = $opts{$_};
	print "オプション:$_ = $v\n";
}

my $radiko = Radiko->new(\%opts);
my @station = $radiko->get_station_id();
my $tune_id = exists $opts{tune_id} ? $opts{tune_id} : "";
if (grep /^$tune_id$/i, @station) {
	print "受信可能な放送局:@station\n";
} else {
	my $term = Term::ReadLine->new("録音する放送局");
	$tune_id = $term->get_reply(
		prompt => Encode::encode("locale", "録音する放送局は?"),
		choices => \@station,
		default => $station[0],
	);
}
print "録音する放送局:$tune_id\n";

print "録音開始。\n";
my $flv_file = $radiko->record(
	{
		tune_id => $tune_id,
	},
);
print "録音終了:$flv_file\n";

exit;

モジュール

ファイル名Radiko.pm、文字コードはutf8、BOMなしで。
package Radiko;

use utf8;
use strict;
use warnings;

use DateTime;
use Encode;
use LWP::UserAgent;
use MIME::Base64;
use XML::Simple;



sub new {
	my $class = shift;


	my $self = {
		_ua => LWP::UserAgent->new(),

		_radiko => "https://radiko.jp",
		_list_base => "http://radiko.jp/v2/station/list",
		_stream_base => "http://radiko.jp/v2/station/stream",
		_player_base => "http://radiko.jp",

		_key_file => "player.swf.key",
		_rtmp_dump => "rtmpdump.exe",
		_swf_extract => "swfextract.exe",

		swf_file => "",
		tune_id => "STV",
		rec_minute => 60,
	};

	-e $self->{_rtmp_dump} or die "$self->{_rtmp_dump}がありません。\n";
	-e $self->{_swf_extract} or die "$self->{_swf_extract}がありません。\n";

	my ($opts_ref) = @_;
	for (keys %$opts_ref) {
		$self->{$_} = $$opts_ref{$_};
	}

	return bless $self, $class;
}



sub get_swffile {
	my $self = shift;

	my $player_js;
	my $res = $self->{_ua}->get("$self->{_radiko}/");
	if ($res->is_success) {
		($player_js) = grep /script.*player/, split /\n/, Encode::decode "utf8", $res->content;
		$player_js =~ m#src="(.*)"></script>#;
		$player_js = "$self->{_radiko}/$1";
	} else {
		print "get_swffile:失敗。\n";
	}

	my $player_ver;
	my $player_path;
	my $player_date;
	$res = $self->{_ua}->get($player_js);
	if ($res->is_success) {
		my @html = split /\n/, Encode::decode "utf8", $res->content;
		($player_ver, $player_path) = grep /playerVersion/, @html;
		$player_ver =~ /"(.*)"/;
		$player_ver = $1;
		$player_path =~ m#"(.*)"\+playerVersion\+"\.swf\?_=(.*)",#;
		$player_path = $1;
		$player_date = $2;
	} else {
		print "get_swffile:失敗。\n";
	}

	$self->{swf_file} = $player_path;
	$self->{swf_file} =~ m#([^/]+)$#;
	$self->{swf_file} = "$1$player_ver.swf";
	$res = $self->{_ua}->get("$self->{_player_base}$player_path$player_ver.swf?_=$player_date", ":content_file" => $self->{swf_file});
	if (!$res->is_success) {
		print "get_swffile:失敗。\n";
	}
}



sub get_keyfile {
	my $self = shift;
	print "get_keyfile\n";

	$self->get_swffile() if !defined $self->{swf_file} || !-e $self->{swf_file};

	system Encode::encode "cp932", "$self->{_swf_extract} -b 14 $self->{swf_file} -o $self->{_key_file}";
}



sub get_token {
	my $self = shift;

	my $res = $self->{_ua}->post(
		"$self->{_radiko}/v2/api/auth1_fms",
		pragma => "no-cache",
		X_Radiko_App => "pc_1",
		X_Radiko_App_Version => "2.0.1",
		X_Radiko_User => "test-stream",
		X_Radiko_Device => "pc",
		Content => "\r\n",
	);

	if ($res->is_success) {
		my %auth1;
		for (split /\r\n/, Encode::decode "utf8", $res->content) {
			chomp;
			if (/^X/) {
				my ($k, $v) = split /=/;
				$auth1{$k} = $v;
			}
		}

		$self->{authtoken} = $auth1{"X-Radiko-AuthToken"},
		$self->{key_offset} = $auth1{"X-Radiko-KeyOffset"};
		$self->{key_length} = $auth1{"X-Radiko-KeyLength"};
	} else {
		print "get_token:失敗。\n";
	}
}



sub get_key {
	my $self = shift;

	$self->get_keyfile() if !-e $self->{_key_file};
	$self->get_token() if !defined $self->{key_offset};

	my $authkey;
	if (open my $key, "<:raw", $self->{_key_file}) {
		binmode $key;
		seek $key, $self->{key_offset}, 0;
		read $key, $authkey, $self->{key_length};
		close $key;
	} else {
		print "get_key:失敗:$self->{_key_file}\n";
	}

	$self->{authkey} = encode_base64($authkey, "");
}



sub get_region {
	my $self = shift;

	$self->get_key() if !defined $self->{authkey};
	if (defined $self->{authkey}) {
		my $res = $self->{_ua}->post(
			"$self->{_radiko}/v2/api/auth2_fms",
			pragma => "no-cache",
			X_Radiko_App => "pc_1",
			X_Radiko_App_Version => "2.0.1",
			X_Radiko_User => "test-stream",
			X_Radiko_Device => "pc",
			X_Radiko_AuthToken => $self->{authtoken},
			X_Radiko_PartialKey => $self->{authkey},
			Content => "\r\n",
		);

		my @region;
		if ($res->is_success) {
			my $con = Encode::decode "utf8", $res->content;
			@region = split /\r\n/, Encode::decode "utf8", $res->content;
			my ($region) = grep /./, @region;
			@region = split /,/, $region;
			$self->{region} = $region[0];
		} else {
			print "get_region:失敗。\n";
		}
	}
}



sub get_station_id {
	my $self = shift;

	$self->get_region() if !defined $self->{region};
	if (defined $self->{region}) {
		my $res = $self->{_ua}->get("$self->{_list_base}/$self->{region}.xml");
		if ($res->is_success) {
			my $xml = XMLin($res->content);
			@{$self->{station_id}} = sort map {$xml->{station}->{$_}->{id}} keys %{$xml->{station}};
		} else {
			print "get_station_id:失敗。\n";
		}
	}

	return @{$self->{station_id}};
}



sub get_stream {
	my $self = shift;

	$self->get_station_id() if !defined $self->{station_id};
	my ($tuned) = grep /^$self->{tune_id}$/i, @{$self->{station_id}};
	if (defined $tuned) {
		$self->{tuned} = $tuned;
		my $guide_url = "$self->{_stream_base}/$self->{tuned}.xml";
		my $res = $self->{_ua}->get($guide_url);
		if ($res->is_success) {
			my $xml = XMLin($res->content);
			$self->{stream_url} = $xml->{item}->[0];
		} else {
			print "get_stream:失敗。\n";
		}
	}
}



sub record {
	my $self = shift;
	my ($opts_ref) = @_;
	for (keys %$opts_ref) {
		$self->{$_} = $$opts_ref{$_};
	}

	$self->get_stream() if !defined $self->{stream_url};
	my $save_file = DateTime->now(
		time_zone => "Asia/Tokyo",
		locale => "ja",
	)->strftime("%Y%m%d_%H%M%S_$self->{tuned}.flv");
	$save_file = $self->{save_file} if defined $self->{save_file};
	if (exists $self->{save_base}) {
		$save_file = "$self->{save_base}/$save_file";
	}
	if (defined $self->{stream_url}) {

		my $rec_minute = 60;
		$rec_minute = $self->{rec_minute} if defined $self->{rec_minute};

		my $rec_second = 60 * $rec_minute;

		system Encode::encode "cp932", "$self->{_rtmp_dump} --rtmp \"$self->{stream_url}\" -C S:\"\" -C S:\"\" -C S:\"\" -C S:\"$self->{authtoken}\" --live --stop $rec_second --flv \"$save_file\"";
	}

	return $save_file;
}



1;