irecorder 0.0.5-linux → 0.0.6-linux
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +1 -1
- data/README +3 -3
- data/Rakefile +3 -3
- data/bin/irecorder.rb +141 -95
- data/lib/bbcnet.rb +6 -9
- data/lib/cache.rb +18 -12
- data/lib/download.rb +22 -11
- data/lib/mylibs.rb +5 -0
- data/lib/programmewin.rb +34 -31
- data/lib/taskwin.rb +8 -6
- data/resources/bbcstyle.qss +5 -0
- metadata +6 -5
data/MIT-LICENSE
CHANGED
data/README
CHANGED
@@ -3,7 +3,7 @@ You can browse BBC Radio programmes and click to download stream file.
|
|
3
3
|
files will be converted to mp3 files automatically.
|
4
4
|
irecorder allow to play without any other browser or play on your prefered browser.
|
5
5
|
|
6
|
-
|
6
|
+
KDE ruby
|
7
7
|
==============
|
8
8
|
irecorder require kdebindings.
|
9
9
|
please check your distro package to install it.
|
@@ -11,7 +11,7 @@ or you can compile from source.
|
|
11
11
|
http://download.kde.org/download.php?url=stable/4.5.0/src/kdebindings-4.5.0.tar.bz2
|
12
12
|
|
13
13
|
|
14
|
-
|
14
|
+
Installation
|
15
15
|
==================
|
16
16
|
install most important package kdebindings4.
|
17
17
|
on Mandriva Linux use urpmi command.
|
@@ -36,7 +36,7 @@ launch irecorder just type irecorder.rb
|
|
36
36
|
# irecorder.rb
|
37
37
|
|
38
38
|
|
39
|
-
|
39
|
+
Build Gem
|
40
40
|
=================
|
41
41
|
if you want to gem package from source, use rake command.
|
42
42
|
|
data/Rakefile
CHANGED
@@ -6,10 +6,10 @@ require 'rake/gempackagetask'
|
|
6
6
|
|
7
7
|
spec = Gem::Specification.new do |s|
|
8
8
|
s.name = "irecorder"
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.6"
|
10
10
|
s.author = "ruby.twiddler"
|
11
11
|
s.email = "ruby.twiddler at gmail.com"
|
12
|
-
s.homepage = "http://
|
12
|
+
s.homepage = "http://github.com/rubytwiddler/irecorder/wiki"
|
13
13
|
s.platform = "linux"
|
14
14
|
s.summary = "BBC iPlayer like audio recorder with KDE GUI."
|
15
15
|
s.files = FileList["{bin,lib}/**/*"].to_a
|
@@ -17,7 +17,7 @@ spec = Gem::Specification.new do |s|
|
|
17
17
|
s.executables = [ 'irecorder.rb' ]
|
18
18
|
s.license = "MIT-LICENSE"
|
19
19
|
s.require_path = "lib"
|
20
|
-
s.requirements = %w{ korundum4 qtwebkit kio }
|
20
|
+
s.requirements = %w{ korundum4 qtwebkit kio ktexteditor }
|
21
21
|
s.add_runtime_dependency( 'nokogiri', '>= 1.4.0' )
|
22
22
|
s.description = <<-EOF
|
23
23
|
BBC iPlayer like audio recorder with KDE GUI.
|
data/bin/irecorder.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# 2010 by ruby.twiddler@gmail.com
|
4
4
|
#
|
5
|
-
# iPlayer
|
5
|
+
# iPlayer like stream recorder
|
6
6
|
# record real/wma (rtsp/mms) audio stream
|
7
7
|
#
|
8
8
|
|
@@ -12,14 +12,13 @@ require 'ftools'
|
|
12
12
|
APP_NAME = File.basename(__FILE__).sub(/\.rb/, '')
|
13
13
|
APP_DIR = File::dirname(File.expand_path(File.dirname(__FILE__)))
|
14
14
|
LIB_DIR = File::join(APP_DIR, "lib")
|
15
|
-
APP_VERSION = "0.0.
|
15
|
+
APP_VERSION = "0.0.6"
|
16
16
|
|
17
17
|
# standard libs
|
18
18
|
require 'rubygems'
|
19
19
|
require 'uri'
|
20
20
|
require 'net/http'
|
21
21
|
require 'open-uri'
|
22
|
-
require 'rss'
|
23
22
|
require 'shellwords'
|
24
23
|
require 'fileutils'
|
25
24
|
require 'singleton'
|
@@ -48,9 +47,6 @@ require "settings"
|
|
48
47
|
# Main Window Class
|
49
48
|
#
|
50
49
|
class MainWindow < KDE::MainWindow
|
51
|
-
slots :startDownload, :updateTask, :getList, :reloadStyleSheet, :clearStyleSheet
|
52
|
-
slots :mediaFilterChanged, :playProgramme
|
53
|
-
slots 'programmeCellClicked(int,int)'
|
54
50
|
|
55
51
|
GroupName = "MainWindow"
|
56
52
|
|
@@ -72,7 +68,7 @@ class MainWindow < KDE::MainWindow
|
|
72
68
|
# BBCNet.setProxy('http://194.36.10.154:3127')
|
73
69
|
# initialize values
|
74
70
|
$log = MyLogger.new(@logWin)
|
75
|
-
$log.level = MyLogger::
|
71
|
+
$log.level = MyLogger::DEBUG
|
76
72
|
$log.info { 'Log Start.' }
|
77
73
|
|
78
74
|
# assign from config file.
|
@@ -132,20 +128,42 @@ class MainWindow < KDE::MainWindow
|
|
132
128
|
|
133
129
|
|
134
130
|
# Help menu
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
131
|
+
aboutDlg = KDE::AboutApplicationDialog.new($about)
|
132
|
+
openAboutAction = KDE::Action.new(KDE::Icon.new('irecorder'),
|
133
|
+
i18n('About iRecorder'), self)
|
134
|
+
connect(openAboutAction, SIGNAL(:triggered), aboutDlg, SLOT(:exec))
|
135
|
+
|
136
|
+
#
|
137
|
+
openDocUrlAction = KDE::Action.new(KDE::Icon.new('help-contents'),
|
138
|
+
i18n('Open Document Wiki'), self)
|
139
|
+
connect(openDocUrlAction, SIGNAL(:triggered), self, SLOT(:openDocUrl))
|
140
|
+
|
141
|
+
#
|
142
|
+
openReportIssueUrlAction = KDE::Action.new(
|
143
|
+
KDE::Icon.new('tools-report-bug'), i18n('Report Bug'), self)
|
144
|
+
connect(openReportIssueUrlAction, SIGNAL(:triggered), self,
|
145
|
+
SLOT(:openReportIssueUrl))
|
146
|
+
|
147
|
+
#
|
148
|
+
openSourceAction = KDE::Action.new(KDE::Icon.new('document-open-folder'),
|
149
|
+
i18n('Open Source Folder'), self)
|
150
|
+
connect(openSourceAction, SIGNAL(:triggered), self, SLOT(:openSource))
|
151
|
+
|
152
|
+
helpMenu = KDE::Menu.new(i18n('&Help'), self)
|
153
|
+
helpMenu.addAction(openDocUrlAction)
|
154
|
+
# helpMenu.addAction(openHomeUrlAction)
|
155
|
+
helpMenu.addAction(openReportIssueUrlAction)
|
156
|
+
# helpMenu.addAction(openRDocAction)
|
157
|
+
helpMenu.addAction(openSourceAction)
|
158
|
+
helpMenu.addSeparator
|
159
|
+
helpMenu.addAction(openAboutAction)
|
142
160
|
|
143
161
|
# insert menus in MenuBar
|
144
162
|
menu = KDE::MenuBar.new
|
145
163
|
menu.addMenu( fileMenu )
|
146
164
|
menu.addMenu( settingsMenu )
|
147
165
|
menu.addSeparator
|
148
|
-
menu.addMenu( helpMenu
|
166
|
+
menu.addMenu( helpMenu )
|
149
167
|
setMenuBar(menu)
|
150
168
|
end
|
151
169
|
|
@@ -194,6 +212,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
194
212
|
|
195
213
|
#-------------------------------------------------------------
|
196
214
|
#
|
215
|
+
TvType, RadioType, RadioCategoryType = [-1,0,1] # TvType = -1 (hide)
|
197
216
|
TVChannelRssTbl = [
|
198
217
|
['BBC One', 'bbc_one' ],
|
199
218
|
['BBC Two', 'bbc_two' ],
|
@@ -252,11 +271,15 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
252
271
|
w.objectName = 'channelToolBox'
|
253
272
|
end
|
254
273
|
|
274
|
+
# default value
|
275
|
+
toolBox.currentIndex = 2
|
276
|
+
|
255
277
|
# TV & Radio Channels selector
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
278
|
+
# @tvChannelListBox = KDE::ListWidget.new
|
279
|
+
# # TV Channels
|
280
|
+
# @tvChannelListBox.addItems( TVChannelRssTbl.map do |w| w[0] end )
|
281
|
+
# toolBox.addItem( @tvChannelListBox, 'TV Channels' )
|
282
|
+
|
260
283
|
|
261
284
|
# Radio Channels
|
262
285
|
@radioChannelListBox = KDE::ListWidget.new
|
@@ -266,7 +289,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
266
289
|
# Category selector
|
267
290
|
@categoryListBox = KDE::ListWidget.new
|
268
291
|
@categoryListBox.addItems( CategoryRssTbl.map do |w| w[0] end )
|
269
|
-
toolBox.addItem( @categoryListBox, 'Categories' )
|
292
|
+
toolBox.addItem( @categoryListBox, 'Radio Categories' )
|
270
293
|
|
271
294
|
toolBox
|
272
295
|
end
|
@@ -339,6 +362,8 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
339
362
|
connect(w,SIGNAL('textChanged(const QString &)'),
|
340
363
|
@programmeTable, SLOT('filterChanged(const QString &)'))
|
341
364
|
w.setClearButtonShown(true)
|
365
|
+
connect(@programmeTable, SIGNAL('filterRequest(const QString &)'),
|
366
|
+
w, SLOT('setText(const QString &)'))
|
342
367
|
end
|
343
368
|
)
|
344
369
|
end
|
@@ -346,35 +371,20 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
346
371
|
vbxw.addWidget( @listTitleLabel = Qt::Label.new('') )
|
347
372
|
vbxw.addWidget(@programmeTable)
|
348
373
|
|
349
|
-
# 'Start Download' Button
|
350
|
-
@tvFilterBtn = KDE::PushButton.new(i18n("TV")) do |w|
|
351
|
-
w.objectName = 'mediaButton'
|
352
|
-
w.checkable = true
|
353
|
-
w.autoExclusive = true
|
354
|
-
connect( w, SIGNAL(:clicked), self, SLOT(:mediaFilterChanged) )
|
355
|
-
end
|
356
|
-
|
357
|
-
@radioFilterBtn = KDE::PushButton.new(i18n("Radio")) do |w|
|
358
|
-
w.objectName = 'mediaButton'
|
359
|
-
w.checkable = true
|
360
|
-
w.autoExclusive = true
|
361
|
-
w.checked = true
|
362
|
-
connect( w, SIGNAL(:clicked), self, SLOT(:mediaFilterChanged) )
|
363
|
-
end
|
364
|
-
|
365
374
|
playIcon = KDE::Icon.new(':images/play-22.png')
|
366
375
|
playBtn = KDE::PushButton.new( playIcon, i18n("Play")) do |w|
|
367
376
|
w.objectName = 'playButton'
|
368
377
|
connect( w, SIGNAL(:clicked), self, SLOT(:playProgramme) )
|
369
378
|
end
|
370
379
|
|
380
|
+
# 'Start Download' Button
|
371
381
|
downloadIcon = KDE::Icon.new(':images/download-22.png')
|
372
382
|
downloadBtn = KDE::PushButton.new( downloadIcon, i18n("Download")) do |w|
|
373
383
|
w.objectName = 'downloadButton'
|
374
384
|
connect( w, SIGNAL(:clicked), self, SLOT(:startDownload) )
|
375
385
|
end
|
376
386
|
|
377
|
-
vbxw.addWidgets(
|
387
|
+
vbxw.addWidgets( nil, playBtn, nil, downloadBtn, nil )
|
378
388
|
end
|
379
389
|
end
|
380
390
|
|
@@ -468,7 +478,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
468
478
|
end
|
469
479
|
|
470
480
|
# ------------------------------------------------------------------------
|
471
|
-
|
481
|
+
slots :reloadStyleSheet
|
472
482
|
def reloadStyleSheet
|
473
483
|
styleStr = IO.read(APP_DIR + '/resources/bbcstyle.qss')
|
474
484
|
$app.styleSheet = styleStr
|
@@ -476,7 +486,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
476
486
|
$log.info { 'Reloaded StyleSheet.' }
|
477
487
|
end
|
478
488
|
|
479
|
-
|
489
|
+
slots :clearStyleSheet
|
480
490
|
def clearStyleSheet
|
481
491
|
$app.styleSheet = nil
|
482
492
|
$log.info { 'Cleared StyleSheet.' }
|
@@ -484,6 +494,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
484
494
|
|
485
495
|
|
486
496
|
# slot :
|
497
|
+
slots 'programmeCellClicked(int,int)'
|
487
498
|
def programmeCellClicked(row, column)
|
488
499
|
prog = @programmeTable[row]
|
489
500
|
color = "#%06x" % (@programmeSummaryWebView.palette.color(Qt::Palette::Text).rgb & 0xffffff)
|
@@ -500,25 +511,21 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
500
511
|
@programmeSummaryWebView.setHtml(html)
|
501
512
|
end
|
502
513
|
|
503
|
-
# slot :
|
504
|
-
def mediaFilterChanged
|
505
|
-
setMediaFilter
|
506
|
-
@programmeTable.filterChanged(@filterLineEdit.text)
|
507
|
-
end
|
508
514
|
|
509
515
|
|
510
|
-
|
516
|
+
def makeProcCommand(command, url)
|
517
|
+
cmd, args = command.split(/\s+/, 2)
|
518
|
+
args = args.split(/\s+/).map do |a|
|
519
|
+
a.gsub(/%\w/, url)
|
520
|
+
end
|
521
|
+
[ cmd, args ]
|
522
|
+
end
|
523
|
+
|
524
|
+
slots :playProgramme
|
511
525
|
def playProgramme
|
512
526
|
items = @programmeTable.selectedItems
|
513
527
|
return unless items.size > 0
|
514
528
|
|
515
|
-
def makeProcCommand(command, url)
|
516
|
-
cmd, args = command.split(/\s+/, 2)
|
517
|
-
args = args.split(/\s+/).map do |a|
|
518
|
-
a.gsub(/%\w/, url)
|
519
|
-
end
|
520
|
-
[ cmd, args ]
|
521
|
-
end
|
522
529
|
|
523
530
|
def getIplayerUrl(prog)
|
524
531
|
# big type console
|
@@ -563,6 +570,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
563
570
|
$log.info { "episode Url : #{url}" }
|
564
571
|
minfo = BBCNet::CacheMetaInfoDevice.read(url)
|
565
572
|
$log.debug { "#{minfo.inspect}" }
|
573
|
+
raise "No stream Url" unless minfo.wma
|
566
574
|
url = minfo.wma.url
|
567
575
|
|
568
576
|
cmd, args = makeProcCommand(directPlayerCommand, url)
|
@@ -572,17 +580,51 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
572
580
|
proc.start(cmd, args)
|
573
581
|
end
|
574
582
|
rescue => e
|
575
|
-
|
576
|
-
|
583
|
+
if e.kind_of? RuntimeError
|
584
|
+
$log.info { e.message }
|
585
|
+
else
|
586
|
+
$log.error { e }
|
587
|
+
end
|
588
|
+
# some messages must be treated.
|
589
|
+
# already expired.
|
590
|
+
# some xml error.
|
591
|
+
# no url
|
592
|
+
passiveMessage(i18n("There is no direct stream for this programme.\n %s" %[prog.title]))
|
577
593
|
end
|
578
594
|
end
|
579
595
|
|
596
|
+
slots :openDocUrl
|
597
|
+
def openDocUrl
|
598
|
+
openUrlDocument('http://github.com/rubytwiddler/irecorder/wiki')
|
599
|
+
end
|
600
|
+
|
601
|
+
slots :openReportIssueUrl
|
602
|
+
def openReportIssueUrl
|
603
|
+
openUrlDocument('http://github.com/rubytwiddler/irecorder/issues')
|
604
|
+
end
|
605
|
+
|
606
|
+
slots :openSource
|
607
|
+
def openSource
|
608
|
+
cmd = 'dolphin'
|
609
|
+
args = [ APP_DIR ]
|
610
|
+
proc = Qt::Process.new(self)
|
611
|
+
proc.start(cmd, args)
|
612
|
+
end
|
613
|
+
|
614
|
+
def openUrlDocument(url)
|
615
|
+
webPlayerCommand = IRecSettings.webPlayerCommand
|
616
|
+
cmd, args = makeProcCommand(webPlayerCommand, url)
|
617
|
+
$log.debug { "execute cmd '#{cmd}', args '#{args.inspect}'" }
|
618
|
+
proc = Qt::Process.new(self)
|
619
|
+
proc.start(cmd, args)
|
620
|
+
end
|
580
621
|
|
581
622
|
# ------------------------------------------------------------------------
|
582
623
|
#
|
583
624
|
# slot: called when 'Get List' Button clicked signal invoked.
|
584
625
|
#
|
585
626
|
public
|
627
|
+
slots :getList
|
586
628
|
def getList
|
587
629
|
feedAdr = getFeedAdr
|
588
630
|
return if feedAdr.nil?
|
@@ -591,10 +633,9 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
591
633
|
|
592
634
|
begin
|
593
635
|
makeTablefromRss( CacheRssDevice.read(feedAdr) )
|
594
|
-
rescue
|
636
|
+
rescue => e
|
595
637
|
$log.error { e }
|
596
638
|
end
|
597
|
-
mediaFilterChanged
|
598
639
|
setListTitle
|
599
640
|
end
|
600
641
|
|
@@ -608,18 +649,18 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
608
649
|
|
609
650
|
channelStr = nil
|
610
651
|
case @channelType
|
611
|
-
when
|
652
|
+
when TvType
|
612
653
|
# get TV channel
|
613
654
|
@channelIndex = @tvChannelListBox.currentRow
|
614
655
|
channelStr = TVChannelRssTbl[ @channelIndex ][1]
|
615
|
-
when
|
656
|
+
when RadioType
|
616
657
|
# get Radio channel
|
617
658
|
@channelIndex = @radioChannelListBox.currentRow
|
618
659
|
channelStr = RadioChannelRssTbl[ @channelIndex ][1]
|
619
|
-
when
|
660
|
+
when RadioCategoryType
|
620
661
|
# get Category
|
621
662
|
@channelIndex = @categoryListBox.currentRow
|
622
|
-
channelStr = 'categories/' + CategoryRssTbl[ @channelIndex ][1]
|
663
|
+
channelStr = 'categories/' + CategoryRssTbl[ @channelIndex ][1] + '/radio'
|
623
664
|
end
|
624
665
|
|
625
666
|
return nil if channelStr.nil?
|
@@ -648,23 +689,25 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
648
689
|
|
649
690
|
protected
|
650
691
|
def makeTablefromRss(rss)
|
651
|
-
|
692
|
+
@programmeTable.clearContents
|
693
|
+
@programmeTable.rowCount = 0
|
694
|
+
@filterLineEdit.clear
|
695
|
+
entries = rss.css('entry')
|
696
|
+
return unless rss and entries and entries.size
|
697
|
+
|
652
698
|
sortFlag = @programmeTable.sortingEnabled
|
653
699
|
@programmeTable.sortingEnabled = false
|
654
700
|
@programmeTable.hide
|
655
|
-
@programmeTable.
|
656
|
-
@filterLineEdit.clear
|
657
|
-
@programmeTable.rowCount = rss.entries.size
|
658
|
-
setMediaFilter
|
701
|
+
@programmeTable.rowCount = entries.size
|
659
702
|
|
660
703
|
# ['Title', 'Category', 'Updated' ]
|
661
|
-
|
662
|
-
title = i.title.content
|
663
|
-
updated = i.updated.content
|
664
|
-
contents = i.content.content
|
665
|
-
linkItem = i.
|
666
|
-
link = linkItem ? linkItem
|
667
|
-
categories = i.
|
704
|
+
entries.each_with_index do |i, r|
|
705
|
+
title = i.at_css('title').content
|
706
|
+
updated = i.at_css('updated').content
|
707
|
+
contents = i.at_css('content').content
|
708
|
+
linkItem = i.css('link').find do |l| l['rel'] == 'self' end
|
709
|
+
link = linkItem ? linkItem['href'] : nil
|
710
|
+
categories = i.css('category').map do |c| c['term'] end.join(',')
|
668
711
|
$log.misc { title }
|
669
712
|
@programmeTable.addEntry( r, title, categories, updated, contents, link )
|
670
713
|
end
|
@@ -673,19 +716,6 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
673
716
|
@programmeTable.show
|
674
717
|
end
|
675
718
|
|
676
|
-
def setMediaFilter
|
677
|
-
@programmeTable.mediaFilter =
|
678
|
-
case @channelType
|
679
|
-
when 2
|
680
|
-
@tvFilterBtn.enabled = true
|
681
|
-
@radioFilterBtn.enabled = true
|
682
|
-
@tvFilterBtn.checked ? 'tv' : 'radio'
|
683
|
-
else
|
684
|
-
@tvFilterBtn.enabled = false
|
685
|
-
@radioFilterBtn.enabled = false
|
686
|
-
''
|
687
|
-
end
|
688
|
-
end
|
689
719
|
|
690
720
|
|
691
721
|
# ------------------------------------------------------------------------
|
@@ -695,13 +725,18 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
695
725
|
# Start Downloading
|
696
726
|
#
|
697
727
|
public
|
728
|
+
slots :startDownload
|
698
729
|
def startDownload
|
699
730
|
rowsSet = {} # use Hash as Set.
|
700
731
|
@programmeTable.selectedItems.each do |i| rowsSet[i.row] = true end
|
701
732
|
|
702
|
-
|
733
|
+
titles = {}
|
734
|
+
rowsSet.keys.map do |r|
|
735
|
+
@programmeTable[r]
|
736
|
+
end .each do |p| titles[p.title] = p end
|
737
|
+
|
738
|
+
titles.each_value do |prog|
|
703
739
|
begin
|
704
|
-
prog = @programmeTable[r]
|
705
740
|
url = prog.content[UrlRegexp] # String[] method extract only 1st one.
|
706
741
|
|
707
742
|
$log.info { "episode Url : #{url}" }
|
@@ -712,15 +747,22 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
712
747
|
$log.info { "save name : #{fName}" }
|
713
748
|
|
714
749
|
startDownOneFile(minfo, fName)
|
750
|
+
|
751
|
+
passiveMessage(i18n("Start Download programme '%s'") % [prog.title])
|
752
|
+
|
715
753
|
rescue Timeout::Error, StandardError => e
|
716
|
-
|
717
|
-
|
754
|
+
if e.kind_of? RuntimeError
|
755
|
+
$log.info { e.message }
|
756
|
+
else
|
757
|
+
$log.error { e }
|
758
|
+
end
|
759
|
+
passiveMessage(i18n("There is no direct stream for this programme.\n%s" %[prog.title]))
|
718
760
|
end
|
719
761
|
end
|
720
762
|
end
|
721
763
|
|
722
764
|
|
723
|
-
|
765
|
+
protected
|
724
766
|
#
|
725
767
|
def getSaveName(prog, ext='wma')
|
726
768
|
tags = prog.categories.split(/,/)
|
@@ -765,10 +807,10 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
765
807
|
|
766
808
|
def getChannelTitle
|
767
809
|
case @channelType
|
768
|
-
when
|
810
|
+
when TvType
|
769
811
|
# get TV channel
|
770
812
|
TVChannelRssTbl[ @channelIndex ][0]
|
771
|
-
when
|
813
|
+
when RadioType
|
772
814
|
# get Radio channel
|
773
815
|
RadioChannelRssTbl[ @channelIndex ][0]
|
774
816
|
else
|
@@ -814,6 +856,7 @@ BBC iPlayer like audio (mms/rtsp) stream recorder.
|
|
814
856
|
# slot : periodically called to update task view.
|
815
857
|
#
|
816
858
|
protected
|
859
|
+
slots :updateTask
|
817
860
|
def updateTask
|
818
861
|
@taskWin.each do |task|
|
819
862
|
task.process.updateView
|
@@ -826,9 +869,12 @@ end
|
|
826
869
|
# main start
|
827
870
|
#
|
828
871
|
|
829
|
-
about = KDE::AboutData.new(APP_NAME, APP_NAME, KDE::ki18n(APP_NAME), APP_VERSION
|
830
|
-
|
831
|
-
|
872
|
+
$about = KDE::AboutData.new(APP_NAME, APP_NAME, KDE::ki18n(APP_NAME), APP_VERSION,
|
873
|
+
KDE::ki18n('BBC iRecorder KDE')
|
874
|
+
)
|
875
|
+
$about.setProgramIconName(':images/irecorder-22.png')
|
876
|
+
$about.addLicenseTextFile(APP_DIR + '/MIT-LICENSE')
|
877
|
+
KDE::CmdLineArgs.init(ARGV, $about)
|
832
878
|
# options = KDE::CmdLineOptions.new()
|
833
879
|
# options.add( "+url", KDE::ki18n( "The url to record)" ),"")
|
834
880
|
|
data/lib/bbcnet.rb
CHANGED
@@ -5,7 +5,6 @@ require 'rubygems'
|
|
5
5
|
require 'uri'
|
6
6
|
require 'net/http'
|
7
7
|
require 'open-uri'
|
8
|
-
require 'rss'
|
9
8
|
require 'nokogiri'
|
10
9
|
require 'shellwords'
|
11
10
|
require 'fileutils'
|
@@ -25,8 +24,8 @@ class BBCNet
|
|
25
24
|
DirectStreamRegexp = URI.regexp(['mms', 'rtsp', 'rtmp', 'rtmpt'])
|
26
25
|
|
27
26
|
class CacheMetaInfoDevice < CasheDevice::CacheDeviceBase
|
28
|
-
def initialize(
|
29
|
-
super(
|
27
|
+
def initialize(cacheDuration = 40*60, cacheMax=200)
|
28
|
+
super(cacheDuration, cacheMax)
|
30
29
|
end
|
31
30
|
|
32
31
|
# return : [ data, key ]
|
@@ -81,6 +80,9 @@ class BBCNet
|
|
81
80
|
# res = IO.read("../tmp/iplayer-playlist-me.xml")
|
82
81
|
|
83
82
|
doc = Nokogiri::XML(res)
|
83
|
+
item = doc.at_css("noItems")
|
84
|
+
raise "No Playlist " + item[:reason] if item
|
85
|
+
|
84
86
|
item = doc.at_css("item")
|
85
87
|
@media = item[:kind].gsub(/programme/i, '')
|
86
88
|
@duration = item[:duration].to_i
|
@@ -158,11 +160,6 @@ class BBCNet
|
|
158
160
|
#
|
159
161
|
|
160
162
|
# convert epsode Url to console Url
|
161
|
-
# example
|
162
|
-
# from
|
163
|
-
# http://www.bbc.co.uk/iplayer/episode/b007jpkt/Miss_Marple_A_Caribbean_Mystery_Episode_1/
|
164
|
-
# to
|
165
|
-
# http://www.bbc.co.uk/iplayer/console/b007jpkt
|
166
163
|
def self.getPlayerConsoleUrl(url)
|
167
164
|
"http://www.bbc.co.uk/iplayer/console/" + extractPid(url)
|
168
165
|
end
|
@@ -188,7 +185,7 @@ class BBCNet
|
|
188
185
|
while url != old and not url[DirectStreamRegexp] do
|
189
186
|
old = url
|
190
187
|
res = BBCNet.read(url)
|
191
|
-
url = res[ DirectStreamRegexp ]
|
188
|
+
url = res[ DirectStreamRegexp ] || res[ UrlRegexp ] || old
|
192
189
|
$log.debug { "new url:#{url}, old url:#{old}" }
|
193
190
|
$log.debug { "no url in response '#{res}'" } if url[ UrlRegexp ]
|
194
191
|
end
|
data/lib/cache.rb
CHANGED
@@ -9,9 +9,9 @@ module CasheDevice
|
|
9
9
|
attr_accessor :expireTime, :url, :key
|
10
10
|
end
|
11
11
|
|
12
|
-
attr_accessor :
|
13
|
-
def initialize(
|
14
|
-
@
|
12
|
+
attr_accessor :cacheDuration, :cacheMax
|
13
|
+
def initialize(cacheDuration = 26*60, cacheMax=10)
|
14
|
+
@cacheDuration = cacheDuration # 12 minutes
|
15
15
|
@cache = Hash.new
|
16
16
|
@cacheLRU = [] # Least Recently Used
|
17
17
|
@cacheMax = cacheMax
|
@@ -46,7 +46,7 @@ module CasheDevice
|
|
46
46
|
end
|
47
47
|
cached = CachedData.new
|
48
48
|
cached.url = url
|
49
|
-
cached.expireTime = startTime + @
|
49
|
+
cached.expireTime = startTime + @cacheDuration
|
50
50
|
data, cached.key = directRead(url)
|
51
51
|
@cache[url] = cached
|
52
52
|
@cacheLRU.push(cached)
|
@@ -67,22 +67,22 @@ end
|
|
67
67
|
# practical implementations.
|
68
68
|
#
|
69
69
|
class CacheRssDevice < CasheDevice::CacheDeviceBase
|
70
|
-
def initialize(
|
71
|
-
super(
|
70
|
+
def initialize(cacheDuration = 12*60, cacheMax=6)
|
71
|
+
super(cacheDuration, cacheMax)
|
72
72
|
end
|
73
73
|
|
74
74
|
# return : [ data, key ]
|
75
75
|
# key : key to restore data.
|
76
76
|
def directRead(url)
|
77
|
-
data =
|
77
|
+
data = Nokogiri::XML(CacheHttpDiskDevice.read(url))
|
78
78
|
[ data, data ]
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
82
|
|
83
83
|
class CacheHttpDiskDevice < CasheDevice::CacheDeviceBase
|
84
|
-
def initialize(
|
85
|
-
super(
|
84
|
+
def initialize(cacheDuration = 12*60, cacheMax=50)
|
85
|
+
super(cacheDuration, cacheMax)
|
86
86
|
@tmpdir = Dir.tmpdir + '/bbc_cache'
|
87
87
|
FileUtils.mkdir_p(@tmpdir)
|
88
88
|
end
|
@@ -96,11 +96,17 @@ class CacheHttpDiskDevice < CasheDevice::CacheDeviceBase
|
|
96
96
|
# return : [ data, key ]
|
97
97
|
# key : key to restore data.
|
98
98
|
def directRead(url)
|
99
|
-
|
99
|
+
$log.debug { "directRead(): " + self.class.name }
|
100
100
|
tmpfname = tempFileName(url)
|
101
101
|
|
102
|
+
if File.exist?(tmpfname) then
|
103
|
+
$log.debug { "File ctime : " + File.ctime(tmpfname).to_s}
|
104
|
+
$log.debug { "expire time : " + (File.ctime(tmpfname) + @cacheDuration).to_s }
|
105
|
+
$log.debug { "Now Time : " + Time.now.to_s }
|
106
|
+
end
|
107
|
+
|
102
108
|
if File.exist?(tmpfname) and
|
103
|
-
File.ctime(tmpfname) + @
|
109
|
+
File.ctime(tmpfname) + @cacheDuration > Time.now then
|
104
110
|
data = IO.read(tmpfname)
|
105
111
|
else
|
106
112
|
data = BBCNet.read(url)
|
@@ -110,6 +116,6 @@ class CacheHttpDiskDevice < CasheDevice::CacheDeviceBase
|
|
110
116
|
end
|
111
117
|
|
112
118
|
def tempFileName(url)
|
113
|
-
File.join(@tmpdir, url.scan(%r{\w
|
119
|
+
File.join(@tmpdir, url.scan(%r{(?:iplayer/)[\w\/]+$}).first.gsub!(/iplayer\//,'').gsub!(%r|/|, '_'))
|
114
120
|
end
|
115
121
|
end
|
data/lib/download.rb
CHANGED
@@ -7,7 +7,6 @@ require "bbcnet.rb"
|
|
7
7
|
#
|
8
8
|
#
|
9
9
|
class DownloadProcess < Qt::Process
|
10
|
-
slots 'taskFinished(int,QProcess::ExitStatus)'
|
11
10
|
attr_reader :sourceUrl, :fileName
|
12
11
|
attr_accessor :taskItem
|
13
12
|
|
@@ -136,13 +135,12 @@ class DownloadProcess < Qt::Process
|
|
136
135
|
def cancelTask
|
137
136
|
if running? then
|
138
137
|
self.terminate
|
138
|
+
taskFinished(1,0)
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
142
|
def removeData
|
143
|
-
|
144
|
-
self.terminate
|
145
|
-
end
|
143
|
+
cancelTask
|
146
144
|
begin
|
147
145
|
File.delete(@rawFilePath)
|
148
146
|
File.delete(@outFilePath)
|
@@ -159,18 +157,23 @@ class DownloadProcess < Qt::Process
|
|
159
157
|
Time.now - @startTime
|
160
158
|
end
|
161
159
|
|
162
|
-
|
160
|
+
slots 'taskFinished(int,QProcess::ExitStatus)'
|
163
161
|
def taskFinished(exitCode, exitStatus)
|
164
162
|
checkReadOutput
|
165
163
|
if (exitCode.to_i.nonzero? || exitStatus.to_i.nonzero?) && checkErroredStatus then
|
166
164
|
self.status = ERROR
|
167
|
-
|
165
|
+
errMsg = makeErrorMsg
|
166
|
+
$log.error { [ errMsg, "exitCode=#{exitCode}, exitStatus=#{exitStatus}" ] }
|
167
|
+
passiveMessage(errMsg)
|
168
168
|
else
|
169
169
|
$log.info {
|
170
170
|
[ "Successed to download a File '%#2$s'",
|
171
171
|
"Successed to convert a File '%#2$s'", ][@stage] %
|
172
172
|
[ @sourceUrl, @rawFilePath ]
|
173
173
|
}
|
174
|
+
if @stage == CONVERT then
|
175
|
+
passiveMessage(i18n("Download, Convert Complete. '%#1$s'") % [@outFilePath])
|
176
|
+
end
|
174
177
|
nextTask
|
175
178
|
end
|
176
179
|
end
|
@@ -222,10 +225,10 @@ class DownloadProcess < Qt::Process
|
|
222
225
|
# debug code.
|
223
226
|
if DEBUG_DOWNLOAD then
|
224
227
|
if rand > 0.4 then
|
225
|
-
cmdApp = "
|
228
|
+
cmdApp = APP_DIR + "/mytests/sleepjob.rb"
|
226
229
|
cmdArgs = %w{ touch a/b/ }
|
227
230
|
else
|
228
|
-
cmdApp = "
|
231
|
+
cmdApp = APP_DIR + "/mytests/sleepjob.rb"
|
229
232
|
cmdArgs = %w{ touch } << @rawFilePath.shellescape
|
230
233
|
end
|
231
234
|
end
|
@@ -246,10 +249,10 @@ class DownloadProcess < Qt::Process
|
|
246
249
|
# debug code.
|
247
250
|
if DEBUG_DOWNLOAD then
|
248
251
|
if rand > 0.4 then
|
249
|
-
cmdApp = "
|
252
|
+
cmdApp = APP_DIR + "/mytests/sleepjob.rb"
|
250
253
|
cmdArgs = %w{ touch a/b/ }
|
251
254
|
else
|
252
|
-
cmdApp = "
|
255
|
+
cmdApp = APP_DIR + "/mytests/sleepjob.rb"
|
253
256
|
cmdArgs = %w{ cp -f } + [ @rawFilePath.shellescape, @outFilePath.shellescape ]
|
254
257
|
end
|
255
258
|
end
|
@@ -300,7 +303,15 @@ class DownloadProcess < Qt::Process
|
|
300
303
|
when CONVERT
|
301
304
|
begin
|
302
305
|
$log.debug { "check duration for convert." }
|
303
|
-
|
306
|
+
outDuration = AudioFile.getDuration(@outFilePath)
|
307
|
+
rawDuration = AudioFile.getDuration(@rawFilePath)
|
308
|
+
isError = outDuration < rawDuration - 3*60 - 10
|
309
|
+
if isError then
|
310
|
+
$log.warn { [ "duration check error",
|
311
|
+
" outDuration : #{outDuration}",
|
312
|
+
" rawDuration : #{rawDuration}" ] }
|
313
|
+
end
|
314
|
+
isError
|
304
315
|
rescue => e
|
305
316
|
$log.warn { e }
|
306
317
|
true
|
data/lib/mylibs.rb
CHANGED
@@ -105,6 +105,11 @@ class HBoxLayoutWidget < Qt::Widget
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
+
def passiveMessage(text)
|
109
|
+
%x{ kdialog --passivepopup #{text.shellescape} }
|
110
|
+
end
|
111
|
+
|
112
|
+
|
108
113
|
#--------------------------------------------------------------------------
|
109
114
|
#
|
110
115
|
#
|
data/lib/programmewin.rb
CHANGED
@@ -3,8 +3,6 @@
|
|
3
3
|
#
|
4
4
|
#
|
5
5
|
class ProgrammeTableWidget < Qt::TableWidget
|
6
|
-
slots 'filterChanged(const QString &)'
|
7
|
-
|
8
6
|
#
|
9
7
|
#
|
10
8
|
class Programme
|
@@ -46,7 +44,6 @@ class ProgrammeTableWidget < Qt::TableWidget
|
|
46
44
|
#------------------------------------------------------------------------
|
47
45
|
#
|
48
46
|
#
|
49
|
-
attr_accessor :mediaFilter
|
50
47
|
|
51
48
|
def initialize()
|
52
49
|
super(0, 3)
|
@@ -58,7 +55,6 @@ class ProgrammeTableWidget < Qt::TableWidget
|
|
58
55
|
self.sortingEnabled = true
|
59
56
|
sortByColumn(2, Qt::DescendingOrder )
|
60
57
|
|
61
|
-
@mediaFilter = ''
|
62
58
|
|
63
59
|
# Hash table : key column_0_item => Programme entry.
|
64
60
|
@table = Hash.new
|
@@ -82,12 +78,10 @@ class ProgrammeTableWidget < Qt::TableWidget
|
|
82
78
|
# slot : called when filterLineEdit text is changed.
|
83
79
|
#
|
84
80
|
public
|
85
|
-
|
81
|
+
slots 'filterChanged(const QString &)'
|
86
82
|
def filterChanged(text)
|
87
83
|
return unless text
|
88
84
|
|
89
|
-
text += ' ' + @mediaFilter unless @mediaFilter.empty?
|
90
|
-
|
91
85
|
regxs = text.split(/[,\s]+/).map do |w|
|
92
86
|
/#{Regexp.escape(w.strip)}/i
|
93
87
|
end
|
@@ -117,47 +111,47 @@ class ProgrammeTableWidget < Qt::TableWidget
|
|
117
111
|
|
118
112
|
protected
|
119
113
|
def contextMenuEvent(e)
|
120
|
-
|
114
|
+
prog = self[itemAt(e.pos).row]
|
121
115
|
menu = createPopup
|
122
|
-
|
116
|
+
action = menu.exec(e.globalPos)
|
117
|
+
action and execPopup(action, prog)
|
118
|
+
menu.deleteLater
|
123
119
|
end
|
124
120
|
|
125
121
|
def createPopup()
|
126
122
|
menu = Qt::Menu.new
|
123
|
+
a = menu.addAction(KDE::Icon.new('search'), i18n('Search Same Programme'))
|
124
|
+
a.setVData('searchSame@')
|
125
|
+
a = menu.addAction(KDE::Icon.new('search'), i18n('Search Same Category tags'))
|
126
|
+
a.setVData('searchSameTags@')
|
127
|
+
menu.addSeparator
|
127
128
|
insertPlayerActions(menu)
|
128
129
|
menu
|
129
130
|
end
|
130
131
|
|
131
|
-
def execPopup(
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
else
|
142
|
-
# self.method(cmd).call(item)
|
143
|
-
end
|
132
|
+
def execPopup(action, item)
|
133
|
+
$log.code { "execute : '#{action.vData}'" }
|
134
|
+
cmd, exe = action.vData.split(/@/, 2)
|
135
|
+
$log.code { "cmd(#{cmd}), exe(#{exe})" }
|
136
|
+
if cmd == 'play'
|
137
|
+
playMedia(exe, item)
|
138
|
+
elsif self.respond_to?(cmd)
|
139
|
+
self.method(cmd).call(item)
|
140
|
+
else
|
141
|
+
$log.warn { "No method #{cmd} in contextmenu." }
|
144
142
|
end
|
145
|
-
menu.deleteLater
|
146
143
|
end
|
147
144
|
|
148
145
|
def insertPlayerActions(menu)
|
149
146
|
Mime::services('.wma').each do |s|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
a.setVData('play@' + s.exec)
|
154
|
-
end
|
147
|
+
exeName = s.exec[/\w+/]
|
148
|
+
a = menu.addAction(KDE::Icon.new(exeName), 'Play with ' + exeName)
|
149
|
+
a.setVData('play@' + s.exec)
|
155
150
|
end
|
156
151
|
end
|
157
152
|
|
158
|
-
def playMedia(exe,
|
153
|
+
def playMedia(exe, prog)
|
159
154
|
begin
|
160
|
-
prog = self[item.row]
|
161
155
|
url = prog.content[UrlRegexp] # String[] method extract only 1st one.
|
162
156
|
|
163
157
|
$log.info { "episode Url : #{url}" }
|
@@ -168,7 +162,7 @@ class ProgrammeTableWidget < Qt::TableWidget
|
|
168
162
|
args = args.split(/\s+/).map do |a|
|
169
163
|
a.gsub(/%\w/, url)
|
170
164
|
end
|
171
|
-
|
165
|
+
$log.debug { "execute cmd '#{cmd}', args '#{args.inspect}'" }
|
172
166
|
proc = Qt::Process.new(self)
|
173
167
|
proc.start(cmd, args)
|
174
168
|
|
@@ -177,6 +171,15 @@ class ProgrammeTableWidget < Qt::TableWidget
|
|
177
171
|
KDE::MessageBox::information(self, i18n("There is not direct stream for this programme."))
|
178
172
|
end
|
179
173
|
end
|
174
|
+
|
175
|
+
signals 'filterRequest(const QString &)'
|
176
|
+
def searchSame(prog)
|
177
|
+
emit filterRequest( prog.title.sub(/:.*/, '') )
|
178
|
+
end
|
179
|
+
|
180
|
+
def searchSameTags(prog)
|
181
|
+
emit filterRequest( prog.categories )
|
182
|
+
end
|
180
183
|
end
|
181
184
|
|
182
185
|
|
data/lib/taskwin.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#-------------------------------------------------------------------------------------------
|
2
2
|
#
|
3
3
|
# Task Window
|
4
4
|
#
|
@@ -107,7 +107,6 @@ class TaskWindow < Qt::Widget
|
|
107
107
|
# context menu : right click popup menu.
|
108
108
|
protected
|
109
109
|
def contextMenuEvent(e)
|
110
|
-
$log.misc { "right button is clicked." }
|
111
110
|
wItem = itemAt(e.pos)
|
112
111
|
if wItem
|
113
112
|
openContextPopup(e.globalPos, wItem)
|
@@ -121,7 +120,6 @@ class TaskWindow < Qt::Widget
|
|
121
120
|
def openContextPopup(pos, wItem)
|
122
121
|
poRow = wItem.row
|
123
122
|
poColumn = wItem.column
|
124
|
-
$log.misc { "right clicked item (row:#{poRow}, column:#{poColumn})" }
|
125
123
|
process = taskItemAtRow(wItem.row).process
|
126
124
|
|
127
125
|
menu = Qt::Menu.new
|
@@ -182,7 +180,9 @@ class TaskWindow < Qt::Widget
|
|
182
180
|
if process.rawDownloaded? then
|
183
181
|
createPlayers(menu, i18n('Play File with'), "playMP3@", '.mp3')
|
184
182
|
end
|
185
|
-
|
183
|
+
if !process.finished? or File.exist?(process.rawFilePath) then
|
184
|
+
createPlayers(menu, i18n('Play Temp File with'), "playTemp@", '.wma')
|
185
|
+
end
|
186
186
|
end
|
187
187
|
end
|
188
188
|
|
@@ -250,9 +250,10 @@ class TaskWindow < Qt::Widget
|
|
250
250
|
# contextMenu Event
|
251
251
|
def removeTaskData(process, wItem)
|
252
252
|
if process.running? then
|
253
|
-
process.
|
254
|
-
$log.info { "task and data removed." }
|
253
|
+
process.cancelTask
|
255
254
|
end
|
255
|
+
process.removeData
|
256
|
+
$log.info { "task and data removed." }
|
256
257
|
ti = taskItemAtRow(wItem.row)
|
257
258
|
deleteItem(ti)
|
258
259
|
end
|
@@ -294,6 +295,7 @@ class TaskWindow < Qt::Widget
|
|
294
295
|
def initialize(text)
|
295
296
|
super(text)
|
296
297
|
self.flags = Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled
|
298
|
+
self.toolTip = text
|
297
299
|
end
|
298
300
|
end
|
299
301
|
|
data/resources/bbcstyle.qss
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: irecorder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
11
11
|
platform: linux
|
12
12
|
authors:
|
13
13
|
- ruby.twiddler
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-09-02 00:00:00 +09:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -66,7 +66,7 @@ files:
|
|
66
66
|
- Rakefile
|
67
67
|
- resources/bbcstyle.qss
|
68
68
|
has_rdoc: true
|
69
|
-
homepage: http://
|
69
|
+
homepage: http://github.com/rubytwiddler/irecorder/wiki
|
70
70
|
licenses:
|
71
71
|
- MIT-LICENSE
|
72
72
|
post_install_message:
|
@@ -96,6 +96,7 @@ requirements:
|
|
96
96
|
- korundum4
|
97
97
|
- qtwebkit
|
98
98
|
- kio
|
99
|
+
- ktexteditor
|
99
100
|
rubyforge_project:
|
100
101
|
rubygems_version: 1.3.7
|
101
102
|
signing_key:
|