2 * abby Copyright (C) 2009 Toni Gundogdu.
3 * This file is part of abby.
5 * abby is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * abby is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 #include <QMainWindow>
20 #include <QCloseEvent>
21 #include <QMessageBox>
22 #include <QFileDialog>
25 #include <QInputDialog>
26 #include <QNetworkProxy>
34 #include "formatdlg.h"
38 #define critCcliveProcessFailed(parent, msg) \
40 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
41 QString( tr("Error while trying to run c/clive:\n%1") ).arg(msg)); \
44 #define critCcliveNotSpecified(parent) \
46 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
47 QString( tr("c/clive executable not found, please check the path.") )); \
50 #define critCcliveExitedWithError(parent,code,msg) \
52 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
53 QString( tr("c/clive exited with error code %1:\n%2") ) \
54 .arg(code).arg(msg)); \
58 typedef unsigned int _uint
;
60 MainWindow::MainWindow()
61 : cancelledFlag(false), isCcliveFlag(false)
64 The word "English" is not meant to be translated literally.
65 Instead, replace "English" with the target translation language,
66 e.g. "Suomi", "Deutch", etc. abby uses this word in the
67 preferences dialog to select current language.
69 const QString lang
= tr("English");
73 // Dialogs. Be extravagant about system memory.
74 prefs
= new PreferencesDialog (this);
75 rss
= new RSSDialog (this);
76 scan
= new ScanDialog (this);
77 format
= new FormatDialog (this);
84 connect(&process
, SIGNAL( started() ),
85 this, SLOT( onProcStarted() ));
87 connect(&process
, SIGNAL( error(QProcess::ProcessError
) ),
88 this, SLOT( onProcError(QProcess::ProcessError
) ));
90 // NOTE: Merge stdout/stderr from c/clive
91 connect(&process
, SIGNAL( readyReadStandardOutput() ),
92 this, SLOT( onProcStdoutReady() ));
94 connect(&process
, SIGNAL( readyReadStandardError() ),
95 this, SLOT( onProcStdoutReady() ));
97 connect(&process
, SIGNAL( finished(int, QProcess::ExitStatus
) ),
98 this, SLOT( onProcFinished(int, QProcess::ExitStatus
) ));
101 connect(linksList
, SIGNAL( itemDoubleClicked(QListWidgetItem
*) ),
102 this, SLOT( onItemDoubleClicked(QListWidgetItem
*) ));
105 if (parseCcliveVersionOutput())
106 parseCcliveHostsOutput();
109 updateWidgets (true);
112 streamBox
->setHidden(true);
113 streamSpin
->setHidden(true);
118 MainWindow::parseCcliveHostsOutput() {
121 QString path
= prefs
->ccliveEdit
->text();
123 if (path
.isEmpty()) {
125 QMessageBox::information(
127 QCoreApplication::applicationName(),
128 tr("abby requires `clive' or `cclive'. "
129 "Please define path to either executable.")
138 proc
.setEnvironment(QStringList() << "CCLIVE_NO_CONFIG=1");
139 proc
.setProcessChannelMode(QProcess::MergedChannels
);
140 proc
.start(path
, QStringList() << "--hosts");
142 format
->resetHosts();
144 if (!proc
.waitForFinished()) {
145 critCcliveProcessFailed(this, proc
.errorString() );
149 const QString output
=
150 QString::fromLocal8Bit( proc
.readAll() );
158 output
.split("\n", QString::SkipEmptyParts
);
160 lst
.removeLast(); // The note line.
162 const register _uint size
= lst
.size();
163 for (register _uint i
=0; i
<size
; ++i
) {
165 QString ln
= lst
[i
].remove("\r");
166 QStringList tmp
= ln
.split("\t");
168 if (!tmp
[0].isEmpty() && !tmp
[1].isEmpty())
169 hosts
[tmp
[0]] = tmp
[1];
172 format
->parseHosts(hosts
);
175 critCcliveExitedWithError(this, exitCode
, output
);
184 MainWindow::parseCcliveVersionOutput() {
187 prefs
->ccliveEdit
->text();
190 if (path
.isEmpty()) {
198 if (!path
.isEmpty()) {
199 prefs
->ccliveEdit
->setText(path
);
204 Util::verifyCclivePath(
212 catch (const NoCcliveException
& x
) {
213 QMessageBox::warning(this, tr("Warning"), x
.what());
220 MainWindow::ccliveSupportsHost(const QString
& lnk
) {
222 const QString host
= QUrl(lnk
).host();
224 for (QStringMap::const_iterator iter
= hosts
.begin();
225 iter
!= hosts
.end(); ++iter
)
227 QRegExp
re( iter
.key());
229 if (re
.indexIn(host
) != -1)
237 MainWindow::updateWidgets(const bool updateCcliveDepends
) {
238 // Enable widgets based on preferences and other settings.
241 s
= prefs
->streamEdit
->text();
242 streamBox
->setEnabled(!s
.isEmpty());
244 streamBox
->setCheckState(Qt::Unchecked
);
246 s
= prefs
->commandEdit
->text();
247 commandBox
->setEnabled(!s
.isEmpty());
249 commandBox
->setCheckState(Qt::Unchecked
);
251 if (updateCcliveDepends
) {
252 // The most time consuming check is to run (c)clive.
253 // Run it only when we cannot work around it.
255 regexpLabel
->show();
258 cclassLabel
->hide();
262 regexpLabel
->hide();
265 cclassLabel
->show();
272 MainWindow::closeEvent(QCloseEvent
*event
) {
273 int rc
= QMessageBox::Yes
;
274 if (process
.state() != QProcess::NotRunning
) {
275 rc
= QMessageBox::warning(
278 tr("c/clive process is still active, really close abby?"),
279 QMessageBox::Yes
|QMessageBox::No
283 if (rc
== QMessageBox::Yes
) {
293 MainWindow::writeSettings() {
295 s
.beginGroup("MainWindow");
296 s
.setValue("size", size());
297 s
.setValue("pos", pos());
298 s
.setValue("regexpEdit", regexpEdit
->text());
299 s
.setValue("findallBox", findallBox
->checkState());
300 s
.setValue("cclassEdit", cclassEdit
->text());
301 s
.setValue("fnamefmtEdit", fnamefmtEdit
->text());
306 MainWindow::readSettings() {
308 s
.beginGroup("MainWindow");
309 resize( s
.value("size", QSize(525,265)).toSize() );
310 move( s
.value("pos", QPoint(200,200)).toPoint() );
311 regexpEdit
->setText( s
.value("regexpEdit").toString() );
312 findallBox
->setCheckState(
313 s
.value("findallBox").toBool()
316 cclassEdit
->setText( s
.value("cclassEdit").toString() );
317 fnamefmtEdit
->setText( s
.value("fnamefmtEdit").toString() );
322 MainWindow::updateLog(const QString
& newText
) {
323 QString text
= logEdit
->toPlainText() + newText
;
324 logEdit
->setPlainText(text
);
331 MainWindow::onPreferences() {
333 QString old
= prefs
->ccliveEdit
->text();
337 QString _new
= prefs
->ccliveEdit
->text();
340 if (parseCcliveVersionOutput())
341 parseCcliveHostsOutput();
344 updateWidgets(old
!= _new
);
350 MainWindow::setProxy() {
351 if (!prefs
->proxyEdit
->text().isEmpty()
352 && prefs
->proxyCombo
->currentIndex() > 0)
354 QUrl
url(prefs
->proxyEdit
->text());
357 QNetworkProxy::HttpProxy
,
362 QNetworkProxy::setApplicationProxy(proxy
);
365 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy
);
369 MainWindow::onStreamStateChanged(int state
) {
370 streamSpin
->setEnabled(state
!= 0);
374 MainWindow::onStart() {
376 if ( ccliveVersion
.isEmpty() ) {
377 critCcliveNotSpecified(this);
382 if (linksList
->count() == 0) {
384 if (linksList
->count() == 0)
388 QString path
= prefs
->ccliveEdit
->text();
390 // Check video save directory
392 // cclive has no option for this but we can work around it by
393 // changing the current working directory.
395 QString savedir
= prefs
->savedirEdit
->text();
396 if (savedir
.isEmpty()) {
397 QMessageBox::information(this, QCoreApplication::applicationName(),
398 tr("Please define a directory for downloaded videos."));
403 process
.setWorkingDirectory(savedir
);
405 // Construct cclive/clive args
409 args
<< "--print-fname" << "--continue";
411 QString s
= prefs
->streamEdit
->text();
412 if (!s
.isEmpty() && streamBox
->isChecked()) {
413 args
<< QString("--stream-exec=%1").arg(s
);
414 args
<< QString("--stream=%1").arg(streamSpin
->value());
417 s
= prefs
->commandEdit
->text();
418 if (!s
.isEmpty() && commandBox
->isChecked()) {
419 if (!s
.endsWith(";"))
421 args
<< QString("--exec=%1").arg(s
);
422 args
<< QString("--exec-run");
425 if (prefs
->proxyCombo
->currentIndex() == 0)
426 args
<< "--no-proxy";
428 s
= prefs
->proxyEdit
->text();
430 args
<< QString("--proxy=%1").arg(s
);
433 if (prefs
->limitBox
->checkState()) {
434 int n
= prefs
->limitSpin
->value();
435 args
<< QString("--limit-rate=%1").arg(n
);
438 if (prefs
->timeoutBox
->checkState()) {
439 int n
= prefs
->timeoutSpin
->value();
440 if (!prefs
->socksBox
->checkState())
441 args
<< QString("--connect-timeout=%1").arg(n
);
443 args
<< QString("--connect-timeout-socks=%1").arg(n
);
449 s
= regexpEdit
->text();
451 args
<< QString("--regexp=%1").arg(s
);
452 if (findallBox
->checkState())
453 args
<< QString("--find-all");
458 // Set environment variables for clive
459 env
<< "COLUMNS=80" << "LINES=24" // Term::ReadKey
460 << QString("HOME=%1").arg(QDir::homePath()) // $env{HOME}
461 << "CCLIVE_NO_CONFIG=1"; // cclive 0.5.0+
463 s
= cclassEdit
->text();
465 args
<< QString("--cclass=%1").arg(s
);
468 s
= fnamefmtEdit
->text();
470 args
<< QString("--filename-format=%1").arg(s
);
472 // Check if all video page links are of the same host.
474 QUrl
first(linksList
->item(0)->text());
477 const register _uint count
= linksList
->count();
479 for (register _uint i
=0; i
<count
; ++i
) {
481 QUrl
url(linksList
->item(i
)->text());
483 if (url
.host() != first
.host()) {
491 // Use format dialog setting for the host.
493 s
= format
->getFormatSetting(first
.host());
495 args
<< QString("--format=%1").arg(s
);
497 for (register _uint i
=0; i
<count
; ++i
)
498 args
<< QString("%1").arg(linksList
->item(i
)->text());
500 totalProgressbar
->setMaximum(linksList
->count());
505 updateLog("% " +path
+ " " +args
.join(" ")+ "\n");
507 // And finally start the process
509 cancelledFlag
= false;
510 process
.setEnvironment(env
);
511 process
.setProcessChannelMode(QProcess::MergedChannels
);
512 process
.start(path
,args
);
516 MainWindow::onCancel() {
517 cancelledFlag
= true;
522 MainWindow::onAbout() {
523 AboutDialog(this, ccliveVersion
, curlMod
, curlVersion
).exec();
526 #define fillList(dlg) \
528 QTreeWidgetItemIterator iter(dlg->itemsTree); \
530 if ((*iter)->checkState(0) == Qt::Checked) \
531 addPageLink((*iter)->text(1)); \
537 MainWindow::onRSS() {
538 if (rss
->exec() == QDialog::Accepted
) {
541 rss
->writeSettings();
545 MainWindow::onScan() {
546 if (scan
->exec() == QDialog::Accepted
) {
549 scan
->writeSettings();
553 MainWindow::onPasteURL() {
555 QClipboard
*cb
= QApplication::clipboard();
556 QStringList lst
= cb
->text().split("\n");
557 const register _uint size
= lst
.size();
559 for (register _uint i
=0; i
<size
; ++i
)
564 MainWindow::onAdd() {
565 addPageLink(QInputDialog::getText(this,
566 QCoreApplication::applicationName(), tr("Add link:")));
570 MainWindow::onRemove() {
572 QList
<QListWidgetItem
*> sel
= linksList
->selectedItems();
577 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
578 tr("Really remove the selected links?"),
579 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
585 const register _uint size
= sel
.size();
587 for (register _uint i
=0; i
<size
; ++i
) {
588 const int row
= linksList
->row(sel
[i
]);
589 delete linksList
->takeItem(row
);
594 MainWindow::onClear() {
596 if (linksList
->count() == 0)
599 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
600 tr("Really clear list?"),
601 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
611 MainWindow::addPageLink(QString lnk
) {
618 if (!lnk
.startsWith("http://", Qt::CaseInsensitive
))
619 lnk
.insert(0,"http://");
621 if (!ccliveSupportsHost(lnk
)) {
622 QMessageBox::critical(this, QCoreApplication::applicationName(),
623 QString(tr("%1: unsupported website")).arg(QUrl(lnk
).host()));
627 QList
<QListWidgetItem
*> found
628 = linksList
->findItems(lnk
, Qt::MatchExactly
);
630 if (found
.size() == 0)
631 linksList
->addItem(lnk
);
635 MainWindow::onFormats() {
636 if ( hosts
.isEmpty() || ccliveVersion
.isEmpty() ) {
637 critCcliveNotSpecified(this);
643 format
->saveCurrent();
644 format
->writeSettings();
648 MainWindow::onProcStarted() {
649 statusBar() ->clearMessage();
650 fileLabel
->setText("-");
651 sizeLabel
->setText("-- / --");
652 rateLabel
->setText("--.-");
653 etaLabel
->setText("--:--");
654 progressBar
->setValue(0);
655 totalProgressbar
->setValue(0);
657 addButton
->setEnabled(false);
658 pasteButton
->setEnabled(false);
659 removeButton
->setEnabled(false);
660 clearButton
->setEnabled(false);
661 rssButton
->setEnabled(false);
662 scanButton
->setEnabled(false);
663 startButton
->setEnabled(false);
664 cancelButton
->setEnabled(true);
666 action_Download
->setEnabled(false);
668 action_Link
->setEnabled(false);
669 action_RSS
->setEnabled(false);
670 action_Scan
->setEnabled(false);
671 action_Paste
->setEnabled(false);
673 action_Remove
->setEnabled(false);
674 action_Clear
->setEnabled(false);
676 linksList
->setEnabled(false);
678 tabWidget
->setTabEnabled(1, false);
682 MainWindow::onProcError(QProcess::ProcessError err
) {
683 if (err
== QProcess::FailedToStart
) {
684 QString msg
= tr("Error: Failed to start the process.");
685 statusBar()->showMessage(msg
);
691 MainWindow::onProcStdoutReady() {
693 // NOTE: We read both channels stdout and stderr.
696 memset(&data
, 0, sizeof(data
));
698 QStatusBar
*sb
= statusBar();
699 bool appendLogFlag
= true;
701 while (process
.readLine(data
, sizeof(data
))) {
703 appendLogFlag
= true;
705 QString ln
= QString::fromLocal8Bit(data
);
708 if (ln
.startsWith("fetch http://")) {
709 sb
->showMessage( tr("Fetching ...") );
710 totalProgressbar
->setValue( totalProgressbar
->value()+1 );
713 else if (ln
.startsWith("verify"))
714 sb
->showMessage( tr("Verifying link ...") );
716 else if (ln
.startsWith("file:")) {
717 QRegExp
re("file: (.*) (\\d+.\\d+)M");
719 fileLabel
->setText( re
.capturedTexts()[1].simplified() );
720 sb
->showMessage( tr("Downloading video ...") );
723 else if (ln
.startsWith("error:")) {
729 appendLogFlag
= false;
731 // In some parallel world I have written a cleaner regexp.
732 static const char progress_re
[] =
734 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
735 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
738 QRegExp
re(progress_re
);
740 if (re
.indexIn(ln
)) {
741 QStringList cap
= re
.capturedTexts();
745 if (cap
[0].isEmpty())
762 progressBar
->setValue( cap
[PERCENT
].toInt() );
764 sizeLabel
->setText(QString("%1.%2M / %3.%4M")
765 .arg(cap
[SIZE_NOW_X
])
766 .arg(cap
[SIZE_NOW_Y
])
767 .arg(cap
[SIZE_EXPECTED_X
])
768 .arg(cap
[SIZE_EXPECTED_Y
]));
770 rateLabel
->setText(QString("%1.%2%3/s")
773 .arg(cap
[SPEED_TYPE
]));
775 etaLabel
->setText(cap
[ETA
].simplified());
780 updateLog(ln
+ "\n");
782 memset(&data
, 0, sizeof(data
));
787 MainWindow::onProcFinished(int exitCode
, QProcess::ExitStatus exitStatus
) {
791 switch (exitStatus
) {
792 case QProcess::NormalExit
:
795 status
= tr("c/clive exited normally.");
799 QString(tr("c/clive exited with code %1, see log."))
805 status
= cancelledFlag
806 ? tr("c/clive terminated by user.")
807 : tr("c/clive crashed, see log.");
812 statusBar()->showMessage(status
);
814 addButton
->setEnabled(true);
815 pasteButton
->setEnabled(true);
816 removeButton
->setEnabled(true);
817 clearButton
->setEnabled(true);
818 rssButton
->setEnabled(true);
819 scanButton
->setEnabled(true);
820 startButton
->setEnabled(true);
821 cancelButton
->setEnabled(false);
823 action_Download
->setEnabled(true);
825 action_Link
->setEnabled(true);
826 action_RSS
->setEnabled(true);
827 action_Scan
->setEnabled(true);
828 action_Paste
->setEnabled(true);
830 action_Remove
->setEnabled(true);
831 action_Clear
->setEnabled(true);
833 linksList
->setEnabled(true);
835 tabWidget
->setTabEnabled(1, true);
839 MainWindow::onItemDoubleClicked(QListWidgetItem
*item
) {
842 QString lnk
= QInputDialog::getText(this,
843 QCoreApplication::applicationName(), tr("Edit link:"),
844 QLineEdit::Normal
, item
->text(), &ok
);
846 if (ok
&& !lnk
.isEmpty())