irecorder 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +19 -0
- data/README +45 -0
- data/Rakefile +72 -0
- data/bin/irecorder +9 -0
- data/bin/irecorder.rb +885 -0
- data/ext/Rakefile +27 -0
- data/lib/bbcnet.rb +271 -0
- data/lib/cache.rb +121 -0
- data/lib/download.rb +441 -0
- data/lib/irecorder_resource.rb +264 -0
- data/lib/logwin.rb +119 -0
- data/lib/mylibs.rb +462 -0
- data/lib/programmewin.rb +185 -0
- data/lib/settings.rb +376 -0
- data/lib/taskwin.rb +363 -0
- data/resources/bbcstyle.qss +443 -0
- metadata +107 -0
data/ext/Rakefile
ADDED
@@ -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 ]
|
data/lib/bbcnet.rb
ADDED
@@ -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
|
data/lib/cache.rb
ADDED
@@ -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
|
data/lib/download.rb
ADDED
@@ -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
|
+
|