ルモーリン

Radiko録音プログラム

投稿:2016-04-11

聞きたいラジオ放送が深夜で、加齢で夜更かしできなくなったので録音したい。
実際の処理をするプログラムを入手して同じフォルダに置いてください。
  1. rtmpdump
  2. swfextract
書式
perl rec_radiko.pl --tune_id=≪放送局ID≫ --rec_minute=≪録音分数≫ --save_file=≪保存ファイル名≫ --save_base=≪保存先パス名≫
省略したパラメタはそれぞれ、
  • tune_id→受信可能な放送局のID一覧を表示、番号で選択
  • rec_minute→60分
  • save_file→YYYYMMDD_HHMMSS_xxxx.flv(年月日_時分秒_放送局ID.flv)
  • 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;