rbtune 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ # Pit のフロントエンド。
2
+ # key に uri を指定すると、accountでアカウントとパスワードのセットを得られる。
3
+ # key が空の場合、queryによってアカウントとパスワードのセットを登録できる。
4
+ # パスワードの難読化はごく甘く、単にBase64でエンコードしているのみ。
5
+
6
+ require "pit"
7
+ require "base64"
8
+ require 'io/console'
9
+
10
+ class KeyChain
11
+ attr_reader :key
12
+
13
+ def initialize(key)
14
+ @key = key
15
+ end
16
+
17
+
18
+ def account
19
+ radiko = Pit.get(key)
20
+ if radiko.has_key? :account and radiko.has_key? :password
21
+ [ radiko[:account], Base64.decode64(radiko[:password]) ]
22
+ end
23
+ end
24
+
25
+
26
+ def set(account, password)
27
+ Pit.set(key, data: { account: account, password: Base64.encode64(password)})
28
+ [account, password]
29
+ end
30
+
31
+
32
+ def query(prompt, account)
33
+ old_account, old_password = account()
34
+ puts prompt
35
+ print ' password : '
36
+ STDOUT.flush
37
+ new_password = STDIN.noecho(&:gets).chomp
38
+ puts
39
+ case
40
+ when new_password.empty?
41
+ raise "空のパスワードは無効です (何も変更されません)"
42
+ when new_password == old_password
43
+ raise "旧パスワードと同じパスワードです"
44
+ end
45
+ new_password
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ # ffmpeg.rb
2
+ require "timeout"
3
+ require "open3"
4
+ require "player/mplayer"
5
+
6
+ class FFMpeg < Player
7
+ def initialize
8
+ self['loglevel'] = 'warning'
9
+ self['n'] = '' # do not overwrite
10
+ @mplayer = Mplayer.new('-')
11
+ end
12
+
13
+
14
+ def command
15
+ 'ffmpeg'
16
+ end
17
+
18
+ def options
19
+ map{ |k,v| "-#{k} #{v}"}*' '
20
+ end
21
+
22
+
23
+ def to_s
24
+ # novideoオプション -vn は、この位置でないと機能しない
25
+ %Q(#{command} #{options} -vn #{@output})
26
+ end
27
+
28
+
29
+ def play
30
+ self['f'] = 'mpegts'
31
+ @output = 'pipe:1'
32
+ cmd = "#{to_s} | #{@mplayer}"
33
+ $stderr.puts 'play: '+cmd
34
+ `#{cmd}`
35
+ raise $? unless $?.success?
36
+ end
37
+
38
+
39
+ def rec(file, sec, quiet = true)
40
+ self['t'] = sec if sec
41
+ if quiet
42
+ @output = file
43
+ cmd = to_s
44
+ else
45
+ self['f'] = 'mpegts'
46
+ @output = 'pipe:1'
47
+ cmd = "#{to_s} | tee #{file} | #{@mplayer}"
48
+ end
49
+
50
+ puts "rec: #{cmd}"
51
+ stdout, stderr, status = Open3.capture3(cmd)
52
+ end
53
+
54
+ end
@@ -0,0 +1,66 @@
1
+ # mplayer.rb
2
+ require "timeout"
3
+ require "open3"
4
+ require "player/player"
5
+
6
+ class Mplayer < Player
7
+
8
+ def initialize(url)
9
+ @url=url
10
+ self['cache'] = 64
11
+ self['cache-min'] = 16
12
+ self['quiet'] = '' # 画面表示をしない
13
+ end
14
+
15
+
16
+ def command
17
+ 'mplayer'
18
+ end
19
+
20
+ def options
21
+ map{ |k,v| "-#{k} #{v}"}*' '
22
+ end
23
+
24
+
25
+ def to_s
26
+ %Q(#{command} #{@url} #{options} )
27
+ end
28
+
29
+
30
+ def play
31
+ puts to_s
32
+ `#{to_s}`
33
+ end
34
+
35
+
36
+ def rec(file, sec, quiet = true)
37
+ self['dumpstream'] = ''
38
+ self['dumpfile'] = file
39
+ if quiet
40
+ self['nosound'] = '' # 音声を再生しない
41
+ end
42
+
43
+ puts "rec (#{sec}s): #{to_s}"
44
+ stdin, stdout, stderr, wait_thread = Open3.popen3(to_s)
45
+ dsec = -1
46
+ psec = dsec
47
+ wait = 0
48
+ i = 0
49
+ WAIT_LIMIT = 3
50
+ while dsec <= sec
51
+ dsec = duration(file)
52
+ if dsec == psec
53
+ wait += 1
54
+ break if wait > WAIT_LIMIT
55
+ else
56
+ wait = 0
57
+ psec = dsec
58
+ end
59
+ p [i, dsec, psec, wait] if wait > 0
60
+ i += 1
61
+ sleep 1
62
+ end
63
+ stdin.write 'q'
64
+ end
65
+
66
+ end
@@ -0,0 +1,29 @@
1
+ # Player 仮想クラス
2
+ # Mplayer等で継承して使う
3
+
4
+ class Player < Hash
5
+
6
+ def command; end
7
+ def play; end
8
+ def rec(file, sec, quiet = true); end
9
+
10
+
11
+ def options
12
+ map{ |k,v| "--#{k} #{v}"}*' '
13
+ end
14
+
15
+
16
+ def to_s
17
+ %Q(#{command} #{options} )
18
+ end
19
+
20
+
21
+ # 音声ファイル file の長さ(sec)を返す
22
+ def duration(file)
23
+ stdout, stderr, status = Open3.capture3("ffprobe #{file}")
24
+ stderr =~ /Duration: (\d{2}):(\d{2}):(\d{2}.\d{2})/m
25
+ hour, min, sec = [$1, $2, $3].map(&:to_f)
26
+ hour*60*60 + min*60 + sec
27
+ end
28
+
29
+ end
@@ -0,0 +1,37 @@
1
+ # rtmpdump.rb
2
+ require "player/player"
3
+
4
+
5
+ class RtmpDump < Player
6
+ def initialize
7
+ self['live'] = ''
8
+ self['quiet'] = ''
9
+ end
10
+
11
+
12
+ def command
13
+ 'rtmpdump'
14
+ end
15
+
16
+
17
+ def play
18
+ # puts "play: #{to_s} | mplayer -"
19
+ `#{to_s} | mplayer -`
20
+ end
21
+
22
+
23
+ def rec(file, sec, quiet = true)
24
+ self['stop'] = sec
25
+
26
+ if quiet
27
+ self['flv'] = file
28
+ puts "rec: #{to_s}"
29
+ `#{to_s}`
30
+ else
31
+ puts "rec: #{to_s}"
32
+ `#{to_s} | tee #{file} | mplayer -`
33
+ end
34
+ end
35
+
36
+
37
+ end
@@ -0,0 +1,8 @@
1
+ require "rbtune/version"
2
+ require "rbtune/radiru"
3
+ require "rbtune/radiko"
4
+ require "rbtune/radiko_premium"
5
+ require "rbtune/simul"
6
+ require "rbtune/listenradio"
7
+ require "rbtune/jcba"
8
+ require "rbtune/agqr"
@@ -0,0 +1,36 @@
1
+ # 文化放送 超A&G+ を受信する
2
+
3
+ require "rbtune/radio"
4
+ require "player/rtmpdump"
5
+ require "player/ffmpeg"
6
+ require "fileutils"
7
+
8
+ class Agqr < Radio
9
+ def initialize
10
+ @ext = 'flv'
11
+ @out_ext = 'm4a'
12
+ end
13
+
14
+ def fetch_stations
15
+ uri = "rtmp://fms-base1.mitene.ad.jp/agqr/aandg1"
16
+ [Station.new('AGQR', uri, name: '超A&G+', ascii_name: 'aandg1')]
17
+ end
18
+
19
+
20
+ def create_player(uri)
21
+ rtmpdump = RtmpDump.new
22
+ rtmpdump['rtmp'] = uri
23
+ rtmpdump
24
+ end
25
+
26
+
27
+ def convert(tmpfile, recfile)
28
+ ffmpeg = FFMpeg.new
29
+ ffmpeg['loglevel'] = 'quiet'
30
+ ffmpeg['i'] = %Q("#{tmpfile}")
31
+ ffmpeg['acodec'] = 'copy'
32
+ stdout, stderr, status = ffmpeg.rec recfile, nil
33
+ FileUtils.rm tmpfile if status.success?
34
+ end
35
+
36
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ =begin
3
+ JCBAサイマルラジオを受信する
4
+ =end
5
+ require "rbtune/listenradio"
6
+ require "rbtune/station"
7
+
8
+
9
+ class Jcba < ListenRadio
10
+
11
+ def stations_uri
12
+ 'https://www.jcbasimul.com'
13
+ end
14
+
15
+
16
+ def parse_stations(body)
17
+ radioboxes = body / 'div.areaList ul li'
18
+ stations = radioboxes.map do |station|
19
+ h3 = station.at 'h3'
20
+ rplayer = station.at 'div.rplayer'
21
+ text = station.at 'div.text'
22
+
23
+ id = rplayer['id']
24
+ uri = "http://musicbird-hls.leanstream.co/musicbird/#{id}.stream/playlist.m3u8"
25
+ name = h3.text.sub(%r( / .*), '')
26
+ desc = text.text
27
+ # puts "(#{h3.text}) (#{rplayer['id']}) (#{text.text})"
28
+ Station.new(id, uri, name: name, description: desc)
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ =begin
3
+ リスラジまたはJCBAサイマルラジオを受信する
4
+ =end
5
+
6
+
7
+ require "rbtune/radio"
8
+ require "player/ffmpeg"
9
+ require "json"
10
+
11
+
12
+ class ListenRadio < Radio
13
+
14
+ def initialize
15
+ super
16
+ @ext = 'mp4'
17
+ end
18
+
19
+ def create_player(uri)
20
+ player = FFMpeg.new
21
+ player['i'] = uri # input stream
22
+ player['acodec'] = 'copy' # acodecオプションはiオプションのあとに置かないとエラー
23
+ player
24
+ end
25
+
26
+
27
+ def stations_uri
28
+ 'http://listenradio.jp/service/channel.aspx'
29
+ end
30
+
31
+
32
+ def parse_stations(body)
33
+ json = JSON[body.body, symbolize_names: true]
34
+ stations = json[:Channel].map do |station|
35
+ name = station[:ChannelName]
36
+ id = station[:ChannelId]
37
+ desc = station[:ChannelDetail]
38
+ uri = station[:ChannelHls]
39
+ # puts "'#{name}' <#{uri}> #{desc}"
40
+ Station.new(id, uri, name: name, description: desc)
41
+ end
42
+ end
43
+
44
+
45
+ end
@@ -0,0 +1,143 @@
1
+ #radiko.rb
2
+ # coding: utf-8
3
+
4
+ require "mechanize"
5
+ require "rbtune/radio"
6
+ require "player/rtmpdump"
7
+ require 'swf_ruby'
8
+
9
+
10
+ class Radiko < Radio
11
+ attr_reader :authtoken
12
+
13
+
14
+ def playerurl
15
+ "http://radiko.jp/apps/js/flash/myplayer-release.swf"
16
+ end
17
+
18
+
19
+ def playerfile
20
+ "player.swf"
21
+ end
22
+
23
+
24
+ def keyfile
25
+ "authkey.png"
26
+ end
27
+
28
+
29
+ # get_auth2 の返り値により @area_id, @area_ja, @area_en が設定される
30
+ def open
31
+ unless File.exists? playerfile
32
+ $stderr.puts 'fetching player...'
33
+ fetch_file playerurl, playerfile
34
+ end
35
+ unless File.exists? keyfile
36
+ swfextract playerfile, 12, keyfile
37
+ end
38
+
39
+ @authtoken, partialkey = authenticate1 'https://radiko.jp/v2/api/auth1_fms'
40
+ @area_id, @area_ja, @area_en = authenticate2 'https://radiko.jp/v2/api/auth2_fms', authtoken, partialkey
41
+ puts "area: #{area_id} (#{area_ja}: #{area_en})"
42
+ end
43
+
44
+
45
+ def channel_to_uri
46
+ xml = agent.get "http://radiko.jp/v2/station/stream/#{@channel}.xml"
47
+ xml.at('//url/item').text
48
+ end
49
+
50
+
51
+ def create_player(uri)
52
+ rtmpdump = RtmpDump.new
53
+ rtmpdump['rtmp'] = uri
54
+ rtmpdump['swfVfy'] = playerurl
55
+ rtmpdump['conn'] = %Q(S:"" --conn S:"" --conn S:"" --conn S:#{authtoken})
56
+ rtmpdump
57
+ end
58
+
59
+
60
+ def authenticate1(url)
61
+ res = agent.post url, {}, {
62
+ 'pragma' => 'no-cache',
63
+ 'X-Radiko-App' => 'pc_ts',
64
+ 'X-Radiko-App-Version' => '4.0.0',
65
+ 'X-Radiko-User' => 'test-stream',
66
+ 'X-Radiko-Device' => 'pc',
67
+ }
68
+ s = res.body
69
+ s.sub! /\r\n\r\n.*/m, ''
70
+ arr = s.split(/\r\n/).map{|s| s.split('=')}.flatten
71
+ auth1 = Hash[*arr]
72
+ authtoken = auth1['X-Radiko-AuthToken'] || auth1['X-RADIKO-AUTHTOKEN']
73
+ offset = auth1['X-Radiko-KeyOffset'].to_i
74
+ length = auth1['X-Radiko-KeyLength'].to_i
75
+ partialkey = read_partialkey keyfile, offset, length
76
+ [authtoken, partialkey]
77
+ end
78
+
79
+
80
+ def read_partialkey(file, offset, length)
81
+ key = File.open(file, "rb") { |io| io.read(offset + length) }
82
+ Base64.encode64(key[offset,length]).chomp
83
+ end
84
+
85
+
86
+ # return: area info
87
+ def authenticate2(url, authtoken, partialkey)
88
+ # pp [url, authtoken, partialkey]
89
+ agent.verify_mode = OpenSSL::SSL::VERIFY_NONE
90
+ res = agent.post url, {}, {
91
+ 'pragma' => 'no-cache',
92
+ 'X-Radiko-App' => 'pc_ts',
93
+ 'X-Radiko-App-Version' => '4.0.0',
94
+ 'X-Radiko-User' => 'test-stream',
95
+ 'X-Radiko-Device' => 'pc',
96
+ 'X-Radiko-Authtoken' => authtoken,
97
+ 'X-Radiko-Partialkey' => partialkey,
98
+ }
99
+ body = res.body
100
+ body.force_encoding 'utf-8'
101
+ body.split(',').map(&:strip)
102
+ end
103
+
104
+
105
+ def stations_uri
106
+ "http://radiko.jp/v3/station/list/#{area_id}.xml"
107
+ end
108
+
109
+
110
+ def parse_stations(body)
111
+ stations = body.search '//station'
112
+ stations.map do |station|
113
+ id = station.at('id').text
114
+ name = station.at('name').text
115
+ uri = id
116
+ ascii_name = station.at('ascii_name').text
117
+ Station.new(id, uri, name: name, ascii_name: ascii_name)
118
+ end
119
+ end
120
+
121
+
122
+ def fetch_file(url, file=nil)
123
+ content = agent.get_file(url)
124
+ File.open(file, "wb") { |fout| fout.write content } if file
125
+ content
126
+ end
127
+
128
+
129
+ def swfextract(swffile, character_id, out_file)
130
+ swf = SwfRuby::SwfDumper.new
131
+ swf.open(swffile)
132
+ swf.tags.each_with_index do |tag, i|
133
+ tag = swf.tags[i]
134
+ if tag.character_id && tag.character_id == character_id
135
+ offset = swf.tags_addresses[i]
136
+ len = tag.length
137
+ File.open(out_file, 'wb') { |out| out.print tag.data[6..-1] }
138
+ break
139
+ end
140
+ end
141
+ end
142
+
143
+ end