add autodetection, rewrite c/clive verification.
[abby.git] / src / mainwnd.cpp
blob5b16ac7042f9b71079c16dd76e6080908ae3d3e6
1 /*
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>
19 #include <QSettings>
20 #include <QCloseEvent>
21 #include <QMessageBox>
22 #include <QFileDialog>
23 #include <QClipboard>
24 //#include <QDebug>
25 #include <QInputDialog>
26 #include <QNetworkProxy>
27 #include <QRegExp>
28 #include <QMap>
30 #include "mainwnd.h"
31 #include "prefsdlg.h"
32 #include "rssdlg.h"
33 #include "scandlg.h"
34 #include "formatdlg.h"
35 #include "aboutdlg.h"
36 #include "util.h"
38 #define critCcliveProcessFailed(parent, msg) \
39 do { \
40 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
41 QString( tr("Error while trying to run c/clive:\n%1") ).arg(msg)); \
42 } while (0)
44 #define critCcliveNotSpecified(parent) \
45 do { \
46 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
47 QString( tr("c/clive executable not found, please check the path.") )); \
48 } while (0)
50 #define critCcliveExitedWithError(parent,code,msg) \
51 do { \
52 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
53 QString( tr("c/clive exited with error code %1:\n%2") ) \
54 .arg(code).arg(msg)); \
55 } while (0)
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");
71 setupUi(this);
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);
79 // Settings.
80 readSettings();
81 setProxy();
83 // Process.
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) ));
100 // Misc.
101 connect(linksList, SIGNAL( itemDoubleClicked(QListWidgetItem *) ),
102 this, SLOT( onItemDoubleClicked(QListWidgetItem *) ));
104 // Parse.
105 if (parseCcliveVersionOutput())
106 parseCcliveHostsOutput();
108 // Widget voodoo.
109 updateWidgets (true);
111 #ifdef WIN32
112 streamBox ->setHidden(true);
113 streamSpin->setHidden(true);
114 #endif
117 bool
118 MainWindow::parseCcliveHostsOutput() {
119 hosts.clear();
121 QString path = prefs->ccliveEdit->text();
123 if (path.isEmpty()) {
125 QMessageBox::information(
126 this,
127 QCoreApplication::applicationName(),
128 tr("abby requires `clive' or `cclive'. "
129 "Please define path to either executable.")
132 onPreferences();
134 return false;
137 QProcess proc;
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() );
146 return false;
148 else {
149 const QString output =
150 QString::fromLocal8Bit( proc.readAll() );
152 const int exitCode =
153 proc.exitCode();
155 if (exitCode == 0) {
157 QStringList lst =
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);
174 else {
175 critCcliveExitedWithError(this, exitCode, output);
176 return false;
180 return true;
183 bool
184 MainWindow::parseCcliveVersionOutput() {
186 QString path =
187 prefs->ccliveEdit->text();
189 try {
190 if (path.isEmpty()) {
191 Util::detectCclive(
192 path,
193 ccliveVersion,
194 curlVersion,
195 curlMod,
196 &isCcliveFlag
198 if (!path.isEmpty()) {
199 prefs->ccliveEdit->setText(path);
200 return true;
204 Util::verifyCclivePath(
205 path,
206 ccliveVersion,
207 curlVersion,
208 curlMod,
209 &isCcliveFlag
212 catch (const NoCcliveException& x) {
213 QMessageBox::warning(this, tr("Warning"), x.what());
214 return false;
216 return true;
219 bool
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)
230 return true;
233 return false;
236 void
237 MainWindow::updateWidgets(const bool updateCcliveDepends) {
238 // Enable widgets based on preferences and other settings.
239 QString s;
241 s = prefs->streamEdit->text();
242 streamBox->setEnabled(!s.isEmpty());
243 if (s.isEmpty())
244 streamBox->setCheckState(Qt::Unchecked);
246 s = prefs->commandEdit->text();
247 commandBox->setEnabled(!s.isEmpty());
248 if (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.
254 if (isCcliveFlag) {
255 regexpLabel ->show();
256 regexpEdit ->show();
257 findallBox ->show();
258 cclassLabel ->hide();
259 cclassEdit ->hide();
261 else {
262 regexpLabel ->hide();
263 regexpEdit ->hide();
264 findallBox ->hide();
265 cclassLabel ->show();
266 cclassEdit ->show();
271 void
272 MainWindow::closeEvent(QCloseEvent *event) {
273 int rc = QMessageBox::Yes;
274 if (process.state() != QProcess::NotRunning) {
275 rc = QMessageBox::warning(
276 this,
277 tr("Warning"),
278 tr("c/clive process is still active, really close abby?"),
279 QMessageBox::Yes|QMessageBox::No
283 if (rc == QMessageBox::Yes) {
284 process.kill();
285 writeSettings();
286 event->accept();
288 else
289 event->ignore();
292 void
293 MainWindow::writeSettings() {
294 QSettings s;
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());
302 s.endGroup();
305 void
306 MainWindow::readSettings() {
307 QSettings s;
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()
314 ? Qt::Checked
315 : Qt::Unchecked);
316 cclassEdit->setText( s.value("cclassEdit").toString() );
317 fnamefmtEdit->setText( s.value("fnamefmtEdit").toString() );
318 s.endGroup();
321 void
322 MainWindow::updateLog(const QString& newText) {
323 QString text = logEdit->toPlainText() + newText;
324 logEdit->setPlainText(text);
328 // Slots
330 void
331 MainWindow::onPreferences() {
333 QString old = prefs->ccliveEdit->text();
335 prefs->exec();
337 QString _new = prefs->ccliveEdit->text();
339 if (old != _new) {
340 if (parseCcliveVersionOutput())
341 parseCcliveHostsOutput();
344 updateWidgets(old != _new);
346 setProxy();
349 void
350 MainWindow::setProxy() {
351 if (!prefs->proxyEdit->text().isEmpty()
352 && prefs->proxyCombo->currentIndex() > 0)
354 QUrl url(prefs->proxyEdit->text());
356 QNetworkProxy proxy(
357 QNetworkProxy::HttpProxy,
358 url.host(),
359 url.port()
362 QNetworkProxy::setApplicationProxy(proxy);
364 else
365 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
368 void
369 MainWindow::onStreamStateChanged(int state) {
370 streamSpin->setEnabled(state != 0);
373 void
374 MainWindow::onStart() {
376 if ( ccliveVersion.isEmpty() ) {
377 critCcliveNotSpecified(this);
378 onPreferences();
379 return;
382 if (linksList->count() == 0) {
383 onAdd();
384 if (linksList->count() == 0)
385 return;
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."));
399 onPreferences();
400 return;
403 process.setWorkingDirectory(savedir);
405 // Construct cclive/clive args
407 QStringList 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(";"))
420 s += ";";
421 args << QString("--exec=%1").arg(s);
422 args << QString("--exec-run");
425 if (prefs->proxyCombo->currentIndex() == 0)
426 args << "--no-proxy";
427 else {
428 s = prefs->proxyEdit->text();
429 if (!s.isEmpty())
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);
442 else
443 args << QString("--connect-timeout-socks=%1").arg(n);
446 QStringList env;
448 if (isCcliveFlag) {
449 s = regexpEdit->text();
450 if (!s.isEmpty())
451 args << QString("--regexp=%1").arg(s);
452 if (findallBox->checkState())
453 args << QString("--find-all");
454 } else {
456 args << "--stderr";
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();
464 if (!s.isEmpty())
465 args << QString("--cclass=%1").arg(s);
468 s = fnamefmtEdit->text();
469 if (!s.isEmpty())
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());
476 bool allSame = true;
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()) {
484 allSame = false;
485 break;
489 s = "flv";
491 // Use format dialog setting for the host.
492 if (allSame)
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());
502 // Prepare log
504 logEdit->clear();
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);
515 void
516 MainWindow::onCancel() {
517 cancelledFlag = true;
518 process.kill();
521 void
522 MainWindow::onAbout() {
523 AboutDialog(this, ccliveVersion, curlMod, curlVersion).exec();
526 #define fillList(dlg) \
527 do { \
528 QTreeWidgetItemIterator iter(dlg->itemsTree); \
529 while (*iter) { \
530 if ((*iter)->checkState(0) == Qt::Checked) \
531 addPageLink((*iter)->text(1)); \
532 ++iter; \
534 } while (0)
536 void
537 MainWindow::onRSS() {
538 if (rss->exec() == QDialog::Accepted) {
539 fillList(rss);
541 rss->writeSettings();
544 void
545 MainWindow::onScan() {
546 if (scan->exec() == QDialog::Accepted) {
547 fillList(scan);
549 scan->writeSettings();
552 void
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)
560 addPageLink(lst[i]);
563 void
564 MainWindow::onAdd() {
565 addPageLink(QInputDialog::getText(this,
566 QCoreApplication::applicationName(), tr("Add link:")));
569 void
570 MainWindow::onRemove() {
572 QList<QListWidgetItem*> sel = linksList->selectedItems();
574 if (sel.size() == 0)
575 return;
577 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
578 tr("Really remove the selected links?"),
579 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
580 == QMessageBox::No)
582 return;
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);
593 void
594 MainWindow::onClear() {
596 if (linksList->count() == 0)
597 return;
599 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
600 tr("Really clear list?"),
601 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
602 == QMessageBox::No)
604 return;
607 linksList->clear();
610 void
611 MainWindow::addPageLink(QString lnk) {
613 lnk = lnk.trimmed();
615 if (lnk.isEmpty())
616 return;
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()));
624 return;
627 QList<QListWidgetItem *> found
628 = linksList->findItems(lnk, Qt::MatchExactly);
630 if (found.size() == 0)
631 linksList->addItem(lnk);
634 void
635 MainWindow::onFormats() {
636 if ( hosts.isEmpty() || ccliveVersion.isEmpty() ) {
637 critCcliveNotSpecified(this);
638 onPreferences();
639 return;
642 format->exec();
643 format->saveCurrent();
644 format->writeSettings();
647 void
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);
681 void
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);
686 updateLog(msg);
690 void
691 MainWindow::onProcStdoutReady() {
693 // NOTE: We read both channels stdout and stderr.
695 char data[1024];
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);
706 ln.remove("\n");
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");
718 re.indexIn(ln);
719 fileLabel->setText( re.capturedTexts()[1].simplified() );
720 sb->showMessage( tr("Downloading video ...") );
723 else if (ln.startsWith("error:")) {
724 // Log it.
727 else {
729 appendLogFlag = false;
731 // In some parallel world I have written a cleaner regexp.
732 static const char progress_re[] =
733 "(\\d+)%" // percent
734 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
735 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
736 "\\s+(.*)"; // eta
738 QRegExp re(progress_re);
740 if (re.indexIn(ln)) {
741 QStringList cap = re.capturedTexts();
743 cap.removeFirst();
745 if (cap[0].isEmpty())
746 continue;
748 //qDebug() << cap;
750 enum {
751 PERCENT = 0,
752 SIZE_NOW_X,
753 SIZE_NOW_Y,
754 SIZE_EXPECTED_X,
755 SIZE_EXPECTED_Y,
756 SPEED_X,
757 SPEED_Y,
758 SPEED_TYPE,
759 ETA,
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")
771 .arg(cap[SPEED_X])
772 .arg(cap[SPEED_Y])
773 .arg(cap[SPEED_TYPE]));
775 etaLabel ->setText(cap[ETA].simplified());
779 if (appendLogFlag)
780 updateLog(ln + "\n");
782 memset(&data, 0, sizeof(data));
786 void
787 MainWindow::onProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
789 QString status;
791 switch (exitStatus) {
792 case QProcess::NormalExit:
793 switch (exitCode) {
794 case 0:
795 status = tr("c/clive exited normally.");
796 break;
797 default:
798 status =
799 QString(tr("c/clive exited with code %1, see log."))
800 .arg(exitCode);
801 break;
803 break;
804 default:
805 status = cancelledFlag
806 ? tr("c/clive terminated by user.")
807 : tr("c/clive crashed, see log.");
808 break;
811 updateLog(status);
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);
838 void
839 MainWindow::onItemDoubleClicked(QListWidgetItem *item) {
840 bool ok;
842 QString lnk = QInputDialog::getText(this,
843 QCoreApplication::applicationName(), tr("Edit link:"),
844 QLineEdit::Normal, item->text(), &ok);
846 if (ok && !lnk.isEmpty())
847 item->setText(lnk);