irecorder 0.0.4-linux

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,244 @@
1
+ #
2
+ #
3
+ #
4
+ require 'rubygems'
5
+ require 'uri'
6
+ require 'net/http'
7
+ require 'open-uri'
8
+ require 'rss'
9
+ require 'nokogiri'
10
+ require 'shellwords'
11
+ require 'fileutils'
12
+
13
+ UrlRegexp = URI.regexp(['rtsp','http'])
14
+
15
+ #
16
+ #
17
+ class BBCNet
18
+ RtspRegexp = URI.regexp(['rtsp'])
19
+ MmsRegexp = URI.regexp(['mms'])
20
+ DirectStreamRegexp = URI.regexp(['mms', 'rtsp', 'rtmp', 'rtmpt'])
21
+
22
+
23
+ #------------------------------------------------------------------------
24
+ # get stream metadata
25
+ # episode url => pid => xml playlist => version pid (vpid aka. identifier)
26
+ # => xml stream metadata => wma
27
+ #
28
+ class MetaInfo
29
+ def self.get(url)
30
+ pid = BBCNet.extractPid(url)
31
+ # info = @@cachePid[pid]
32
+ # return info if info
33
+ self.new(pid)
34
+ end
35
+
36
+ attr_reader :pid
37
+ Keys = [ :duration, :vpid, :group, :media, :onAirDate, :channel, :title, :summary, :aacLow, :aacStd, :real, :wma, :streams ]
38
+ def initialize(pid)
39
+ @pid = pid
40
+ Keys.each do |k|
41
+ s = ('@' + k.to_s).to_sym
42
+ self.instance_variable_set(s, nil)
43
+ self.class.class_eval %Q{
44
+ def #{k}
45
+ #{s}
46
+ end
47
+ }
48
+ end
49
+
50
+ @streams = []
51
+ end
52
+
53
+
54
+ #
55
+ # read duration, vpid, group, media, onAirDate, channel
56
+ # from XmlPlaylist
57
+ def readXmlPlaylist
58
+ return self if @vpid
59
+
60
+ res = BBCNet.read("http://www.bbc.co.uk/iplayer/playlist/#{@pid}")
61
+ # res = IO.read("../tmp/iplayer-playlist-me.xml")
62
+
63
+ doc = Nokogiri::XML(res)
64
+ item = doc.at_css("item")
65
+ @media = item[:kind].gsub(/programme/i, '')
66
+ @duration = item[:duration].to_i
67
+ @vpid = item[:identifier]
68
+ @group = item[:group]
69
+ @onAirDate = BBCNet.getTime(item.at_css("broadcast").content.to_s)
70
+ @channel = item.at_css("service").content.to_s
71
+ @title = item.at_css("title").content.to_s
72
+ @summary = doc.at_css("summary").content.to_s
73
+ self
74
+ end
75
+
76
+
77
+ class StreamInfo
78
+ # example) 48, wma, time, audio, http://..
79
+ attr_accessor :bitrate, :encoding, :expires, :type, :indirectUrl
80
+ alias :kind :type
81
+
82
+ def url
83
+ @url ||= BBCNet.getDirectStreamUrl(@indirectUrl)
84
+ end
85
+ end
86
+
87
+ def readXmlStreamMeta
88
+ readXmlPlaylist unless @vpid
89
+
90
+ res = BBCNet.read("http://www.bbc.co.uk/mediaselector/4/mtis/stream/#{vpid}")
91
+ # res = IO.read("../tmp/iplayer-stream-meta-me.xml")
92
+
93
+ doc = Nokogiri::XML(res)
94
+ me = doc.css("media")
95
+ me.each do |m|
96
+ stmInf = StreamInfo.new
97
+ stmInf.encoding = m[:encoding] # wma
98
+ stmInf.bitrate = m[:bitrate].to_i # 48
99
+ expiresStr = m[:expires]
100
+ stmInf.expires = BBCNet.getTime(expiresStr) if expiresStr
101
+ stmInf.type = m[:kind] # audio
102
+
103
+ con = m.at_css("connection")
104
+ stmInf.indirectUrl = con[:href]
105
+ @streams <<= stmInf
106
+
107
+ case stmInf.encoding
108
+ when /\bwma\b/i
109
+ @wma = stmInf
110
+ when /\baac\b/i
111
+ if stmInf.bitrate < 64
112
+ @aacLow = stmInf
113
+ else
114
+ @aacStd = stmInf
115
+ end
116
+ when /\breal\b/i
117
+ @real = stmInf
118
+ end
119
+ end
120
+ self
121
+ end
122
+
123
+ alias :update :readXmlStreamMeta
124
+
125
+ end
126
+
127
+
128
+
129
+ def self.getTime(str)
130
+ tm = str.match(/(\d{4})-(\d\d)-(\d\d)\w(\d\d):(\d\d):(\d\d)/)
131
+ par = ((1..6).inject([]) do |a, n| a << tm[n].to_i end)
132
+ Time.gm( *par )
133
+ end
134
+
135
+
136
+ #------------------------------------------------------------------------
137
+ #
138
+ #
139
+
140
+ # convert epsode Url to console Url
141
+ # example
142
+ # from
143
+ # http://www.bbc.co.uk/iplayer/episode/b007jpkt/Miss_Marple_A_Caribbean_Mystery_Episode_1/
144
+ # to
145
+ # http://www.bbc.co.uk/iplayer/console/b007jpkt
146
+ def self.getPlayerConsoleUrl(url)
147
+ "http://www.bbc.co.uk/iplayer/console/" + extractPid(url)
148
+ end
149
+
150
+ # get PID from BBC episode Url
151
+ def self.extractPid(url)
152
+ case url
153
+ when %r!/(?:item|episode|programmes)/([a-z0-9]{8})!
154
+ $1
155
+ when %r!^[a-z0-9]{8}$!
156
+ url
157
+ when %r!\b(b[a-z0-9]{7}\b)!
158
+ $1
159
+ else
160
+ raise "No PID in Url '%s'" % url
161
+ end
162
+ end
163
+
164
+
165
+ # .asf/.ram => .wma/.ra
166
+ def self.getDirectStreamUrl(url)
167
+ unless url[DirectStreamRegexp] then
168
+ res = self.read(url)
169
+ newSrc= res[ DirectStreamRegexp ]
170
+ url = newSrc unless newSrc.nil?
171
+ end
172
+ url
173
+ end
174
+
175
+
176
+ def self.read(url)
177
+ header = { "User-Agent" => self.randomUserAgent }
178
+ if defined? @@proxy
179
+ header[:proxy] = @@proxy
180
+ end
181
+
182
+ uri = URI.parse(url)
183
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
184
+ http.get(uri.path, header)
185
+ end
186
+ res.body
187
+ end
188
+
189
+ def self.setProxy(url)
190
+ @@proxy = url
191
+ end
192
+
193
+ private
194
+ UserAgentList = [
195
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/<RAND>.8 (KHTML, like Gecko) Chrome/2.0.178.0 Safari/<RAND>.8',
196
+ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; YPC 3.2.0; SLCC1; .NET CLR 2.0.50<RAND>; .NET CLR 3.0.04<RAND>)',
197
+ 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; tr) AppleWebKit/<RAND>.4+ (KHTML, like Gecko) Version/4.0dp1 Safari/<RAND>.11.2',
198
+ 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50<RAND>; .NET CLR 3.5.30<RAND>; .NET CLR 3.0.30<RAND>; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)',
199
+ 'Mozilla/6.0 (Windows; U; Windows NT 7.0; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.9 (.NET CLR 3.5.30<RAND>)',
200
+ ]
201
+ def self.randomUserAgent
202
+ ua = UserAgentList[ rand UserAgentList.length ]
203
+ ua.gsub(/<RAND>/, "%03d" % rand(1000))
204
+ end
205
+
206
+ end
207
+
208
+ module AudioFile
209
+ # return seconds of audio file duration.
210
+ def self.getDuration(file)
211
+ case file[/\.\w+$/].downcase
212
+ when ".mp3"
213
+ cmd = "| exiftool -S -Duration %s" % file.shellescape
214
+ when ".wma"
215
+ cmd = "| exiftool -S -PlayDuration %s" % file.shellescape
216
+ end
217
+ msg = open(cmd) do |f| f.read end
218
+ a = msg.scan(/(?:(\d+):){0,2}(\d+)/)[0]
219
+ i = -1
220
+ a.reverse.inject(0) do |s, d|
221
+ i += 1
222
+ s + d.to_i * [ 1, 60, 3600 ][i]
223
+ end
224
+ end
225
+ end
226
+
227
+
228
+
229
+ if __FILE__ == $0 then
230
+ puts AudioFile::getDuration(ARGV.shift)
231
+ exit 0
232
+
233
+ pid = "b00mzvfq"
234
+ if ARGV.size > 0 then
235
+ pid = ARGV.shift
236
+ end
237
+ minfo = BBCNet::MetaInfo.new(pid)
238
+ minfo.readXmlStreamMeta
239
+ puts minfo.inspect
240
+
241
+ minfo.streams.each do |s|
242
+ puts "url : " + s.url
243
+ end
244
+ end
@@ -0,0 +1,325 @@
1
+ require 'fileutils'
2
+
3
+ require "bbcnet.rb"
4
+
5
+ #-------------------------------------------------------------------
6
+ #
7
+ #
8
+ #
9
+ class DownloadProcess < Qt::Process
10
+ slots 'taskFinished(int,QProcess::ExitStatus)'
11
+ attr_reader :sourceUrl, :fileName
12
+ attr_accessor :taskItem
13
+
14
+ #
15
+ DEBUG_DOWNLOAD = false
16
+
17
+ # @stage
18
+ DOWNLOAD = 0
19
+ CONVERT = 1
20
+ FINISHED = 2
21
+
22
+ # @status
23
+ INITIAL = 0
24
+ RUNNING = 1
25
+ ERROR = 2
26
+ DONE = 3
27
+ def statusMessage
28
+ case @status
29
+ when INITIAL, RUNNING, DONE
30
+ %w{ Downloading Converting Finished }[@stage]
31
+ when ERROR
32
+ "Error : " + %w{ Download Convert Finish }[@stage]
33
+ else
34
+ "???"
35
+ end
36
+ end
37
+
38
+ def status=(st)
39
+ @status = st
40
+ @taskItem.status = statusMessage if @taskItem
41
+ $log.misc { "status:#{status}" }
42
+ end
43
+
44
+ #--------------------------
45
+ # check status
46
+ def running?
47
+ @status == RUNNING
48
+ end
49
+
50
+ def error?
51
+ @status == ERROR
52
+ end
53
+
54
+ #--------------------------
55
+ # check stage
56
+ def finished?
57
+ @stage == FINISHED
58
+ end
59
+
60
+ def rawDownloaded?
61
+ @stage >= CONVERT
62
+ end
63
+
64
+ class Command
65
+ attr_accessor :app, :args, :msg
66
+ def initialize(app, args, msg)
67
+ @app = app
68
+ @args = args
69
+ @msg = msg
70
+ end
71
+ end
72
+
73
+ attr_reader :sourceUrl, :rawFileName
74
+
75
+ def initialize(parent, metaInfo, fName)
76
+ super(parent)
77
+ @metaInfo = metaInfo
78
+ @parent = parent
79
+ @taskItem = nil
80
+ @startTime = Time.new
81
+ @sourceUrl = @metaInfo.wma.url
82
+ @rawFileName = fName
83
+ @rawFilePath = File.join(IRecSettings.rawDownloadDir.path, fName)
84
+ mkdirSavePath(@rawFilePath)
85
+ $log.debug { "@rawFilePath : #{@rawFilePath }" }
86
+
87
+ @stage = DOWNLOAD
88
+ @status = INITIAL
89
+
90
+ connect(self, SIGNAL('finished(int,QProcess::ExitStatus)'), self, SLOT('taskFinished(int,QProcess::ExitStatus)') )
91
+ end
92
+
93
+
94
+ def beginTask
95
+ # 1st stage : download
96
+ if File.exist?(@rawFilePath) then
97
+ @stage = CONVERT
98
+ @outFileName = @rawFileName.gsub(/\.\w+$/i, '.mp3')
99
+ @outFilePath = File.join(IRecSettings.downloadDir.path, @outFileName)
100
+ $log.debug { "@rawFilePath duration:" + AudioFile.getDuration(@rawFilePath).to_s }
101
+ $log.debug { "@outFilePath duration:" + AudioFile.getDuration(@outFilePath).to_s }
102
+ $log.debug { "metainf duration:" + @metaInfo.duration.to_s }
103
+ if File.exist?(@outFilePath) then
104
+ taskFinished(1,0)
105
+ else
106
+ $log.debug { "begin convert" }
107
+ beginConvert
108
+ return
109
+ end
110
+ else
111
+ beginDownload
112
+ end
113
+
114
+ # beginDownload
115
+ end
116
+
117
+
118
+ def retryTask
119
+ $log.debug { "retry! in main." }
120
+ if error? then
121
+ # retry
122
+ case @stage
123
+ when DOWNLOAD
124
+ @startTime = Time.new
125
+ beginDownload
126
+ when CONVERT
127
+ @startTime = Time.new
128
+ beginConvert
129
+ end
130
+ else
131
+ $log.warn { "cannot retry the successfully finished or running process." }
132
+ end
133
+ end
134
+
135
+ def cancelTask
136
+ if running? then
137
+ self.terminate
138
+ end
139
+ end
140
+
141
+ def updateLapse
142
+ taskItem.updateTime(lapse)
143
+ end
144
+
145
+ def lapse
146
+ Time.now - @startTime
147
+ end
148
+
149
+ # slot :
150
+ def taskFinished(exitCode, exitStatus)
151
+ checkReadOutput
152
+ if (exitCode.to_i.nonzero? || exitStatus.to_i.nonzero?) && checkErroredStatus then
153
+ self.status = ERROR
154
+ $log.error { [ makeErrorMsg, "exitCode=#{exitCode}, exitStatus=#{exitStatus}" ] }
155
+ else
156
+ $log.info {
157
+ [ "Successed to download a File '%#2$s'",
158
+ "Successed to convert a File '%#2$s'", ][@stage] %
159
+ [ @sourceUrl, @rawFilePath ]
160
+ }
161
+ nextTask
162
+ end
163
+ end
164
+
165
+ def updateView
166
+ if running? then
167
+ # update Lapse time
168
+ updateLapse
169
+
170
+ # dump IO message buffer
171
+ checkReadOutput
172
+ end
173
+ end
174
+
175
+
176
+ protected
177
+ # increment stage
178
+ def nextTask
179
+ @stage += 1
180
+ case @stage
181
+ when DOWNLOAD
182
+ beginDownload
183
+ when CONVERT
184
+ beginConvert
185
+ else
186
+ removeRawFile
187
+ allTaskFinished
188
+ end
189
+ end
190
+
191
+ def beginDownload
192
+ $log.info { " DownloadProcess : beginDownload." }
193
+ @stage = DOWNLOAD
194
+ @downNG = true
195
+ self.status = RUNNING
196
+ @currentCommand = makeMPlayerDownloadCmd
197
+
198
+ $log.info { @currentCommand.msg }
199
+ start(@currentCommand.app, @currentCommand.args)
200
+ end
201
+
202
+ def makeMPlayerDownloadCmd
203
+ # make MPlayer Downlaod comand
204
+ cmdMsg = "mplayer -noframedrop -dumpfile %s -dumpstream %s" %
205
+ [@rawFilePath.shellescape, @sourceUrl.shellescape]
206
+ cmdApp = "mplayer"
207
+ cmdArgs = ['-noframedrop', '-dumpfile', @rawFilePath, '-dumpstream', @sourceUrl]
208
+
209
+ # debug code.
210
+ if DEBUG_DOWNLOAD then
211
+ if rand > 0.4 then
212
+ cmdApp = "test/sleepjob.rb"
213
+ cmdArgs = %w{ touch a/b/ }
214
+ else
215
+ cmdApp = "test/sleepjob.rb"
216
+ cmdArgs = %w{ touch } << @rawFilePath.shellescape
217
+ end
218
+ end
219
+
220
+ Command.new( cmdApp, cmdArgs, cmdMsg )
221
+ end
222
+
223
+ def beginConvert
224
+ @stage = CONVERT
225
+ self.status = RUNNING
226
+ @outFileName = @rawFileName.gsub(/\.\w+$/i, '.mp3')
227
+ @outFilePath = File.join(IRecSettings.downloadDir.path, @outFileName)
228
+ mkdirSavePath(@outFilePath)
229
+
230
+ cmdMsg = "nice -n 19 ffmpeg -i %s -f mp3 %s" %
231
+ [ @rawFilePath.shellescape, @outFilePath.shellescape]
232
+ cmdApp = "nice"
233
+ cmdArgs = [ '-n', '19', 'ffmpeg', '-i', @rawFilePath, '-f', 'mp3', @outFilePath ]
234
+
235
+
236
+ # debug code.
237
+ if DEBUG_DOWNLOAD then
238
+ if rand > 0.4 then
239
+ cmdApp = "test/sleepjob.rb"
240
+ cmdArgs = %w{ touch a/b/ }
241
+ else
242
+ cmdApp = "test/sleepjob.rb"
243
+ cmdArgs = %w{ cp -f } + [ @rawFilePath.shellescape, @outFilePath.shellescape ]
244
+ end
245
+ end
246
+
247
+ @currentCommand = Command.new( cmdApp, cmdArgs, cmdMsg )
248
+ $log.info { @currentCommand.msg }
249
+ start(@currentCommand.app, @currentCommand.args)
250
+ end
251
+
252
+
253
+ def removeRawFile
254
+ unless IRecSettings.leaveRawFile then
255
+ begin
256
+ File.delete(@rawFilePath)
257
+ rescue => e
258
+ $log.error { e }
259
+ end
260
+ end
261
+ end
262
+
263
+ def checkOutput(msg)
264
+ msgSum = msg.join(' ')
265
+ @downNG &&= false if msgSum =~ /Everything done/i
266
+ end
267
+
268
+ # check and read output
269
+ def checkReadOutput
270
+ msg = readAllStandardOutput.data .reject do |l| l.empty? end
271
+ checkOutput(msg)
272
+ $log.info { msg }
273
+ end
274
+
275
+ # return error or not
276
+ def checkErroredStatus
277
+ case @stage
278
+ when DOWNLOAD
279
+ return @downNG if @downNG
280
+ begin
281
+ $log.debug { "check duration for download." }
282
+ AudioFile.getDuration(@rawFilePath) < @metaInfo.duration - 5
283
+ rescue => e
284
+ $log.info{ e }
285
+ true
286
+ end
287
+ when CONVERT
288
+ begin
289
+ $log.debug { "check duration for convert." }
290
+ AudioFile.getDuration(@outFilePath) < @metaInfo.duration - 40
291
+ rescue => e
292
+ $log.info{ e }
293
+ true
294
+ end
295
+ else
296
+ true
297
+ end
298
+ end
299
+
300
+
301
+
302
+
303
+ protected
304
+ def allTaskFinished
305
+ @stage = FINISHED
306
+ self.status = DONE
307
+ end
308
+
309
+ def makeErrorMsg
310
+ [ "Failed to download a File '%#2$s'",
311
+ "Failed to convert a File '%#2$s'", ][@stage] %
312
+ [ @sourceUrl, @rawFilePath ]
313
+ end
314
+
315
+
316
+ def mkdirSavePath(fName)
317
+ dir = File.dirname(fName)
318
+ unless File.exist? dir
319
+ $log.info{ "mkdir : " + dir }
320
+ FileUtils.mkdir_p(dir)
321
+ end
322
+ end
323
+ end
324
+
325
+