irecorder 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'ftools'
4
+
5
+
6
+ desc "Install Application Menu"
7
+ task :install_menu do
8
+ menuDir = %x{ kde4-config --install xdgdata-apps }.strip
9
+ menuEntryFile = File.join(menuDir, 'irecorder.desktop')
10
+ open(menuEntryFile,'w') do |f|
11
+ f.write(<<-EOF
12
+ [Desktop Entry]
13
+ Name=iRecorder
14
+ Comment=BBC iPlayer like audio recorder with KDE GUI.
15
+ Exec=irecorder.rb %f
16
+ Icon=irecorder
17
+ Terminal=false
18
+ Type=Application
19
+ Categories=Qt;KDE;AudioVideo;Radio;News;Music;Player
20
+ MimeType=application/x-gem;
21
+ EOF
22
+ )
23
+ %x{ update-menus }
24
+ end
25
+ end
26
+
27
+ task :default => [ :install_menu ]
@@ -0,0 +1,271 @@
1
+ #
2
+ #
3
+ #
4
+ require 'rubygems'
5
+ require 'uri'
6
+ require 'net/http'
7
+ require 'open-uri'
8
+ require 'nokogiri'
9
+ require 'shellwords'
10
+ require 'fileutils'
11
+ require 'tmpdir'
12
+ require 'singleton'
13
+ require 'Qt'
14
+
15
+ # my libs
16
+ require "cache"
17
+ require "logwin"
18
+
19
+ UrlRegexp = URI.regexp(['rtsp','http'])
20
+
21
+ #
22
+ #
23
+ class BBCNet
24
+ RtspRegexp = URI.regexp(['rtsp'])
25
+ MmsRegexp = URI.regexp(['mms'])
26
+ DirectStreamRegexp = URI.regexp(['mms', 'rtsp', 'rtmp', 'rtmpt'])
27
+
28
+ class CacheMetaInfoDevice < CasheDevice::CacheDeviceBase
29
+ def initialize(cacheDuration = 40*60, cacheMax=200)
30
+ super(cacheDuration, cacheMax)
31
+ end
32
+
33
+ # @return : [ data, key ]
34
+ # key : key to restore data.
35
+ def directRead(pid)
36
+ data = BBCNet::MetaInfo.new(pid).update
37
+ [ data, data ]
38
+ end
39
+
40
+ def self.read(url)
41
+ pid = BBCNet.extractPid(url)
42
+ self.instance.read(pid)
43
+ end
44
+ end
45
+
46
+ #------------------------------------------------------------------------
47
+ # get stream metadata
48
+ # episode url => pid => xml playlist => version pid (vpid aka. identifier)
49
+ # => xml stream metadata => wma
50
+ #
51
+ class MetaInfo
52
+ def self.get(url)
53
+ pid = BBCNet.extractPid(url)
54
+ self.new(pid)
55
+ end
56
+
57
+ attr_reader :pid
58
+ Keys = [ :duration, :vpid, :group, :media, :onAirDate, :channel, :title, :summary, :aacLow, :aacStd, :real, :wma, :streams ]
59
+ def initialize(pid)
60
+ @pid = pid
61
+ Keys.each do |k|
62
+ s = ('@' + k.to_s).to_sym
63
+ self.instance_variable_set(s, nil)
64
+ self.class.class_eval %Q{
65
+ def #{k}
66
+ #{s}
67
+ end
68
+ }
69
+ end
70
+
71
+ @streams = []
72
+ end
73
+
74
+
75
+ #
76
+ # read duration, vpid, group, media, onAirDate, channel
77
+ # from XmlPlaylist
78
+ def readXmlPlaylist
79
+ return self if @vpid
80
+
81
+ res = BBCNet.read("http://www.bbc.co.uk/iplayer/playlist/#{@pid}")
82
+ # res = IO.read("../tmp/iplayer-playlist-me.xml")
83
+
84
+ doc = Nokogiri::XML(res)
85
+ item = doc.at_css("noItems")
86
+ raise "No Playlist " + item[:reason] if item
87
+
88
+ item = doc.at_css("item")
89
+ @media = item[:kind].gsub(/programme/i, '')
90
+ @duration = item[:duration].to_i
91
+ @vpid = item[:identifier]
92
+ @group = item[:group]
93
+ @onAirDate = BBCNet.getTime(item.at_css("broadcast").content.to_s)
94
+ @channel = item.at_css("service").content.to_s
95
+ @title = item.at_css("title").content.to_s
96
+ @summary = doc.at_css("summary").content.to_s
97
+ self
98
+ end
99
+
100
+
101
+ class StreamInfo
102
+ # example) 48, wma, time, audio, http://..
103
+ attr_accessor :bitrate, :encoding, :expires, :type, :indirectUrl
104
+ alias :kind :type
105
+
106
+ def url
107
+ @url ||= BBCNet.getDirectStreamUrl(@indirectUrl)
108
+ end
109
+ end
110
+
111
+ def readXmlStreamMeta
112
+ readXmlPlaylist unless @vpid
113
+
114
+ res = BBCNet.read("http://www.bbc.co.uk/mediaselector/4/mtis/stream/#{vpid}")
115
+ # res = IO.read("../tmp/iplayer-stream-meta-me.xml")
116
+
117
+ doc = Nokogiri::XML(res)
118
+ me = doc.css("media")
119
+ me.each do |m|
120
+ stmInf = StreamInfo.new
121
+ stmInf.encoding = m[:encoding] # wma
122
+ stmInf.bitrate = m[:bitrate].to_i # 48
123
+ expiresStr = m[:expires]
124
+ stmInf.expires = BBCNet.getTime(expiresStr) if expiresStr
125
+ stmInf.type = m[:kind] # audio
126
+
127
+ con = m.at_css("connection")
128
+ stmInf.indirectUrl = con[:href]
129
+ @streams <<= stmInf
130
+
131
+ case stmInf.encoding
132
+ when /\bwma\b/i
133
+ @wma = stmInf
134
+ when /\baac\b/i
135
+ if stmInf.bitrate < 64
136
+ @aacLow = stmInf
137
+ else
138
+ @aacStd = stmInf
139
+ end
140
+ when /\breal\b/i
141
+ @real = stmInf
142
+ end
143
+ end
144
+ self
145
+ end
146
+
147
+ alias :update :readXmlStreamMeta
148
+
149
+ end
150
+
151
+
152
+
153
+ def self.getTime(str)
154
+ tm = str.match(/(\d{4})-(\d\d)-(\d\d)\w(\d\d):(\d\d):(\d\d)/)
155
+ par = ((1..6).inject([]) do |a, n| a << tm[n].to_i end)
156
+ Time.gm( *par )
157
+ end
158
+
159
+
160
+ #------------------------------------------------------------------------
161
+ #
162
+ #
163
+
164
+ # convert epsode Url to console Url
165
+ def self.getPlayerConsoleUrl(url)
166
+ "http://www.bbc.co.uk/iplayer/console/" + extractPid(url)
167
+ end
168
+
169
+ # get PID from BBC episode Url
170
+ def self.extractPid(url)
171
+ case url
172
+ when %r!/(?:item|episode|programmes)/([a-z0-9]{8})!
173
+ $1
174
+ when %r!^[a-z0-9]{8}$!
175
+ url
176
+ when %r!\b(b[a-z0-9]{7}\b)!
177
+ $1
178
+ else
179
+ raise "No PID in Url '%s'" % url
180
+ end
181
+ end
182
+
183
+
184
+ # .asf/.ram => .wma/.ra
185
+ def self.getDirectStreamUrl(url)
186
+ old = ''
187
+ while url != old and not url[DirectStreamRegexp] do
188
+ old = url
189
+ res = BBCNet.read(url)
190
+ url = res[ DirectStreamRegexp ] || res[ UrlRegexp ] || old
191
+ $log.debug { "new url:#{url}, old url:#{old}" }
192
+ $log.debug { "no url in response '#{res}'" } if url[ UrlRegexp ]
193
+ end
194
+ url
195
+ end
196
+
197
+
198
+ def self.read(url)
199
+ header = { "User-Agent" => self.randomUserAgent }
200
+ if defined? @@proxy
201
+ header[:proxy] = @@proxy
202
+ end
203
+
204
+ uri = URI.parse(url)
205
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
206
+ http.get(uri.request_uri, header)
207
+ end
208
+ res.body
209
+ end
210
+
211
+
212
+ def self.setProxy(url)
213
+ @@proxy = url
214
+ end
215
+
216
+ private
217
+ UserAgentList = [
218
+ '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',
219
+ '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>)',
220
+ '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',
221
+ '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)',
222
+ '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>)',
223
+ ]
224
+ def self.randomUserAgent
225
+ ua = UserAgentList[ rand UserAgentList.length ]
226
+ ua.gsub(/<RAND>/, "%03d" % rand(1000))
227
+ end
228
+
229
+ end
230
+
231
+ module AudioFile
232
+ # return seconds of audio file duration.
233
+ def self.getDuration(file)
234
+ return 0 unless File.exist?(file)
235
+
236
+ case file[/\.\w+$/].downcase
237
+ when ".mp3"
238
+ cmd = "| exiftool -S -Duration %s" % file.shellescape
239
+ when ".wma"
240
+ cmd = "| exiftool -S -PlayDuration %s" % file.shellescape
241
+ end
242
+ msg = open(cmd) do |f| f.read end
243
+ a = msg.scan(/(?:(\d+):){0,2}(\d+)/)[0]
244
+ return 0 unless a
245
+ i = -1
246
+ a.reverse.inject(0) do |s, d|
247
+ i += 1
248
+ s + d.to_i * [ 1, 60, 3600 ][i]
249
+ end
250
+ end
251
+ end
252
+
253
+
254
+
255
+ if __FILE__ == $0 then
256
+ # puts AudioFile::getDuration(ARGV.shift)
257
+ # exit 0
258
+
259
+ $log = MyLogger.new(STDOUT)
260
+ pid = "b00mzvfq"
261
+ if ARGV.size > 0 then
262
+ pid = ARGV.shift
263
+ end
264
+ minfo = BBCNet::MetaInfo.new(pid)
265
+ minfo.readXmlStreamMeta
266
+ puts minfo.inspect
267
+
268
+ minfo.streams.each do |s|
269
+ puts "url : " + s.url
270
+ end
271
+ end
@@ -0,0 +1,121 @@
1
+ #
2
+ #
3
+ #
4
+ module CasheDevice
5
+ class CacheDeviceBase
6
+ include Singleton
7
+
8
+ class CachedData
9
+ attr_accessor :expireTime, :url, :key
10
+ end
11
+
12
+ attr_accessor :cacheDuration, :cacheMax
13
+ def initialize(cacheDuration = 26*60, cacheMax=10)
14
+ @cacheDuration = cacheDuration
15
+ @cache = Hash.new
16
+ @cacheLRU = [] # Least Recently Used
17
+ @cacheMax = cacheMax
18
+ end
19
+
20
+ # @return : data, key
21
+ # key : key to restore data.
22
+ def directRead(url)
23
+ raise "Implement directRead method."
24
+ end
25
+
26
+ # @return : data
27
+ # restore data from key.
28
+ def restoreCache(key)
29
+ key
30
+ end
31
+
32
+ def read(url)
33
+ startTime = Time.now
34
+ cached = @cache[url]
35
+ if cached and cached.expireTime > startTime then
36
+ @cacheLRU.delete(cached)
37
+ @cacheLRU.push(cached)
38
+ data = restoreCache(cached.key)
39
+ $log.debug { "cached %s: Time %f sec" %
40
+ [self.class.name, (Time.now - startTime).to_f] }
41
+ return data
42
+ end
43
+ if @cacheLRU.size >= @cacheMax then
44
+ oldest = @cacheLRU.shift
45
+ @cache.delete(oldest)
46
+ end
47
+ cached = CachedData.new
48
+ cached.url = url
49
+ cached.expireTime = startTime + @cacheDuration
50
+ data, cached.key = directRead(url)
51
+ @cache[url] = cached
52
+ @cacheLRU.push(cached)
53
+ $log.debug {"direct read %s: Time %f sec" %
54
+ [self.class.name, (Time.now - startTime).to_f] }
55
+ data
56
+ end
57
+
58
+
59
+ def self.read(url)
60
+ self.instance.read(url)
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ #
67
+ # practical implementations.
68
+ #
69
+ class CacheRssDevice < CasheDevice::CacheDeviceBase
70
+ def initialize(cacheDuration = 12*60, cacheMax=6)
71
+ super(cacheDuration, cacheMax)
72
+ end
73
+
74
+ # @return : [ data, key ]
75
+ # key : key to restore data.
76
+ def directRead(url)
77
+ data = Nokogiri::XML(CacheHttpDiskDevice.read(url))
78
+ [ data, data ]
79
+ end
80
+ end
81
+
82
+
83
+ class CacheHttpDiskDevice < CasheDevice::CacheDeviceBase
84
+ def initialize(cacheDuration = 12*60, cacheMax=50)
85
+ super(cacheDuration, cacheMax)
86
+ @tmpdir = Dir.tmpdir + '/bbc_cache'
87
+ FileUtils.mkdir_p(@tmpdir)
88
+ end
89
+
90
+ # @return : data
91
+ # restore data from key.
92
+ def restoreCache(key)
93
+ IO.read(key)
94
+ end
95
+
96
+ # @return : [ data, key ]
97
+ # key : key to restore data.
98
+ def directRead(url)
99
+ $log.misc { "directRead(): " + self.class.name }
100
+ tmpfname = tempFileName(url)
101
+
102
+ if File.exist?(tmpfname) then
103
+ $log.misc { "File ctime : " + File.ctime(tmpfname).to_s}
104
+ $log.misc { "expire time : " + (File.ctime(tmpfname) + @cacheDuration).to_s }
105
+ $log.misc { "Now Time : " + Time.now.to_s }
106
+ end
107
+
108
+ if File.exist?(tmpfname) and
109
+ File.ctime(tmpfname) + @cacheDuration > Time.now then
110
+ data = IO.read(tmpfname)
111
+ else
112
+ data = BBCNet.read(url)
113
+ open(tmpfname, "w") do |f| f.write(data) end
114
+ end
115
+ [ data, tmpfname ]
116
+ end
117
+
118
+ def tempFileName(url)
119
+ File.join(@tmpdir, url.scan(%r{(?:iplayer/)[\w\/]+$}).first.gsub!(/iplayer\//,'').gsub!(%r|/|, '_'))
120
+ end
121
+ end
@@ -0,0 +1,441 @@
1
+ require 'fileutils'
2
+
3
+ require "bbcnet.rb"
4
+
5
+ #-------------------------------------------------------------------
6
+ #
7
+ #
8
+ class OkCancelDialog < KDE::Dialog
9
+ def initialize(parent)
10
+ super(parent)
11
+ setButtons( KDE::Dialog::Ok | KDE::Dialog::Cancel )
12
+ @textEdit = Qt::Label.new do |w|
13
+ w.wordWrap= true
14
+ end
15
+ setMainWidget(@textEdit)
16
+ end
17
+
18
+ attr_reader :textEdit
19
+
20
+ def self.ask(parent, text, title = text)
21
+ @@dialog ||= self.new(parent)
22
+ @@dialog.textEdit.text = text
23
+ @@dialog.caption = title
24
+ @@dialog.exec
25
+ end
26
+ end
27
+
28
+
29
+ #-------------------------------------------------------------------
30
+ #
31
+ #
32
+ #
33
+ class DownloadProcess < Qt::Process
34
+ attr_reader :sourceUrl, :fileName
35
+ attr_accessor :taskItem
36
+
37
+ #
38
+ DEBUG_DOWNLOAD = false
39
+
40
+ # @stage
41
+ DOWNLOAD = 0
42
+ CONVERT = 1
43
+ FINISHED = 2
44
+
45
+ # @status
46
+ INITIAL = 0
47
+ RUNNING = 1
48
+ ERROR = 2
49
+ DONE = 3
50
+ def statusMessage
51
+ case @status
52
+ when INITIAL, RUNNING, DONE
53
+ %w{ Downloading Converting Finished }[@stage]
54
+ when ERROR
55
+ "Error : " + %w{ Download Convert Finish }[@stage]
56
+ else
57
+ "???"
58
+ end
59
+ end
60
+
61
+ def status=(st)
62
+ @status = st
63
+ @taskItem.status = statusMessage if @taskItem
64
+ $log.misc { "status:#{status}" }
65
+ end
66
+
67
+ #--------------------------
68
+ # check status
69
+ def running?
70
+ @status == RUNNING
71
+ end
72
+
73
+ def error?
74
+ @status == ERROR
75
+ end
76
+
77
+ #--------------------------
78
+ # check stage
79
+ def finished?
80
+ @stage == FINISHED
81
+ end
82
+
83
+ def rawDownloaded?
84
+ @stage >= CONVERT
85
+ end
86
+
87
+ class Command
88
+ attr_accessor :app, :args, :msg
89
+ def initialize(app, args, msg)
90
+ @app = app
91
+ @args = args
92
+ @msg = msg
93
+ end
94
+ end
95
+
96
+ attr_reader :sourceUrl, :rawFileName
97
+ attr_reader :rawFilePath, :outFilePath
98
+
99
+ def initialize(parent, metaInfo, fName)
100
+ super(parent)
101
+ @metaInfo = metaInfo
102
+ @parent = parent
103
+ @taskItem = nil
104
+ @startTime = Time.new
105
+ @sourceUrl = @metaInfo.wma.url
106
+ @rawFileName = fName
107
+ @rawFilePath = File.join(IRecSettings.rawDownloadDir, fName)
108
+ mkdirSavePath(@rawFilePath)
109
+ @outFileName = @rawFileName.gsub(/\.\w+$/i, '.mp3')
110
+ @outFilePath = File.join(IRecSettings.downloadDir, @outFileName)
111
+ mkdirSavePath(@outFilePath)
112
+ $log.debug { "@rawFilePath : #{@rawFilePath }" }
113
+ $log.debug { "@outFilePath : #{@outFilePath}" }
114
+
115
+ @stage = DOWNLOAD
116
+ @status = INITIAL
117
+
118
+ connect(self, SIGNAL('finished(int,QProcess::ExitStatus)'), self, SLOT('taskFinished(int,QProcess::ExitStatus)') )
119
+ end
120
+
121
+ def decideFinish?
122
+ if File.exist?(@outFilePath) then
123
+ # check outFile validity.
124
+ return ! outFileError?
125
+ end
126
+ return false
127
+ end
128
+
129
+ def decideConvert?
130
+ if File.exist?(@rawFilePath) then
131
+ # check rawFile validity.
132
+ return ! rawFileError?
133
+ end
134
+ return false
135
+ end
136
+
137
+ def decideStartTask
138
+ return FINISHED if decideFinish?
139
+ return CONVERT if decideConvert?
140
+ return DOWNLOAD
141
+ end
142
+
143
+ def beginTask
144
+ startTask = decideStartTask
145
+
146
+ # ask whether proceed or commence from start.
147
+ case startTask
148
+ when FINISHED
149
+ ret = OkCancelDialog.ask(nil, \
150
+ i18n('File %s is already exist. Download it anyway ?') % @outFileName)
151
+ if ret == Qt::Dialog::Accepted then
152
+ startTask = DOWNLOAD
153
+ end
154
+ when CONVERT
155
+ ret = OkCancelDialog.ask(nil, \
156
+ i18n('Raw file %s is already exist. Download it anyway ?') % @rawFileName)
157
+ if ret == Qt::Dialog::Accepted then
158
+ startTask = DOWNLOAD
159
+ end
160
+ end
161
+
162
+ # initialize task
163
+ case startTask
164
+ when DOWNLOAD
165
+ beginDownload
166
+ when CONVERT
167
+ beginConvert
168
+ when FINISHED
169
+ allTaskFinished
170
+ end
171
+ end
172
+
173
+
174
+ def retryTask
175
+ $log.debug { "retry." }
176
+ if error? then
177
+ # retry
178
+ case @stage
179
+ when DOWNLOAD
180
+ @startTime = Time.new
181
+ beginDownload
182
+ when CONVERT
183
+ @startTime = Time.new
184
+ beginConvert
185
+ end
186
+ else
187
+ $log.warn { "cannot retry the successfully finished or running process." }
188
+ end
189
+ end
190
+
191
+ def retryDownload
192
+ $log.debug { "retry from download." }
193
+ if error? then
194
+ # retry
195
+ @startTime = Time.new
196
+ beginDownload
197
+ else
198
+ $log.warn { "cannot retry the successfully finished or running process." }
199
+ end
200
+ end
201
+
202
+ def cancelTask
203
+ if running? then
204
+ self.terminate
205
+ self.status = ERROR
206
+ errMsg = "Stopped " + %w{ Download Convert ? }[@stage]
207
+ $log.error { [ errMsg ] }
208
+ passiveMessage(errMsg)
209
+ end
210
+ end
211
+
212
+ def removeData
213
+ cancelTask
214
+ begin
215
+ File.delete(@rawFilePath)
216
+ File.delete(@outFilePath)
217
+ rescue => e
218
+ $log.info { e }
219
+ end
220
+ end
221
+
222
+ def updateLapse
223
+ taskItem.updateTime(lapse)
224
+ end
225
+
226
+ def lapse
227
+ Time.now - @startTime
228
+ end
229
+
230
+ def errorStop(exitCode, exitStatus)
231
+ self.status = ERROR
232
+ errMsg = makeErrorMsg
233
+ $log.error { [ errMsg, "exitCode=#{exitCode}, exitStatus=#{exitStatus}" ] }
234
+ passiveMessage(errMsg)
235
+ end
236
+
237
+ slots 'taskFinished(int,QProcess::ExitStatus)'
238
+ def taskFinished(exitCode, exitStatus)
239
+ checkReadOutput
240
+ if error? || ((exitCode.to_i.nonzero? || exitStatus.to_i.nonzero?) && checkErroredStatus) then
241
+ errorStop(exitCode, exitStatus)
242
+ else
243
+ $log.info {
244
+ [ "Successed to download a File '%#2$s'",
245
+ "Successed to convert a File '%#2$s'", "?" ][@stage] %
246
+ [ @sourceUrl, @rawFilePath ]
247
+ }
248
+ if @stage == CONVERT then
249
+ passiveMessage(i18n("Download, Convert Complete. '%#1$s'") % [@outFilePath])
250
+ end
251
+ nextTask
252
+ end
253
+ end
254
+
255
+ def updateView
256
+ if running? then
257
+ # update Lapse time
258
+ updateLapse
259
+
260
+ # dump IO message buffer
261
+ checkReadOutput
262
+ end
263
+ end
264
+
265
+
266
+ protected
267
+ # increment stage
268
+ def nextTask
269
+ @stage += 1
270
+ case @stage
271
+ when DOWNLOAD
272
+ beginDownload
273
+ when CONVERT
274
+ beginConvert
275
+ else
276
+ removeRawFile
277
+ allTaskFinished
278
+ end
279
+ end
280
+
281
+ def beginDownload
282
+ $log.info { " DownloadProcess : beginDownload." }
283
+ @stage = DOWNLOAD
284
+ @downNG = true
285
+ self.status = RUNNING
286
+ @currentCommand = makeMPlayerDownloadCmd
287
+
288
+ $log.info { @currentCommand.msg }
289
+ start(@currentCommand.app, @currentCommand.args)
290
+ end
291
+
292
+ def makeMPlayerDownloadCmd
293
+ # make MPlayer Downlaod comand
294
+ cmdMsg = "mplayer -noframedrop -dumpfile %s -dumpstream %s" %
295
+ [@rawFilePath.shellescape, @sourceUrl.shellescape]
296
+ cmdApp = "mplayer"
297
+ cmdArgs = ['-noframedrop', '-dumpfile', @rawFilePath, '-dumpstream', @sourceUrl]
298
+
299
+ # debug code.
300
+ if DEBUG_DOWNLOAD then
301
+ if rand > 0.4 then
302
+ cmdApp = APP_DIR + "/mytests/sleepjob.rb"
303
+ cmdArgs = %w{ touch a/b/ }
304
+ else
305
+ cmdApp = APP_DIR + "/mytests/sleepjob.rb"
306
+ cmdArgs = %w{ touch } << @rawFilePath.shellescape
307
+ end
308
+ end
309
+
310
+ Command.new( cmdApp, cmdArgs, cmdMsg )
311
+ end
312
+
313
+ def beginConvert
314
+ @stage = CONVERT
315
+ self.status = RUNNING
316
+
317
+ cmdMsg = "nice -n 19 ffmpeg -i %s -f mp3 %s" %
318
+ [ @rawFilePath.shellescape, @outFilePath.shellescape]
319
+ cmdApp = "nice"
320
+ cmdArgs = [ '-n', '19', 'ffmpeg', '-i', @rawFilePath, '-f', 'mp3', @outFilePath ]
321
+
322
+
323
+ # debug code.
324
+ if DEBUG_DOWNLOAD then
325
+ if rand > 0.4 then
326
+ cmdApp = APP_DIR + "/mytests/sleepjob.rb"
327
+ cmdArgs = %w{ touch a/b/ }
328
+ else
329
+ cmdApp = APP_DIR + "/mytests/sleepjob.rb"
330
+ cmdArgs = %w{ cp -f } + [ @rawFilePath.shellescape, @outFilePath.shellescape ]
331
+ end
332
+ end
333
+
334
+ @currentCommand = Command.new( cmdApp, cmdArgs, cmdMsg )
335
+ $log.info { @currentCommand.msg }
336
+ start(@currentCommand.app, @currentCommand.args)
337
+ end
338
+
339
+
340
+ def removeRawFile
341
+ unless IRecSettings.leaveRawFile then
342
+ begin
343
+ File.delete(@rawFilePath)
344
+ rescue => e
345
+ $log.error { e }
346
+ end
347
+ end
348
+ end
349
+
350
+ def checkOutput(msg)
351
+ msgSum = msg.join(' ')
352
+ @downNG &&= false if msgSum =~ /Everything done/i
353
+ end
354
+
355
+ # check and read output
356
+ def checkReadOutput
357
+ msg = readAllStandardOutput.data .reject do |l| l.empty? end
358
+ checkOutput(msg)
359
+ $log.info { msg }
360
+ end
361
+
362
+
363
+ def rawFileError?
364
+ begin
365
+ $log.debug { "check duration for download." }
366
+ rawDuration = AudioFile.getDuration(@rawFilePath)
367
+ isError = rawDuration < @metaInfo.duration - 100
368
+ if isError then
369
+ $log.warn { [ "duration check error",
370
+ " rawDuration : #{rawDuration}" ] }
371
+ end
372
+ return isError if isError
373
+ $log.debug { "check file size for download." }
374
+ isError = File.size(@rawFilePath) < @metaInfo.duration * 5500
375
+ if isError then
376
+ $log.warn { [ "duration check error",
377
+ " File.size(@rawFilePath) :#{File.size(@rawFilePath)}",
378
+ " @metaInfo.duration : #{@metaInfo.duration}" ] }
379
+ end
380
+ return isError
381
+ rescue => e
382
+ $log.warn { e }
383
+ return true
384
+ end
385
+ end
386
+
387
+ def outFileError?
388
+ begin
389
+ $log.debug { "check duration for convert." }
390
+ outDuration = AudioFile.getDuration(@outFilePath)
391
+ isError = outDuration < @metaInfo.duration - 3*60 - 10
392
+ if isError then
393
+ $log.warn { [ "duration check error",
394
+ " outDuration : #{outDuration}" ] }
395
+ end
396
+ return isError
397
+ rescue => e
398
+ $log.warn { e }
399
+ return true
400
+ end
401
+ end
402
+
403
+ # return error or not
404
+ def checkErroredStatus
405
+ case @stage
406
+ when DOWNLOAD
407
+ return @downNG unless @downNG
408
+ rawFileError?
409
+ when CONVERT
410
+ outFileError?
411
+ else
412
+ true
413
+ end
414
+ end
415
+
416
+
417
+
418
+
419
+ protected
420
+ def allTaskFinished
421
+ @stage = FINISHED
422
+ self.status = DONE
423
+ end
424
+
425
+ def makeErrorMsg
426
+ [ "Failed to download a File '%#2$s'",
427
+ "Failed to convert a File '%#2$s'", "?"][@stage] %
428
+ [ @sourceUrl, @rawFilePath ]
429
+ end
430
+
431
+
432
+ def mkdirSavePath(fName)
433
+ dir = File.dirname(fName)
434
+ unless File.exist? dir
435
+ $log.info{ "mkdir : " + dir }
436
+ FileUtils.mkdir_p(dir)
437
+ end
438
+ end
439
+ end
440
+
441
+