httpmgr: fix: emit finished if != http/200.
[abby.git] / src / mainwnd.cpp
blobe7d14405a966fe45f050c6549f415b16f7457605
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
116 // Enable drops.
117 setAcceptDrops(true);
120 bool
121 MainWindow::parseCcliveHostsOutput() {
122 hosts.clear();
124 QString path = prefs->ccliveEdit->text();
126 if (path.isEmpty()) {
128 QMessageBox::information(
129 this,
130 QCoreApplication::applicationName(),
131 tr("abby requires `clive' or `cclive'. "
132 "Please define path to either executable.")
135 onPreferences();
137 return false;
140 QProcess proc;
141 proc.setEnvironment(QStringList() << "CCLIVE_NO_CONFIG=1");
142 proc.setProcessChannelMode(QProcess::MergedChannels);
143 proc.start(path, QStringList() << "--hosts");
145 format->resetHosts();
147 if (!proc.waitForFinished()) {
148 critCcliveProcessFailed(this, proc.errorString() );
149 return false;
151 else {
152 const QString output =
153 QString::fromLocal8Bit( proc.readAll() );
155 const int exitCode =
156 proc.exitCode();
158 if (exitCode == 0) {
160 QStringList lst =
161 output.split("\n", QString::SkipEmptyParts);
163 lst.removeLast(); // The note line.
165 const register _uint size = lst.size();
166 for (register _uint i=0; i<size; ++i) {
168 QString ln = lst[i].remove("\r");
169 QStringList tmp = ln.split("\t");
171 if (!tmp[0].isEmpty() && !tmp[1].isEmpty())
172 hosts[tmp[0]] = tmp[1];
175 format->parseHosts(hosts);
177 else {
178 critCcliveExitedWithError(this, exitCode, output);
179 return false;
183 return true;
186 bool
187 MainWindow::parseCcliveVersionOutput() {
189 QString path =
190 prefs->ccliveEdit->text();
192 try {
193 if (path.isEmpty()) {
194 Util::detectCclive(
195 path,
196 ccliveVersion,
197 libVersion,
198 libName,
199 &isCcliveFlag
201 if (!path.isEmpty()) {
202 prefs->ccliveEdit->setText(path);
203 return true;
207 Util::verifyCclivePath(
208 path,
209 ccliveVersion,
210 libVersion,
211 libName,
212 &isCcliveFlag
215 catch (const NoCcliveException& x) {
216 QMessageBox::warning(this, tr("Warning"), x.what());
217 return false;
219 return true;
222 bool
223 MainWindow::ccliveSupportsHost(const QString& lnk) {
225 const QString host = QUrl(lnk).host();
227 for (QStringMap::const_iterator iter = hosts.begin();
228 iter != hosts.end(); ++iter)
230 QRegExp re( iter.key());
232 if (re.indexIn(host) != -1)
233 return true;
236 return false;
239 void
240 MainWindow::updateWidgets(const bool updateCcliveDepends) {
241 // Enable widgets based on preferences and other settings.
242 QString s;
244 s = prefs->streamEdit->text();
245 streamBox->setEnabled(!s.isEmpty());
246 if (s.isEmpty())
247 streamBox->setCheckState(Qt::Unchecked);
249 s = prefs->commandEdit->text();
250 commandBox->setEnabled(!s.isEmpty());
251 if (s.isEmpty())
252 commandBox->setCheckState(Qt::Unchecked);
254 if (updateCcliveDepends) {
255 // The most time consuming check is to run (c)clive.
256 // Run it only when we cannot work around it.
257 if (isCcliveFlag) {
258 regexpLabel ->show();
259 regexpEdit ->show();
260 substEdit ->show();
261 cclassLabel ->hide();
262 cclassEdit ->hide();
264 else {
265 regexpLabel ->hide();
266 regexpEdit ->hide();
267 substEdit ->hide();
268 cclassLabel ->show();
269 cclassEdit ->show();
274 void
275 MainWindow::closeEvent(QCloseEvent *event) {
276 int rc = QMessageBox::Yes;
277 if (process.state() != QProcess::NotRunning) {
278 rc = QMessageBox::warning(
279 this,
280 tr("Warning"),
281 tr("c/clive process is still active, really close abby?"),
282 QMessageBox::Yes|QMessageBox::No
286 if (rc == QMessageBox::Yes) {
287 process.kill();
288 writeSettings();
289 event->accept();
291 else
292 event->ignore();
295 void
296 MainWindow::writeSettings() {
297 QSettings s;
298 s.beginGroup("MainWindow");
299 s.setValue("size", size());
300 s.setValue("pos", pos());
301 s.setValue("regexpEdit", regexpEdit->text());
302 s.setValue("substEdit", substEdit->text());
303 s.setValue("cclassEdit", cclassEdit->text());
304 s.setValue("fnamefmtEdit", fnamefmtEdit->text());
305 s.endGroup();
308 void
309 MainWindow::readSettings() {
310 QSettings s;
311 s.beginGroup("MainWindow");
312 resize( s.value("size", QSize(525,265)).toSize() );
313 move( s.value("pos", QPoint(200,200)).toPoint() );
314 regexpEdit->setText( s.value("regexpEdit").toString() );
315 substEdit->setText( s.value("substEdit").toString() );
316 cclassEdit->setText( s.value("cclassEdit").toString() );
317 fnamefmtEdit->setText( s.value("fnamefmtEdit").toString() );
318 s.endGroup();
322 // Slots
324 void
325 MainWindow::onPreferences() {
327 QString old = prefs->ccliveEdit->text();
329 prefs->exec();
331 QString _new = prefs->ccliveEdit->text();
333 if (old != _new) {
334 if (parseCcliveVersionOutput())
335 parseCcliveHostsOutput();
338 updateWidgets(old != _new);
340 setProxy();
343 void
344 MainWindow::setProxy() {
345 if (!prefs->proxyEdit->text().isEmpty()
346 && prefs->proxyCombo->currentIndex() > 0)
348 QUrl url(prefs->proxyEdit->text());
350 QNetworkProxy proxy(
351 QNetworkProxy::HttpProxy,
352 url.host(),
353 url.port()
356 QNetworkProxy::setApplicationProxy(proxy);
358 else
359 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
362 void
363 MainWindow::onStreamStateChanged(int state) {
364 streamSpin->setEnabled(state != 0);
367 void
368 MainWindow::onStart() {
370 if ( ccliveVersion.isEmpty() ) {
371 critCcliveNotSpecified(this);
372 onPreferences();
373 return;
376 if (linksList->count() == 0) {
377 onAdd();
378 if (linksList->count() == 0)
379 return;
382 QString path = prefs->ccliveEdit->text();
384 // Check video save directory
386 // cclive has no option for this but we can work around it by
387 // changing the current working directory.
389 QString savedir = prefs->savedirEdit->text();
390 if (savedir.isEmpty()) {
391 QMessageBox::information(this, QCoreApplication::applicationName(),
392 tr("Please define a directory for downloaded videos."));
393 onPreferences();
394 return;
397 process.setWorkingDirectory(savedir);
399 // Construct cclive/clive args
401 QStringList args;
403 args << "--print-fname" << "--continue";
405 QString s = prefs->streamEdit->text();
406 if (!s.isEmpty() && streamBox->isChecked()) {
407 args << QString("--stream-exec=%1").arg(s);
408 args << QString("--stream=%1").arg(streamSpin->value());
411 s = prefs->commandEdit->text();
412 if (!s.isEmpty() && commandBox->isChecked()) {
413 if (!s.endsWith(";"))
414 s += ";";
415 args << QString("--exec=%1").arg(s);
416 args << QString("--exec-run");
419 if (prefs->proxyCombo->currentIndex() == 0)
420 args << "--no-proxy";
421 else {
422 s = prefs->proxyEdit->text();
423 if (!s.isEmpty())
424 args << QString("--proxy=%1").arg(s);
427 if (prefs->limitBox->checkState()) {
428 int n = prefs->limitSpin->value();
429 args << QString("--limit-rate=%1").arg(n);
432 if (prefs->timeoutBox->checkState()) {
433 int n = prefs->timeoutSpin->value();
434 if (!prefs->socksBox->checkState())
435 args << QString("--connect-timeout=%1").arg(n);
436 else
437 args << QString("--connect-timeout-socks=%1").arg(n);
440 QStringList env;
442 if (isCcliveFlag) {
443 s = regexpEdit->text();
444 if (!s.isEmpty())
445 args << QString("--regexp=%1").arg(s);
446 s = substEdit->text();
447 if (!s.isEmpty())
448 args << QString("--substitute=%1").arg(s);
449 } else {
451 args << "--stderr";
453 // Set environment variables for clive
454 env << "COLUMNS=80" << "LINES=24" // Term::ReadKey
455 << QString("HOME=%1").arg(QDir::homePath()) // $env{HOME}
456 << "CCLIVE_NO_CONFIG=1"; // cclive 0.5.0+
458 s = cclassEdit->text();
459 if (!s.isEmpty())
460 args << QString("--cclass=%1").arg(s);
463 s = fnamefmtEdit->text();
464 if (!s.isEmpty())
465 args << QString("--filename-format=%1").arg(s);
467 // Check if all video page links are of the same host.
469 QUrl first(linksList->item(0)->text());
471 bool allSame = true;
472 const register _uint count = linksList->count();
474 for (register _uint i=0; i<count; ++i) {
476 QUrl url(linksList->item(i)->text());
478 if (url.host() != first.host()) {
479 allSame = false;
480 break;
484 s = "flv";
486 // Use format dialog setting for the host.
487 if (allSame)
488 s = format->getFormatSetting(first.host());
490 args << QString("--format=%1").arg(s);
492 for (register _uint i=0; i<count; ++i)
493 args << QString("%1").arg(linksList->item(i)->text());
495 totalProgressbar->setMaximum(linksList->count());
497 // Prepare log
499 logEdit->clear();
500 Util::appendLog(logEdit, "% " +path+ " " +args.join(" "));
502 // And finally start the process
504 cancelledFlag = false;
505 process.setEnvironment(env);
506 process.setProcessChannelMode(QProcess::MergedChannels);
507 process.start(path,args);
510 void
511 MainWindow::onCancel() {
512 cancelledFlag = true;
513 process.kill();
516 void
517 MainWindow::onAbout() {
518 AboutDialog(this, ccliveVersion, isCcliveFlag, libName, libVersion).exec();
521 #define fillList(dlg) \
522 do { \
523 QTreeWidgetItemIterator iter(dlg->itemsTree); \
524 while (*iter) { \
525 if ((*iter)->checkState(0) == Qt::Checked) \
526 addPageLink((*iter)->text(1)); \
527 ++iter; \
529 } while (0)
531 void
532 MainWindow::onRSS() {
533 if (rss->exec() == QDialog::Accepted) {
534 fillList(rss);
536 rss->writeSettings();
539 void
540 MainWindow::onScan() {
541 if (scan->exec() == QDialog::Accepted) {
542 fillList(scan);
544 scan->writeSettings();
547 void
548 MainWindow::onPasteURL() {
550 QClipboard *cb = QApplication::clipboard();
551 QStringList lst = cb->text().split("\n");
552 const register _uint size = lst.size();
554 for (register _uint i=0; i<size; ++i)
555 addPageLink(lst[i]);
558 void
559 MainWindow::onAdd() {
560 addPageLink(QInputDialog::getText(this,
561 QCoreApplication::applicationName(), tr("Add link:")));
564 void
565 MainWindow::onRemove() {
567 QList<QListWidgetItem*> sel = linksList->selectedItems();
569 if (sel.size() == 0)
570 return;
572 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
573 tr("Really remove the selected links?"),
574 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
575 == QMessageBox::No)
577 return;
580 const register _uint size = sel.size();
582 for (register _uint i=0; i<size; ++i) {
583 const int row = linksList->row(sel[i]);
584 delete linksList->takeItem(row);
588 void
589 MainWindow::onClear() {
591 if (linksList->count() == 0)
592 return;
594 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
595 tr("Really clear list?"),
596 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
597 == QMessageBox::No)
599 return;
602 linksList->clear();
605 void
606 MainWindow::addPageLink(QString lnk) {
608 lnk = lnk.trimmed();
610 if (lnk.isEmpty())
611 return;
613 if (!lnk.startsWith("http://", Qt::CaseInsensitive))
614 lnk.insert(0,"http://");
616 if (!ccliveSupportsHost(lnk)) {
617 QMessageBox::critical(this, QCoreApplication::applicationName(),
618 QString(tr("%1: unsupported website")).arg(QUrl(lnk).host()));
619 return;
622 QList<QListWidgetItem *> found
623 = linksList->findItems(lnk, Qt::MatchExactly);
625 if (found.size() == 0)
626 linksList->addItem(lnk);
629 void
630 MainWindow::onFormats() {
631 if ( hosts.isEmpty() || ccliveVersion.isEmpty() ) {
632 critCcliveNotSpecified(this);
633 onPreferences();
634 return;
637 format->exec();
638 format->saveCurrent();
639 format->writeSettings();
642 void
643 MainWindow::onProcStarted() {
644 statusBar() ->clearMessage();
645 fileLabel ->setText("-");
646 sizeLabel ->setText("-- / --");
647 rateLabel ->setText("--.-");
648 etaLabel ->setText("--:--");
649 progressBar ->setValue(0);
650 totalProgressbar->setValue(0);
652 addButton ->setEnabled(false);
653 pasteButton ->setEnabled(false);
654 removeButton->setEnabled(false);
655 clearButton ->setEnabled(false);
656 rssButton ->setEnabled(false);
657 scanButton ->setEnabled(false);
658 startButton ->setEnabled(false);
659 cancelButton->setEnabled(true);
661 action_Download->setEnabled(false);
663 action_Link ->setEnabled(false);
664 action_RSS ->setEnabled(false);
665 action_Scan ->setEnabled(false);
666 action_Paste->setEnabled(false);
668 action_Remove->setEnabled(false);
669 action_Clear->setEnabled(false);
671 linksList ->setEnabled(false);
673 tabWidget->setTabEnabled(1, false);
676 void
677 MainWindow::onProcError(QProcess::ProcessError err) {
678 if (err == QProcess::FailedToStart) {
679 QString msg = tr("Error: Failed to start the process.");
680 statusBar()->showMessage(msg);
681 Util::appendLog(logEdit, msg);
685 void
686 MainWindow::onProcStdoutReady() {
688 // NOTE: We read both channels stdout and stderr.
690 char data[1024];
691 memset(&data, 0, sizeof(data));
693 QStatusBar *sb = statusBar();
694 bool appendLogFlag = true;
696 while (process.readLine(data, sizeof(data))) {
698 appendLogFlag = true;
700 QString ln = QString::fromLocal8Bit(data);
701 ln.remove("\n");
703 if (ln.startsWith("fetch http://")) {
704 sb->showMessage( tr("Fetching ...") );
705 totalProgressbar->setValue( totalProgressbar->value()+1 );
708 else if (ln.startsWith("verify"))
709 sb->showMessage( tr("Verifying link ...") );
711 else if (ln.startsWith("file:")) {
712 QRegExp re("file: (.*) (\\d+.\\d+)M");
713 re.indexIn(ln);
714 fileLabel->setText( re.capturedTexts()[1].simplified() );
715 sb->showMessage( tr("Downloading video ...") );
718 else if (ln.startsWith("error:")) {
719 // Log it.
722 else {
724 appendLogFlag = false;
726 // In some parallel world I have written a cleaner regexp.
727 static const char progress_re[] =
728 "(\\d+)%" // percent
729 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
730 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
731 "\\s+(.*)"; // eta
733 QRegExp re(progress_re);
735 if (re.indexIn(ln)) {
736 QStringList cap = re.capturedTexts();
738 cap.removeFirst();
740 if (cap[0].isEmpty())
741 continue;
743 //qDebug() << cap;
745 enum {
746 PERCENT = 0,
747 SIZE_NOW_X,
748 SIZE_NOW_Y,
749 SIZE_EXPECTED_X,
750 SIZE_EXPECTED_Y,
751 SPEED_X,
752 SPEED_Y,
753 SPEED_TYPE,
754 ETA,
757 progressBar ->setValue( cap[PERCENT].toInt() );
759 sizeLabel ->setText(QString("%1.%2M / %3.%4M")
760 .arg(cap[SIZE_NOW_X])
761 .arg(cap[SIZE_NOW_Y])
762 .arg(cap[SIZE_EXPECTED_X])
763 .arg(cap[SIZE_EXPECTED_Y]));
765 rateLabel ->setText(QString("%1.%2%3/s")
766 .arg(cap[SPEED_X])
767 .arg(cap[SPEED_Y])
768 .arg(cap[SPEED_TYPE]));
770 etaLabel ->setText(cap[ETA].simplified());
774 if (appendLogFlag)
775 Util::appendLog(logEdit, ln);
777 memset(&data, 0, sizeof(data));
781 void
782 MainWindow::onProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
784 QString status;
786 switch (exitStatus) {
787 case QProcess::NormalExit:
788 switch (exitCode) {
789 case 0:
790 status = tr("c/clive exited normally.");
791 break;
792 default:
793 status =
794 QString(tr("c/clive exited with code %1, see log."))
795 .arg(exitCode);
796 break;
798 break;
799 default:
800 status = cancelledFlag
801 ? tr("c/clive terminated by user.")
802 : tr("c/clive crashed, see log.");
803 break;
806 Util::appendLog(logEdit, status);
807 statusBar()->showMessage(status);
809 addButton ->setEnabled(true);
810 pasteButton ->setEnabled(true);
811 removeButton->setEnabled(true);
812 clearButton ->setEnabled(true);
813 rssButton ->setEnabled(true);
814 scanButton ->setEnabled(true);
815 startButton ->setEnabled(true);
816 cancelButton->setEnabled(false);
818 action_Download->setEnabled(true);
820 action_Link ->setEnabled(true);
821 action_RSS ->setEnabled(true);
822 action_Scan ->setEnabled(true);
823 action_Paste->setEnabled(true);
825 action_Remove->setEnabled(true);
826 action_Clear->setEnabled(true);
828 linksList ->setEnabled(true);
830 tabWidget ->setTabEnabled(1, true);
833 void
834 MainWindow::onItemDoubleClicked(QListWidgetItem *item) {
835 bool ok;
837 QString lnk = QInputDialog::getText(this,
838 QCoreApplication::applicationName(), tr("Edit link:"),
839 QLineEdit::Normal, item->text(), &ok);
841 if (ok && !lnk.isEmpty())
842 item->setText(lnk);
845 void
846 MainWindow::dragEnterEvent(QDragEnterEvent *event) {
847 if (event->mimeData()->hasText())
848 event->acceptProposedAction();
851 void
852 MainWindow::dropEvent(QDropEvent *event) {
853 QStringList lst = event->mimeData()->text().split("\n");
854 const register _uint size = lst.size();
856 for (register _uint i=0; i<size; ++i)
857 addPageLink(lst[i]);
859 event->acceptProposedAction();