rbtune 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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