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 Util::verifyCclivePath(
198 catch (const NoCcliveException
& x
) {
199 QMessageBox::warning(this, tr("Warning"), x
.what());
206 MainWindow::ccliveSupportsHost(const QString
& lnk
) {
208 const QString host
= QUrl(lnk
).host();
210 for (QStringMap::const_iterator iter
= hosts
.begin();
211 iter
!= hosts
.end(); ++iter
)
213 QRegExp
re( iter
.key());
215 if (re
.indexIn(host
) != -1)
223 MainWindow::updateWidgets(const bool updateCcliveDepends
) {
224 // Enable widgets based on preferences and other settings.
227 s
= prefs
->streamEdit
->text();
228 streamBox
->setEnabled(!s
.isEmpty());
230 streamBox
->setCheckState(Qt::Unchecked
);
232 s
= prefs
->commandEdit
->text();
233 commandBox
->setEnabled(!s
.isEmpty());
235 commandBox
->setCheckState(Qt::Unchecked
);
237 if (updateCcliveDepends
) {
238 // The most time consuming check is to run (c)clive.
239 // Run it only when we cannot work around it.
241 regexpLabel
->show();
244 cclassLabel
->hide();
248 regexpLabel
->hide();
251 cclassLabel
->show();
258 MainWindow::closeEvent(QCloseEvent
*event
) {
264 MainWindow::writeSettings() {
266 s
.beginGroup("MainWindow");
267 s
.setValue("size", size());
268 s
.setValue("pos", pos());
269 s
.setValue("regexpEdit", regexpEdit
->text());
270 s
.setValue("findallBox", findallBox
->checkState());
271 s
.setValue("cclassEdit", cclassEdit
->text());
272 s
.setValue("fnamefmtEdit", fnamefmtEdit
->text());
277 MainWindow::readSettings() {
279 s
.beginGroup("MainWindow");
280 resize( s
.value("size", QSize(525,265)).toSize() );
281 move( s
.value("pos", QPoint(200,200)).toPoint() );
282 regexpEdit
->setText( s
.value("regexpEdit").toString() );
283 findallBox
->setCheckState(
284 s
.value("findallBox").toBool()
287 cclassEdit
->setText( s
.value("cclassEdit").toString() );
288 fnamefmtEdit
->setText( s
.value("fnamefmtEdit").toString() );
293 MainWindow::updateLog(const QString
& newText
) {
294 QString text
= logEdit
->toPlainText() + newText
;
295 logEdit
->setPlainText(text
);
302 MainWindow::onPreferences() {
304 QString old
= prefs
->ccliveEdit
->text();
308 QString _new
= prefs
->ccliveEdit
->text();
311 if (parseCcliveVersionOutput())
312 parseCcliveHostsOutput();
315 updateWidgets(old
!= _new
);
321 MainWindow::setProxy() {
322 if (!prefs
->proxyEdit
->text().isEmpty()
323 && prefs
->proxyCombo
->currentIndex() > 0)
325 QUrl
url(prefs
->proxyEdit
->text());
328 QNetworkProxy::HttpProxy
,
333 QNetworkProxy::setApplicationProxy(proxy
);
336 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy
);
340 MainWindow::onStreamStateChanged(int state
) {
341 streamSpin
->setEnabled(state
!= 0);
345 MainWindow::onStart() {
347 if ( ccliveVersion
.isEmpty() ) {
348 critCcliveNotSpecified(this);
353 if (linksList
->count() == 0) {
355 if (linksList
->count() == 0)
359 QString path
= prefs
->ccliveEdit
->text();
361 // Check video save directory
363 // cclive has no option for this but we can work around it by
364 // changing the current working directory.
366 QString savedir
= prefs
->savedirEdit
->text();
367 if (savedir
.isEmpty()) {
368 QMessageBox::information(this, QCoreApplication::applicationName(),
369 tr("Please define a directory for downloaded videos."));
374 process
.setWorkingDirectory(savedir
);
376 // Construct cclive/clive args
380 args
<< "--print-fname" << "--continue";
382 QString s
= prefs
->streamEdit
->text();
383 if (!s
.isEmpty() && streamBox
->isChecked()) {
384 args
<< QString("--stream-exec=%1").arg(s
);
385 args
<< QString("--stream=%1").arg(streamSpin
->value());
388 s
= prefs
->commandEdit
->text();
389 if (!s
.isEmpty() && commandBox
->isChecked()) {
390 if (!s
.endsWith(";"))
392 args
<< QString("--exec=%1").arg(s
);
393 args
<< QString("--exec-run");
396 if (prefs
->proxyCombo
->currentIndex() == 0)
397 args
<< "--no-proxy";
399 s
= prefs
->proxyEdit
->text();
401 args
<< QString("--proxy=%1").arg(s
);
404 if (prefs
->limitBox
->checkState()) {
405 int n
= prefs
->limitSpin
->value();
406 args
<< QString("--limit-rate=%1").arg(n
);
409 if (prefs
->timeoutBox
->checkState()) {
410 int n
= prefs
->timeoutSpin
->value();
411 if (!prefs
->socksBox
->checkState())
412 args
<< QString("--connect-timeout=%1").arg(n
);
414 args
<< QString("--connect-timeout-socks=%1").arg(n
);
420 s
= regexpEdit
->text();
422 args
<< QString("--regexp=%1").arg(s
);
423 if (findallBox
->checkState())
424 args
<< QString("--find-all");
429 // Set environment variables for clive
430 env
<< "COLUMNS=80" << "LINES=24" // Term::ReadKey
431 << QString("HOME=%1").arg(QDir::homePath()) // $env{HOME}
432 << "CCLIVE_NO_CONFIG=1"; // cclive 0.5.0+
434 s
= cclassEdit
->text();
436 args
<< QString("--cclass=%1").arg(s
);
439 s
= fnamefmtEdit
->text();
441 args
<< QString("--filename-format=%1").arg(s
);
443 // Check if all video page links are of the same host.
445 QUrl
first(linksList
->item(0)->text());
448 const register _uint count
= linksList
->count();
450 for (register _uint i
=0; i
<count
; ++i
) {
452 QUrl
url(linksList
->item(i
)->text());
454 if (url
.host() != first
.host()) {
462 // Use format dialog setting for the host.
464 s
= format
->getFormatSetting(first
.host());
466 args
<< QString("--format=%1").arg(s
);
468 for (register _uint i
=0; i
<count
; ++i
)
469 args
<< QString("%1").arg(linksList
->item(i
)->text());
471 totalProgressbar
->setMaximum(linksList
->count());
476 updateLog("% " +path
+ " " +args
.join(" ")+ "\n");
478 // And finally start the process
480 cancelledFlag
= false;
481 process
.setEnvironment(env
);
482 process
.setProcessChannelMode(QProcess::MergedChannels
);
483 process
.start(path
,args
);
487 MainWindow::onCancel() {
488 cancelledFlag
= true;
493 MainWindow::onAbout() {
494 AboutDialog(this, ccliveVersion
, curlMod
, curlVersion
).exec();
497 #define fillList(dlg) \
499 QTreeWidgetItemIterator iter(dlg->itemsTree); \
501 if ((*iter)->checkState(0) == Qt::Checked) \
502 addPageLink((*iter)->text(1)); \
508 MainWindow::onRSS() {
509 if (rss
->exec() == QDialog::Accepted
) {
512 rss
->writeSettings();
516 MainWindow::onScan() {
517 if (scan
->exec() == QDialog::Accepted
) {
520 scan
->writeSettings();
524 MainWindow::onPasteURL() {
526 QClipboard
*cb
= QApplication::clipboard();
527 QStringList lst
= cb
->text().split("\n");
528 const register _uint size
= lst
.size();
530 for (register _uint i
=0; i
<size
; ++i
)
535 MainWindow::onAdd() {
536 addPageLink(QInputDialog::getText(this,
537 QCoreApplication::applicationName(), tr("Add link:")));
541 MainWindow::onRemove() {
543 QList
<QListWidgetItem
*> sel
= linksList
->selectedItems();
548 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
549 tr("Really remove the selected links?"),
550 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
556 const register _uint size
= sel
.size();
558 for (register _uint i
=0; i
<size
; ++i
) {
559 const int row
= linksList
->row(sel
[i
]);
560 delete linksList
->takeItem(row
);
565 MainWindow::onClear() {
567 if (linksList
->count() == 0)
570 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
571 tr("Really clear list?"),
572 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
582 MainWindow::addPageLink(QString lnk
) {
589 if (!lnk
.startsWith("http://", Qt::CaseInsensitive
))
590 lnk
.insert(0,"http://");
592 if (!ccliveSupportsHost(lnk
)) {
593 QMessageBox::critical(this, QCoreApplication::applicationName(),
594 QString(tr("%1: unsupported website")).arg(QUrl(lnk
).host()));
598 QList
<QListWidgetItem
*> found
599 = linksList
->findItems(lnk
, Qt::MatchExactly
);
601 if (found
.size() == 0)
602 linksList
->addItem(lnk
);
606 MainWindow::onFormats() {
607 if ( hosts
.isEmpty() || ccliveVersion
.isEmpty() ) {
608 critCcliveNotSpecified(this);
614 format
->saveCurrent();
615 format
->writeSettings();
619 MainWindow::onProcStarted() {
620 statusBar() ->clearMessage();
621 fileLabel
->setText("-");
622 sizeLabel
->setText("-- / --");
623 rateLabel
->setText("--.-");
624 etaLabel
->setText("--:--");
625 progressBar
->setValue(0);
626 totalProgressbar
->setValue(0);
628 addButton
->setEnabled(false);
629 pasteButton
->setEnabled(false);
630 removeButton
->setEnabled(false);
631 clearButton
->setEnabled(false);
632 rssButton
->setEnabled(false);
633 scanButton
->setEnabled(false);
634 startButton
->setEnabled(false);
635 cancelButton
->setEnabled(true);
637 action_Download
->setEnabled(false);
639 action_Link
->setEnabled(false);
640 action_RSS
->setEnabled(false);
641 action_Scan
->setEnabled(false);
642 action_Paste
->setEnabled(false);
644 action_Remove
->setEnabled(false);
645 action_Clear
->setEnabled(false);
647 linksList
->setEnabled(false);
649 tabWidget
->setTabEnabled(1, false);
653 MainWindow::onProcError(QProcess::ProcessError err
) {
654 if (err
== QProcess::FailedToStart
) {
655 QString msg
= tr("Error: Failed to start the process.");
656 statusBar()->showMessage(msg
);
662 MainWindow::onProcStdoutReady() {
664 // NOTE: We read both channels stdout and stderr.
667 memset(&data
, 0, sizeof(data
));
669 QStatusBar
*sb
= statusBar();
670 bool appendLogFlag
= true;
672 while (process
.readLine(data
, sizeof(data
))) {
674 appendLogFlag
= true;
676 QString ln
= QString::fromLocal8Bit(data
);
679 if (ln
.startsWith("fetch http://")) {
680 sb
->showMessage( tr("Fetching ...") );
681 totalProgressbar
->setValue( totalProgressbar
->value()+1 );
684 else if (ln
.startsWith("verify"))
685 sb
->showMessage( tr("Verifying link ...") );
687 else if (ln
.startsWith("file:")) {
688 QRegExp
re("file: (.*) (\\d+.\\d+)M");
690 fileLabel
->setText( re
.capturedTexts()[1].simplified() );
691 sb
->showMessage( tr("Downloading video ...") );
694 else if (ln
.startsWith("error:")) {
700 appendLogFlag
= false;
702 // In some parallel world I have written a cleaner regexp.
703 static const char progress_re
[] =
705 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
706 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
709 QRegExp
re(progress_re
);
711 if (re
.indexIn(ln
)) {
712 QStringList cap
= re
.capturedTexts();
716 if (cap
[0].isEmpty())
733 progressBar
->setValue( cap
[PERCENT
].toInt() );
735 sizeLabel
->setText(QString("%1.%2M / %3.%4M")
736 .arg(cap
[SIZE_NOW_X
])
737 .arg(cap
[SIZE_NOW_Y
])
738 .arg(cap
[SIZE_EXPECTED_X
])
739 .arg(cap
[SIZE_EXPECTED_Y
]));
741 rateLabel
->setText(QString("%1.%2%3/s")
744 .arg(cap
[SPEED_TYPE
]));
746 etaLabel
->setText(cap
[ETA
].simplified());
751 updateLog(ln
+ "\n");
753 memset(&data
, 0, sizeof(data
));
758 MainWindow::onProcFinished(int exitCode
, QProcess::ExitStatus exitStatus
) {
762 switch (exitStatus
) {
763 case QProcess::NormalExit
:
766 status
= tr("c/clive exited normally.");
770 QString(tr("c/clive exited with code %1, see log."))
776 status
= cancelledFlag
777 ? tr("c/clive terminated by user.")
778 : tr("c/clive crashed, see log.");
783 statusBar()->showMessage(status
);
785 addButton
->setEnabled(true);
786 pasteButton
->setEnabled(true);
787 removeButton
->setEnabled(true);
788 clearButton
->setEnabled(true);
789 rssButton
->setEnabled(true);
790 scanButton
->setEnabled(true);
791 startButton
->setEnabled(true);
792 cancelButton
->setEnabled(false);
794 action_Download
->setEnabled(true);
796 action_Link
->setEnabled(true);
797 action_RSS
->setEnabled(true);
798 action_Scan
->setEnabled(true);
799 action_Paste
->setEnabled(true);
801 action_Remove
->setEnabled(true);
802 action_Clear
->setEnabled(true);
804 linksList
->setEnabled(true);
806 tabWidget
->setTabEnabled(1, true);
810 MainWindow::onItemDoubleClicked(QListWidgetItem
*item
) {
813 QString lnk
= QInputDialog::getText(this,
814 QCoreApplication::applicationName(), tr("Edit link:"),
815 QLineEdit::Normal
, item
->text(), &ok
);
817 if (ok
&& !lnk
.isEmpty())