rbtune 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,70 @@
1
+ #radiko.rb
2
+ # coding: utf-8
3
+
4
+ require "rbtune/radiko"
5
+
6
+ class RadikoPremium < Radiko
7
+
8
+ def self.set_authentication(kc, account)
9
+ begin
10
+ password = kc.query('radikoプレミアムのパスワードを入力してください', account)
11
+ radio = RadikoPremium.new
12
+ radio.login account, password
13
+ kc.set account, password
14
+ puts "Radikoプレミアムのアカウントが正しく登録されました"
15
+
16
+ rescue Radio::HTTPForbiddenException
17
+ $stderr.puts "アカウント情報が正しくありません"
18
+
19
+ rescue RuntimeError
20
+ $stderr.puts $!
21
+
22
+ ensure
23
+ radio && radio.close
24
+ end
25
+ end
26
+
27
+
28
+ def headers
29
+ {
30
+ 'pragma' => 'no-cache',
31
+ 'Cache-Control' => 'no-cache',
32
+ 'Expires' => 'Thu, 01 Jan 1970 00:00:00 GMT',
33
+ 'Accept-Language' => 'ja-jp',
34
+ 'Accept-Encoding' => 'gzip, deflate',
35
+ 'Accept' => 'application/json, text/javascript, */*; q=0.01',
36
+ 'X-Requested-With' => 'XMLHttpRequest'
37
+ }
38
+ end
39
+
40
+
41
+ def login(account, password)
42
+ res = agent.post 'https://radiko.jp/ap/member/login/login', {
43
+ mail: account, pass: password
44
+ }
45
+
46
+ begin
47
+ params = []
48
+ referer = nil
49
+ @logged_in = true
50
+ agent.get 'https://radiko.jp/ap/member/webapi/member/login/check', params, referer, headers
51
+ rescue Mechanize::ResponseCodeError => ex
52
+ raise HTTPForbiddenException if ex.message.include?('400 => Net::HTTPBadRequest')
53
+ end
54
+ end
55
+
56
+
57
+ def close
58
+ params = []
59
+ referer = nil
60
+ agent.get 'https://radiko.jp/ap/member/webapi/member/logout', params, referer, headers
61
+ @logged_in = false
62
+ end
63
+
64
+ def stations_uri
65
+ "http://radiko.jp/v3/station/region/full.xml"
66
+ end
67
+
68
+
69
+
70
+ end
@@ -0,0 +1,207 @@
1
+ # Radio仮想基底クラス
2
+ # Radiko, Radiru等で継承して使う
3
+ #
4
+ # radio.login account, password (RadikoPremium 等、必要な場合のみ)
5
+ # radio.open
6
+ # radio.tune channel
7
+ # radio.play または radio.record
8
+ # radio.close
9
+
10
+ require "date"
11
+ require "benchmark"
12
+ require "net/http"
13
+ require "rexml/document"
14
+
15
+ class Radio
16
+ attr_accessor :outdir
17
+ attr_reader :ext
18
+ attr_reader :area_id # for Radiko(Premium)
19
+ attr_reader :area_ja # for Radiko(Premium)
20
+ attr_reader :area_en # for Radiko(Premium)
21
+
22
+ def Radio.inherited(subclass)
23
+ @@bands ||= []
24
+ @@bands << subclass
25
+ end
26
+
27
+
28
+ def initialize
29
+ @outdir = '.'
30
+ @ext = 'm4a'
31
+ end
32
+
33
+
34
+ def self.db
35
+ @@db ||= Station::pstore_db
36
+ end
37
+
38
+
39
+ def self.stations
40
+ @stations ||= self.db.transaction(true) { self.db[name] }
41
+ end
42
+
43
+
44
+ def self.channels
45
+ @channels ||= self.stations.map {|st| [st.id, st.uri]}.to_h
46
+ end
47
+
48
+
49
+ def self.bands
50
+ @@bands
51
+ end
52
+
53
+
54
+ def self.search(channel)
55
+ radio_class, station = self.find(channel) || self.match(channel)
56
+ end
57
+
58
+
59
+ # Radioクラスのリストから、id と一致する放送局を探す
60
+ # return: [Radioクラス, 放送局] or nil
61
+ def self.find(id)
62
+ Radio.bands.each do |tuner|
63
+ if tuner.stations
64
+ station = tuner.stations.find {|station| station.id == id}
65
+ return [tuner, station] if station
66
+ end
67
+ end
68
+ nil
69
+ end
70
+
71
+
72
+ # Radioクラスのリストから、name を含む放送局を探す
73
+ # return: [Radioクラス, 放送局] or nil
74
+ def self.match(name)
75
+ matcher = /#{name}/i
76
+ Radio.bands.each do |tuner|
77
+ next unless tuner.stations
78
+ found = tuner.stations.find { |station| station.name.match?(matcher) || station.ascii_name.match?(matcher) }
79
+ return [tuner, found] if found
80
+ end
81
+ nil
82
+ end
83
+
84
+
85
+ def agent
86
+ @agent ||= Mechanize.new
87
+ end
88
+
89
+
90
+ def create_player(uri)
91
+ # rtmpdumpのコマンドラインを生成する(playから呼ばれる)
92
+ end
93
+
94
+
95
+ def login(account=nil, password=nil)
96
+ # ラジオサービスにログイン
97
+ end
98
+
99
+ def open
100
+ end
101
+
102
+ def close
103
+ end
104
+
105
+
106
+ def tune(channel)
107
+ @channel = channel
108
+ end
109
+
110
+
111
+ def channel_to_uri
112
+ self.class::channels[@channel] || @channel
113
+ end
114
+
115
+
116
+ def record(filename, sec, quiet: false, dt: DateTime.now)
117
+ begin
118
+ uri = channel_to_uri
119
+ raise 'not tuned yet.' unless uri
120
+
121
+ puts "record: #{uri}"
122
+ # $stderr.puts "play: #{sec}, #{filename}, #{quiet}"
123
+ player = create_player uri
124
+ remain_sec = sec
125
+ rtime = 0
126
+ minimum_sec = 60 # 残り録音時間がこれ以下ならば、録音が中断してもやり直さない
127
+ datetimes = []
128
+ begin
129
+ rtime += Benchmark.realtime do
130
+ dt = datetime dt
131
+ datetimes << dt
132
+ tmpfile = make_tmpfile @channel, dt
133
+ player.rec tmpfile, remain_sec, quiet
134
+ end
135
+ remain_sec -= rtime
136
+ dt = DateTime.now
137
+ end while remain_sec >= minimum_sec
138
+
139
+ rescue Interrupt, Errno::EPIPE
140
+ # do nothing
141
+
142
+ ensure
143
+ # 最後にまとめて convert する
144
+ datetimes.each do |dt|
145
+ tmpfile = make_tmpfile @channel, dt
146
+ recfile = make_recfile(filename, dt)
147
+ convert tmpfile, recfile
148
+ end
149
+ end
150
+ end
151
+
152
+
153
+ def play
154
+ uri = channel_to_uri
155
+ raise 'not tuned yet.' unless uri
156
+ puts "play: #{uri}"
157
+ create_player(uri).play
158
+ end
159
+
160
+
161
+ def convert(tmpfile, recfile)
162
+ FileUtils.mv tmpfile, recfile
163
+ end
164
+
165
+
166
+ def convert_ffmpeg(tmpfile, recfile)
167
+ ffmpeg = FFMpeg.new
168
+ ffmpeg['loglevel'] = 'quiet'
169
+ ffmpeg['i'] = %Q("#{tmpfile}")
170
+ ffmpeg['b:a'] = '70k'
171
+ stdout, stderr, status = ffmpeg.rec recfile, nil
172
+ FileUtils.rm tmpfile if status.success?
173
+ end
174
+
175
+
176
+ def datetime(dt)
177
+ dt.to_s[0..15].gsub(/:/, '=')
178
+ end
179
+
180
+
181
+ def make_tmpfile(channel, datetime)
182
+ File.join outdir, "#{channel}.#{datetime}.#{$$}.#{ext}"
183
+ end
184
+
185
+
186
+ def out_ext
187
+ @out_ext || ext
188
+ end
189
+
190
+ def make_recfile(title, datetime)
191
+ File.join outdir, "#{title}.#{datetime}.#{out_ext}"
192
+ end
193
+
194
+
195
+ def fetch_stations
196
+ body = agent.get stations_uri
197
+ stations = parse_stations body
198
+ end
199
+
200
+
201
+ class HTTPBadRequestException < StandardError; end
202
+ class HTTPForbiddenException < StandardError; end
203
+
204
+ end
205
+
206
+
207
+
@@ -0,0 +1,52 @@
1
+ # radiru.rb
2
+ # coding: utf-8
3
+
4
+ require "rbtune/radio"
5
+ require "player/ffmpeg"
6
+ require "fileutils"
7
+
8
+ class Radiru < Radio
9
+
10
+
11
+ def create_player(uri)
12
+ ffmpeg = FFMpeg.new
13
+ ffmpeg['i'] = uri # input stream
14
+ ffmpeg['acodec'] = 'copy' # acodecオプションはiオプションのあとに置かないとエラー
15
+ ffmpeg
16
+ end
17
+
18
+
19
+ def stations_uri
20
+ "https://www.nhk.or.jp/radio/config/config_web.xml"
21
+ end
22
+
23
+
24
+ def parse_stations(body)
25
+ stations = body.search '//data'
26
+ stationsjp = {
27
+ 'r1' => "ラジオ第1",
28
+ 'r2' => "ラジオ第2",
29
+ 'fm' => "FM",
30
+ }
31
+ stations = stations.map do |station|
32
+ areajp = station.at('areajp').text
33
+ area = station.at('area').text
34
+ # 地区ごとに第1, 第2, FM を登録する
35
+ r1, r2, fm = %w(r1 r2 fm).map do |v|
36
+ hls = "#{v}hls"
37
+ id = "nhk#{v}-#{area}".upcase
38
+ uri = station.at(hls).text
39
+ name = "NHK#{stationsjp[v]}-#{areajp}"
40
+ Station.new(id, uri, name: name, ascii_name: id)
41
+ end
42
+ end
43
+ stations.flatten!
44
+ # id: NHKR1, NHKR2, NHKFM を東京局に割り当てる 
45
+ stations.find_all { |station| station.id =~ /-TOKYO/}.reverse.map do |station|
46
+ id = station.id.sub(/-TOKYO/, '')
47
+ stations.unshift Station.new(id, station.uri, name: station.name, ascii_name: id)
48
+ end
49
+ stations
50
+ end
51
+
52
+ end
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rbtune/radio"
4
+ require "player/mplayer"
5
+ require "rbtune/station"
6
+
7
+ class Simul < Radio
8
+
9
+ def initialize
10
+ super
11
+ @ext = 'asf'
12
+ @out_ext = 'm4a'
13
+ end
14
+
15
+
16
+ def channel_to_uri
17
+ ch = super
18
+ if ch.end_with?('.asx')
19
+ parse_asx ch
20
+ else
21
+ ch
22
+ end
23
+ end
24
+
25
+
26
+ def parse_asx(uri)
27
+ asx = Net::HTTP::get URI::parse(uri)
28
+ asx.force_encoding "Shift_JIS"
29
+ asx.encode! 'utf-8'
30
+ asx.downcase!
31
+ if asx.gsub!(%r(</ask>), '</asx>')
32
+ $stderr.puts 'fix!! </ask> to <asx>'
33
+ end
34
+ doc = REXML::Document.new(asx)
35
+ ref = doc.get_elements('//entry/ref')[0]
36
+ ref.attribute('href').value
37
+ end
38
+
39
+
40
+ def create_player(uri)
41
+ mplayer = Mplayer.new uri
42
+ mplayer.merge! 'benchmark' => '', 'vo' => 'null'
43
+ mplayer
44
+ end
45
+
46
+
47
+ def convert(tmpfile, recfile)
48
+ convert_ffmpeg(tmpfile, recfile)
49
+ end
50
+
51
+
52
+ def link_to_station_id(link)
53
+ link =~ %r{(/asx/([\w-]+).asx|nkansai.tv/(\w+)/?\Z|(flower|redswave|fm-tanba|darazfm|AmamiFM|comiten|fm-shimabara|fm-kitaq))}
54
+ id = ($2 || $3 || $4).sub(/fm[-_]/, 'fm').sub(/[-_]fm/, 'fm')
55
+ end
56
+
57
+
58
+ def stations_uri
59
+ 'http://www.simulradio.info'
60
+ end
61
+
62
+
63
+ def parse_stations(body)
64
+ radioboxes = body / 'div.radiobox'
65
+ radioboxes.map do |station|
66
+ rows = station / 'tr'
67
+ cols = rows[1] / 'td'
68
+ ankers = station / 'a'
69
+
70
+ player = ankers.select {|a|
71
+ img = a.at 'img'
72
+ img && img['alt'] == '放送を聴く'
73
+ }
74
+
75
+ title = station.at('td > p > strong > a').text.strip
76
+ links0 = player.map! {|e| e['href']}
77
+ links = links0.select { |uri| uri =~ %r{\.asx\Z|nkansai.tv} }
78
+ if links.empty?
79
+ # $stderr.puts "#{title}: #{links0 * ', '}"
80
+ else
81
+ link = links[0]
82
+ id = link_to_station_id(link)
83
+ if id
84
+ Station.new(id, link, name: title, ascii_name: id)
85
+ else
86
+ $stderr.puts "!!! #{title}: #{link}"
87
+ end
88
+ end
89
+ end.compact
90
+ end
91
+
92
+
93
+ end
@@ -0,0 +1,91 @@
1
+ require "pstore"
2
+
3
+ class Station
4
+ attr_reader :name
5
+ attr_reader :id
6
+ attr_reader :ascii_name
7
+ attr_reader :description
8
+ attr_reader :uri
9
+
10
+ def self.pstore_db
11
+ @db ||= PStore.new(File.join(ENV['HOME'], '.rbtune.db'))
12
+ end
13
+
14
+
15
+ def self.list_stations
16
+ Radio.bands.each do |radio|
17
+ name = radio.to_s
18
+ stations = radio.stations
19
+ if stations.nil? || stations.empty?
20
+ $stderr.puts "warning: #{name} に放送局が登録されていません。"
21
+ $stderr.puts " rbtune --fetch-stations を実行して、放送局情報を取得してください。"
22
+ else
23
+ puts "* #{name}"
24
+ stations.each { |station| puts " #{station}" }
25
+ puts ''
26
+ end
27
+ end
28
+ end
29
+
30
+
31
+ def self.fetch_stations
32
+ db = Station::pstore_db
33
+ Radio.bands.each do |radio_class|
34
+ begin
35
+ $stderr.puts ">>> fetching #{radio_class} stations..."
36
+ radio = radio_class.new
37
+ radio.open
38
+ stations = radio.fetch_stations
39
+ if stations.empty?
40
+ $stderr.puts " warning: no station found."
41
+ else
42
+ db.transaction { db[radio_class.to_s] = stations }
43
+ $stderr.puts " #{stations.size} stations fetched."
44
+ end
45
+
46
+ rescue SocketError
47
+ $stderr.puts $!
48
+
49
+ rescue Net::HTTPNotFound
50
+ $stderr.puts $!
51
+
52
+ rescue REXML::ParseException
53
+ $stderr.puts $!
54
+
55
+ rescue
56
+ # 例外を握りつぶしてすべてのクラスの放送局情報の取得を試みる
57
+ $stderr.puts $!
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ def initialize(id, uri, name: '', ascii_name: '', description: '')
64
+ @id = id.to_s.upcase
65
+ @uri = uri
66
+ @name = normalize_name name
67
+ @ascii_name = ascii_name
68
+ @description = description
69
+ end
70
+
71
+
72
+ def normalize_name(name)
73
+ # NHKの局名はnormalizeしない (NHKFM-* の-が取れてしまうから)
74
+ return name if name.include?('NHK')
75
+ name.strip
76
+ .sub(/[ -]?FM[ -]?/i, 'FM')
77
+ .sub(/fm|FM|エフエム|えふえむ/, 'FM')
78
+ .tr(' !@', ' !@')
79
+ end
80
+
81
+ def to_s
82
+ "#{id}:\t\t#{name}\t\t#{ascii_name}"
83
+ end
84
+
85
+ alias :inspect :to_s
86
+
87
+ def ==(other)
88
+ id == other.id
89
+ end
90
+
91
+ end