irecorder 0.0.5-linux → 0.0.6-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|