irecorder 0.0.7
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.
- 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
|
+
|