add "verify c/clive path" button.
[abby.git] / src / mainwnd.cpp
blobb8021e8d9e2e1bb1c7acd47bf382e67821d5fef6
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 const QString path =
187 prefs->ccliveEdit->text();
189 try {
190 Util::verifyCclivePath(
191 path,
192 ccliveVersion,
193 curlVersion,
194 curlMod,
195 &isCcliveFlag
198 catch (const NoCcliveException& x) {
199 QMessageBox::warning(this, tr("Warning"), x.what());
200 return false;
202 return true;
205 bool
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)
216 return true;
219 return false;
222 void
223 MainWindow::updateWidgets(const bool updateCcliveDepends) {
224 // Enable widgets based on preferences and other settings.
225 QString s;
227 s = prefs->streamEdit->text();
228 streamBox->setEnabled(!s.isEmpty());
229 if (s.isEmpty())
230 streamBox->setCheckState(Qt::Unchecked);
232 s = prefs->commandEdit->text();
233 commandBox->setEnabled(!s.isEmpty());
234 if (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.
240 if (isCcliveFlag) {
241 regexpLabel ->show();
242 regexpEdit ->show();
243 findallBox ->show();
244 cclassLabel ->hide();
245 cclassEdit ->hide();
247 else {
248 regexpLabel ->hide();
249 regexpEdit ->hide();
250 findallBox ->hide();
251 cclassLabel ->show();
252 cclassEdit ->show();
257 void
258 MainWindow::closeEvent(QCloseEvent *event) {
259 writeSettings();
260 event->accept();
263 void
264 MainWindow::writeSettings() {
265 QSettings s;
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());
273 s.endGroup();
276 void
277 MainWindow::readSettings() {
278 QSettings s;
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()
285 ? Qt::Checked
286 : Qt::Unchecked);
287 cclassEdit->setText( s.value("cclassEdit").toString() );
288 fnamefmtEdit->setText( s.value("fnamefmtEdit").toString() );
289 s.endGroup();
292 void
293 MainWindow::updateLog(const QString& newText) {
294 QString text = logEdit->toPlainText() + newText;
295 logEdit->setPlainText(text);
299 // Slots
301 void
302 MainWindow::onPreferences() {
304 QString old = prefs->ccliveEdit->text();
306 prefs->exec();
308 QString _new = prefs->ccliveEdit->text();
310 if (old != _new) {
311 if (parseCcliveVersionOutput())
312 parseCcliveHostsOutput();
315 updateWidgets(old != _new);
317 setProxy();
320 void
321 MainWindow::setProxy() {
322 if (!prefs->proxyEdit->text().isEmpty()
323 && prefs->proxyCombo->currentIndex() > 0)
325 QUrl url(prefs->proxyEdit->text());
327 QNetworkProxy proxy(
328 QNetworkProxy::HttpProxy,
329 url.host(),
330 url.port()
333 QNetworkProxy::setApplicationProxy(proxy);
335 else
336 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
339 void
340 MainWindow::onStreamStateChanged(int state) {
341 streamSpin->setEnabled(state != 0);
344 void
345 MainWindow::onStart() {
347 if ( ccliveVersion.isEmpty() ) {
348 critCcliveNotSpecified(this);
349 onPreferences();
350 return;
353 if (linksList->count() == 0) {
354 onAdd();
355 if (linksList->count() == 0)
356 return;
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."));
370 onPreferences();
371 return;
374 process.setWorkingDirectory(savedir);
376 // Construct cclive/clive args
378 QStringList 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(";"))
391 s += ";";
392 args << QString("--exec=%1").arg(s);
393 args << QString("--exec-run");
396 if (prefs->proxyCombo->currentIndex() == 0)
397 args << "--no-proxy";
398 else {
399 s = prefs->proxyEdit->text();
400 if (!s.isEmpty())
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);
413 else
414 args << QString("--connect-timeout-socks=%1").arg(n);
417 QStringList env;
419 if (isCcliveFlag) {
420 s = regexpEdit->text();
421 if (!s.isEmpty())
422 args << QString("--regexp=%1").arg(s);
423 if (findallBox->checkState())
424 args << QString("--find-all");
425 } else {
427 args << "--stderr";
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();
435 if (!s.isEmpty())
436 args << QString("--cclass=%1").arg(s);
439 s = fnamefmtEdit->text();
440 if (!s.isEmpty())
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());
447 bool allSame = true;
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()) {
455 allSame = false;
456 break;
460 s = "flv";
462 // Use format dialog setting for the host.
463 if (allSame)
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());
473 // Prepare log
475 logEdit->clear();
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);
486 void
487 MainWindow::onCancel() {
488 cancelledFlag = true;
489 process.kill();
492 void
493 MainWindow::onAbout() {
494 AboutDialog(this, ccliveVersion, curlMod, curlVersion).exec();
497 #define fillList(dlg) \
498 do { \
499 QTreeWidgetItemIterator iter(dlg->itemsTree); \
500 while (*iter) { \
501 if ((*iter)->checkState(0) == Qt::Checked) \
502 addPageLink((*iter)->text(1)); \
503 ++iter; \
505 } while (0)
507 void
508 MainWindow::onRSS() {
509 if (rss->exec() == QDialog::Accepted) {
510 fillList(rss);
512 rss->writeSettings();
515 void
516 MainWindow::onScan() {
517 if (scan->exec() == QDialog::Accepted) {
518 fillList(scan);
520 scan->writeSettings();
523 void
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)
531 addPageLink(lst[i]);
534 void
535 MainWindow::onAdd() {
536 addPageLink(QInputDialog::getText(this,
537 QCoreApplication::applicationName(), tr("Add link:")));
540 void
541 MainWindow::onRemove() {
543 QList<QListWidgetItem*> sel = linksList->selectedItems();
545 if (sel.size() == 0)
546 return;
548 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
549 tr("Really remove the selected links?"),
550 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
551 == QMessageBox::No)
553 return;
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);
564 void
565 MainWindow::onClear() {
567 if (linksList->count() == 0)
568 return;
570 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
571 tr("Really clear list?"),
572 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
573 == QMessageBox::No)
575 return;
578 linksList->clear();
581 void
582 MainWindow::addPageLink(QString lnk) {
584 lnk = lnk.trimmed();
586 if (lnk.isEmpty())
587 return;
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()));
595 return;
598 QList<QListWidgetItem *> found
599 = linksList->findItems(lnk, Qt::MatchExactly);
601 if (found.size() == 0)
602 linksList->addItem(lnk);
605 void
606 MainWindow::onFormats() {
607 if ( hosts.isEmpty() || ccliveVersion.isEmpty() ) {
608 critCcliveNotSpecified(this);
609 onPreferences();
610 return;
613 format->exec();
614 format->saveCurrent();
615 format->writeSettings();
618 void
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);
652 void
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);
657 updateLog(msg);
661 void
662 MainWindow::onProcStdoutReady() {
664 // NOTE: We read both channels stdout and stderr.
666 char data[1024];
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);
677 ln.remove("\n");
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");
689 re.indexIn(ln);
690 fileLabel->setText( re.capturedTexts()[1].simplified() );
691 sb->showMessage( tr("Downloading video ...") );
694 else if (ln.startsWith("error:")) {
695 // Log it.
698 else {
700 appendLogFlag = false;
702 // In some parallel world I have written a cleaner regexp.
703 static const char progress_re[] =
704 "(\\d+)%" // percent
705 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
706 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
707 "\\s+(.*)"; // eta
709 QRegExp re(progress_re);
711 if (re.indexIn(ln)) {
712 QStringList cap = re.capturedTexts();
714 cap.removeFirst();
716 if (cap[0].isEmpty())
717 continue;
719 //qDebug() << cap;
721 enum {
722 PERCENT = 0,
723 SIZE_NOW_X,
724 SIZE_NOW_Y,
725 SIZE_EXPECTED_X,
726 SIZE_EXPECTED_Y,
727 SPEED_X,
728 SPEED_Y,
729 SPEED_TYPE,
730 ETA,
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")
742 .arg(cap[SPEED_X])
743 .arg(cap[SPEED_Y])
744 .arg(cap[SPEED_TYPE]));
746 etaLabel ->setText(cap[ETA].simplified());
750 if (appendLogFlag)
751 updateLog(ln + "\n");
753 memset(&data, 0, sizeof(data));
757 void
758 MainWindow::onProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
760 QString status;
762 switch (exitStatus) {
763 case QProcess::NormalExit:
764 switch (exitCode) {
765 case 0:
766 status = tr("c/clive exited normally.");
767 break;
768 default:
769 status =
770 QString(tr("c/clive exited with code %1, see log."))
771 .arg(exitCode);
772 break;
774 break;
775 default:
776 status = cancelledFlag
777 ? tr("c/clive terminated by user.")
778 : tr("c/clive crashed, see log.");
779 break;
782 updateLog(status);
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);
809 void
810 MainWindow::onItemDoubleClicked(QListWidgetItem *item) {
811 bool ok;
813 QString lnk = QInputDialog::getText(this,
814 QCoreApplication::applicationName(), tr("Edit link:"),
815 QLineEdit::Normal, item->text(), &ok);
817 if (ok && !lnk.isEmpty())
818 item->setText(lnk);