Fix progressbar update.
[abby.git] / src / mainwnd.cpp
blobdb5c93420672469f547a55a9388d9f644f9f2056
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"
37 #define critCcliveNotFound \
38 do { \
39 QMessageBox::critical(this, QCoreApplication::applicationName(), \
40 QString( tr("c/clive executable not found, please check the path.") )); \
41 } while (0)
43 typedef unsigned int _uint;
45 MainWindow::MainWindow()
46 : cancelledFlag(false), isCcliveFlag(false)
49 The word "English" is not meant to be translated literally.
50 Instead, replace "English" with the target translation language,
51 e.g. "Suomi", "Deutch", etc. abby uses this word in the
52 preferences dialog to select current language.
54 const QString lang = tr("English");
56 setupUi(this);
58 // Dialogs. Be extravagant about system memory.
59 prefs = new PreferencesDialog (this);
60 rss = new RSSDialog (this);
61 scan = new ScanDialog (this);
62 format = new FormatDialog (this);
64 // Settings.
65 readSettings();
66 setProxy();
68 // Process.
69 connect(&process, SIGNAL( started() ),
70 this, SLOT( onProcStarted() ));
72 connect(&process, SIGNAL( error(QProcess::ProcessError) ),
73 this, SLOT( onProcError(QProcess::ProcessError) ));
75 // NOTE: Merge stdout/stderr from c/clive
76 connect(&process, SIGNAL( readyReadStandardOutput() ),
77 this, SLOT( onProcStdoutReady() ));
79 connect(&process, SIGNAL( readyReadStandardError() ),
80 this, SLOT( onProcStdoutReady() ));
82 connect(&process, SIGNAL( finished(int, QProcess::ExitStatus) ),
83 this, SLOT( onProcFinished(int, QProcess::ExitStatus) ));
85 // Misc.
86 connect(linksList, SIGNAL( itemDoubleClicked(QListWidgetItem *) ),
87 this, SLOT( onItemDoubleClicked(QListWidgetItem *) ));
89 // Parse.
90 if (parseCcliveHostsOutput())
91 parseCcliveVersionOutput();
93 // Widget voodoo.
94 updateWidgets (true);
96 #ifdef WIN32
97 streamBox ->setHidden(true);
98 streamSpin->setHidden(true);
99 #endif
102 bool
103 MainWindow::parseCcliveHostsOutput() {
104 hosts.clear();
106 QString path = prefs->ccliveEdit->text();
108 if (path.isEmpty()) {
110 QMessageBox::information(
111 this,
112 QCoreApplication::applicationName(),
113 tr("abby requires `clive' or `cclive'. "
114 "Please define path to either executable.")
117 onPreferences();
119 return false;
122 QProcess proc;
124 proc.setProcessChannelMode(QProcess::MergedChannels);
125 proc.start(path, QStringList() << "--hosts");
127 if (!proc.waitForFinished()) {
128 qDebug() << path << ": " << proc.errorString();
129 critCcliveNotFound;
130 return false;
132 else {
133 QString output = QString::fromLocal8Bit(proc.readAll());
134 QStringList lst = output.split("\n", QString::SkipEmptyParts);
136 lst.removeLast(); // Note line.
138 const register _uint size = lst.size();
139 for (register _uint i=0; i<size; ++i) {
141 QString ln = lst[i].remove("\r");
142 QStringList tmp = ln.split("\t");
144 if (!tmp[0].isEmpty() && !tmp[1].isEmpty())
145 hosts[tmp[0]] = tmp[1];
148 format->parseHosts(hosts);
151 return true;
154 void
155 MainWindow::parseCcliveVersionOutput() {
157 versionOutput.clear();
158 ccliveVersion.clear();
159 curlVersion.clear();
161 QString path = prefs->ccliveEdit->text();
163 QProcess proc;
164 proc.setProcessChannelMode(QProcess::MergedChannels);
165 proc.start(path, QStringList() << "--version");
167 isCcliveFlag = false;
169 if (!proc.waitForFinished())
170 qDebug() << path << ": " << proc.errorString();
171 else {
172 versionOutput = QString::fromLocal8Bit(proc.readAll());
174 QStringList tmp = versionOutput.split("\n", QString::SkipEmptyParts);
175 QStringList lst = tmp[0].split(" ", QString::SkipEmptyParts);
177 isCcliveFlag = (lst[0] == "cclive");
179 ccliveVersion = lst[2];
180 curlVersion = lst[6];
184 bool
185 MainWindow::ccliveSupportsFeature(const QString& buildOption) {
186 return versionOutput.contains(buildOption);
189 bool
190 MainWindow::ccliveSupportsHost(const QString& lnk) {
192 const QString host = QUrl(lnk).host();
194 for (QStringMap::const_iterator iter = hosts.begin();
195 iter != hosts.end(); ++iter)
197 QRegExp re( iter.key());
199 if (re.indexIn(host) != -1)
200 return true;
203 return false;
206 void
207 MainWindow::updateWidgets(const bool updateCcliveDepends) {
208 // Enable widgets based on preferences and other settings.
209 QString s;
211 s = prefs->streamEdit->text();
212 streamBox->setEnabled(!s.isEmpty());
213 if (s.isEmpty())
214 streamBox->setCheckState(Qt::Unchecked);
216 s = prefs->commandEdit->text();
217 commandBox->setEnabled(!s.isEmpty());
218 if (s.isEmpty())
219 commandBox->setCheckState(Qt::Unchecked);
221 if (updateCcliveDepends) {
222 // The most time consuming check is to run (c)clive.
223 // Run it only when we cannot work around it.
224 if (isCcliveFlag) {
225 regexpLabel ->show();
226 regexpEdit ->show();
227 findallBox ->show();
228 cclassLabel ->hide();
229 cclassEdit ->hide();
231 else {
232 regexpLabel ->hide();
233 regexpEdit ->hide();
234 findallBox ->hide();
235 cclassLabel ->show();
236 cclassEdit ->show();
241 void
242 MainWindow::closeEvent(QCloseEvent *event) {
243 writeSettings();
244 event->accept();
247 void
248 MainWindow::writeSettings() {
249 QSettings s;
250 s.beginGroup("MainWindow");
251 s.setValue("size", size());
252 s.setValue("pos", pos());
253 s.setValue("regexpEdit", regexpEdit->text());
254 s.setValue("findallBox", findallBox->checkState());
255 s.setValue("cclassEdit", cclassEdit->text());
256 s.setValue("fnamefmtEdit", fnamefmtEdit->text());
257 s.endGroup();
260 void
261 MainWindow::readSettings() {
262 QSettings s;
263 s.beginGroup("MainWindow");
264 resize( s.value("size", QSize(525,265)).toSize() );
265 move( s.value("pos", QPoint(200,200)).toPoint() );
266 regexpEdit->setText( s.value("regexpEdit").toString() );
267 findallBox->setCheckState(
268 s.value("findallBox").toBool()
269 ? Qt::Checked
270 : Qt::Unchecked);
271 cclassEdit->setText( s.value("cclassEdit").toString() );
272 fnamefmtEdit->setText( s.value("fnamefmtEdit").toString() );
273 s.endGroup();
276 void
277 MainWindow::updateLog(const QString& newText) {
278 QString text = logEdit->toPlainText() + newText;
279 logEdit->setPlainText(text);
283 // Slots
285 void
286 MainWindow::onPreferences() {
288 QString old = prefs->ccliveEdit->text();
290 prefs->exec();
292 QString _new = prefs->ccliveEdit->text();
294 if (old != _new) {
295 parseCcliveHostsOutput();
296 parseCcliveVersionOutput();
299 updateWidgets(old != _new);
301 setProxy();
304 void
305 MainWindow::setProxy() {
306 if (!prefs->proxyEdit->text().isEmpty()
307 && prefs->proxyCombo->currentIndex() > 0)
309 QUrl url(prefs->proxyEdit->text());
311 QNetworkProxy proxy(
312 QNetworkProxy::HttpProxy,
313 url.host(),
314 url.port()
317 QNetworkProxy::setApplicationProxy(proxy);
319 else
320 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
323 void
324 MainWindow::onStreamStateChanged(int state) {
325 streamSpin->setEnabled(state != 0);
328 void
329 MainWindow::onStart() {
331 if (linksList->count() == 0) {
332 onAdd();
333 if (linksList->count() == 0)
334 return;
337 QString path = prefs->ccliveEdit->text();
339 // Check video save directory
341 // cclive has no option for this but we can work around it by
342 // changing the current working directory.
344 QString savedir = prefs->savedirEdit->text();
345 if (savedir.isEmpty()) {
346 QMessageBox::information(this, QCoreApplication::applicationName(),
347 tr("Please define a directory for downloaded videos."));
348 onPreferences();
349 return;
352 process.setWorkingDirectory(savedir);
354 // Construct cclive/clive args
356 QStringList args;
358 args << "--print-fname" << "--continue";
360 QString s = prefs->streamEdit->text();
361 if (!s.isEmpty() && streamBox->isChecked()) {
362 args << QString("--stream-exec=%1").arg(s);
363 args << QString("--stream=%1").arg(streamSpin->value());
366 s = prefs->commandEdit->text();
367 if (!s.isEmpty() && commandBox->isChecked()) {
368 if (!s.endsWith(";"))
369 s += ";";
370 args << QString("--exec=%1").arg(s);
371 args << QString("--exec-run");
374 if (prefs->proxyCombo->currentIndex() == 0)
375 args << "--no-proxy";
376 else {
377 s = prefs->proxyEdit->text();
378 if (!s.isEmpty())
379 args << QString("--proxy=%1").arg(s);
382 if (prefs->limitBox->checkState()) {
383 int n = prefs->limitSpin->value();
384 args << QString("--limit-rate=%1").arg(n);
387 if (prefs->timeoutBox->checkState()) {
388 int n = prefs->timeoutSpin->value();
389 if (!prefs->socksBox->checkState())
390 args << QString("--connect-timeout=%1").arg(n);
391 else
392 args << QString("--connect-timeout-socks=%1").arg(n);
395 QStringList env;
397 if (isCcliveFlag) {
398 s = regexpEdit->text();
399 if (!s.isEmpty())
400 args << QString("--regexp=%1").arg(s);
401 if (findallBox->checkState())
402 args << QString("--find-all");
403 } else {
405 args << "--stderr";
407 // Set environment variables for clive
408 env << "COLUMNS=80" << "LINES=24" // Term::ReadKey
409 << QString("HOME=%1").arg(QDir::homePath()); // $env{HOME}
411 s = cclassEdit->text();
412 if (!s.isEmpty())
413 args << QString("--cclass=%1").arg(s);
416 s = fnamefmtEdit->text();
417 if (!s.isEmpty())
418 args << QString("--filename-format=%1").arg(s);
420 // Check if all video page links are of the same host.
422 QUrl first(linksList->item(0)->text());
424 bool allSame = true;
425 const register _uint count = linksList->count();
427 for (register _uint i=0; i<count; ++i) {
429 QUrl url(linksList->item(i)->text());
431 if (url.host() != first.host()) {
432 allSame = false;
433 break;
437 s = "flv";
439 // Use format dialog setting for the host.
440 if (allSame)
441 s = format->getFormatSetting(first.host());
443 args << QString("--format=%1").arg(s);
445 for (register _uint i=0; i<count; ++i)
446 args << QString("%1").arg(linksList->item(i)->text());
448 totalProgressbar->setMaximum(linksList->count());
450 // Prepare log
452 logEdit->clear();
453 updateLog("% " +path+ " " +args.join(" ")+ "\n");
455 // And finally start the process
457 cancelledFlag = false;
458 process.setEnvironment(env);
459 process.setProcessChannelMode(QProcess::MergedChannels);
460 process.start(path,args);
463 void
464 MainWindow::onCancel() {
465 cancelledFlag = true;
466 process.kill();
469 void
470 MainWindow::onAbout() {
471 AboutDialog(this, ccliveVersion, curlVersion).exec();
474 #define fillList(dlg) \
475 do { \
476 QTreeWidgetItemIterator iter(dlg->itemsTree); \
477 while (*iter) { \
478 if ((*iter)->checkState(0) == Qt::Checked) \
479 addPageLink((*iter)->text(1)); \
480 ++iter; \
482 } while (0)
484 void
485 MainWindow::onRSS() {
486 if (rss->exec() == QDialog::Accepted) {
487 fillList(rss);
489 rss->writeSettings();
492 void
493 MainWindow::onScan() {
494 if (scan->exec() == QDialog::Accepted) {
495 fillList(scan);
497 scan->writeSettings();
500 void
501 MainWindow::onPasteURL() {
503 QClipboard *cb = QApplication::clipboard();
504 QStringList lst = cb->text().split("\n");
505 const register _uint size = lst.size();
507 for (register _uint i=0; i<size; ++i)
508 addPageLink(lst[i]);
511 void
512 MainWindow::onAdd() {
513 addPageLink(QInputDialog::getText(this,
514 QCoreApplication::applicationName(), tr("Add link:")));
517 void
518 MainWindow::onRemove() {
520 QList<QListWidgetItem*> sel = linksList->selectedItems();
522 if (sel.size() == 0)
523 return;
525 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
526 tr("Really remove the selected links?"),
527 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
528 == QMessageBox::No)
530 return;
533 const register _uint size = sel.size();
535 for (register _uint i=0; i<size; ++i) {
536 const int row = linksList->row(sel[i]);
537 delete linksList->takeItem(row);
541 void
542 MainWindow::onClear() {
544 if (linksList->count() == 0)
545 return;
547 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
548 tr("Really clear list?"),
549 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
550 == QMessageBox::No)
552 return;
555 linksList->clear();
558 void
559 MainWindow::addPageLink(QString lnk) {
561 lnk = lnk.trimmed();
563 if (lnk.isEmpty())
564 return;
566 if (!lnk.startsWith("http://", Qt::CaseInsensitive))
567 lnk.insert(0,"http://");
569 if (!ccliveSupportsHost(lnk)) {
570 QMessageBox::critical(this, QCoreApplication::applicationName(),
571 QString(tr("%1: unsupported website")).arg(QUrl(lnk).host()));
572 return;
575 QList<QListWidgetItem *> found
576 = linksList->findItems(lnk, Qt::MatchExactly);
578 if (found.size() == 0)
579 linksList->addItem(lnk);
582 void
583 MainWindow::onFormats() {
584 if (hosts.isEmpty()) {
585 critCcliveNotFound;
586 onPreferences();
587 return;
589 format->exec();
590 format->saveCurrent();
591 format->writeSettings();
594 void
595 MainWindow::onProcStarted() {
596 statusBar() ->clearMessage();
597 fileLabel ->setText("-");
598 sizeLabel ->setText("-- / --");
599 rateLabel ->setText("--.-");
600 etaLabel ->setText("--:--");
601 progressBar ->setValue(0);
602 totalProgressbar->setValue(0);
604 addButton ->setEnabled(false);
605 pasteButton ->setEnabled(false);
606 removeButton->setEnabled(false);
607 clearButton ->setEnabled(false);
608 rssButton ->setEnabled(false);
609 scanButton ->setEnabled(false);
610 startButton ->setEnabled(false);
611 cancelButton->setEnabled(true);
613 action_Download->setEnabled(false);
615 action_Link ->setEnabled(false);
616 action_RSS ->setEnabled(false);
617 action_Scan ->setEnabled(false);
618 action_Paste->setEnabled(false);
620 action_Remove->setEnabled(false);
621 action_Clear->setEnabled(false);
623 linksList ->setEnabled(false);
625 tabWidget->setTabEnabled(1, false);
627 errorFlag = false;
630 void
631 MainWindow::onProcError(QProcess::ProcessError err) {
632 if (err == QProcess::FailedToStart) {
633 QString msg = tr("Error: Failed to start the process.");
634 statusBar()->showMessage(msg);
635 updateLog(msg);
639 void
640 MainWindow::onProcStdoutReady() {
642 // NOTE: We read both channels stdout and stderr.
644 char data[1024];
645 memset(&data, 0, sizeof(data));
647 QStatusBar *sb = statusBar();
648 bool appendLogFlag = true;
650 while (process.readLine(data, sizeof(data))) {
652 appendLogFlag = true;
654 QString ln = QString::fromLocal8Bit(data);
655 ln.remove("\n");
657 if (ln.startsWith("fetch http://")) {
658 sb->showMessage( tr("Fetching ...") );
659 totalProgressbar->setValue( totalProgressbar->value()+1 );
662 else if (ln.startsWith("verify"))
663 sb->showMessage( tr("Verifying link ...") );
665 else if (ln.startsWith("file:")) {
666 QRegExp re("file: (.*)(\\d+.\\d+)M");
667 re.indexIn(ln);
668 fileLabel->setText( re.capturedTexts()[1].simplified() );
669 sb->showMessage( tr("Downloading video ...") );
672 else if (ln.startsWith("error:"))
673 errorFlag = true;
675 else {
677 appendLogFlag = false;
679 // In an parallel world I have written a cleaner regexp.
680 static const char progress_re[] =
681 "(\\d+)%" // percent
682 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
683 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
684 "\\s+(.*)"; // eta
686 QRegExp re(progress_re);
688 if (re.indexIn(ln)) {
689 QStringList cap = re.capturedTexts();
691 cap.removeFirst();
693 if (cap[0].isEmpty())
694 continue;
696 //qDebug() << cap;
698 enum {
699 PERCENT = 0,
700 SIZE_NOW_X,
701 SIZE_NOW_Y,
702 SIZE_EXPECTED_X,
703 SIZE_EXPECTED_Y,
704 SPEED_X,
705 SPEED_Y,
706 SPEED_TYPE,
707 ETA,
710 progressBar ->setValue( cap[PERCENT].toInt() );
712 sizeLabel ->setText(QString("%1.%2M / %3.%4M")
713 .arg(cap[SIZE_NOW_X])
714 .arg(cap[SIZE_NOW_Y])
715 .arg(cap[SIZE_EXPECTED_X])
716 .arg(cap[SIZE_EXPECTED_Y]));
718 rateLabel ->setText(QString("%1.%2%3/s")
719 .arg(cap[SPEED_X])
720 .arg(cap[SPEED_Y])
721 .arg(cap[SPEED_TYPE]));
723 etaLabel ->setText(cap[ETA].simplified());
727 if (appendLogFlag)
728 updateLog(ln + "\n");
730 memset(&data, 0, sizeof(data));
734 void
735 MainWindow::onProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
736 QString status;
737 if (!errorFlag) {
738 if (exitStatus == QProcess::NormalExit) {
739 status = exitCode != 0
740 ? tr("Process exited with an error. See Log for details")
741 : tr("Process exited normally");
742 } else {
743 status = cancelledFlag
744 ? tr("Process terminated")
745 : tr("Process crashed. See Log for details");
747 updateLog(status + ".");
749 else
750 status = tr("Error(s) occurred. See Log for details.");
752 statusBar()->showMessage(status);
754 addButton ->setEnabled(true);
755 pasteButton ->setEnabled(true);
756 removeButton->setEnabled(true);
757 clearButton ->setEnabled(true);
758 rssButton ->setEnabled(true);
759 scanButton ->setEnabled(true);
760 startButton ->setEnabled(true);
761 cancelButton->setEnabled(false);
763 action_Download->setEnabled(true);
765 action_Link ->setEnabled(true);
766 action_RSS ->setEnabled(true);
767 action_Scan ->setEnabled(true);
768 action_Paste->setEnabled(true);
770 action_Remove->setEnabled(true);
771 action_Clear->setEnabled(true);
773 linksList ->setEnabled(true);
775 tabWidget ->setTabEnabled(1, true);
778 void
779 MainWindow::onItemDoubleClicked(QListWidgetItem *item) {
780 bool ok;
782 QString lnk = QInputDialog::getText(this,
783 QCoreApplication::applicationName(), tr("Edit link:"),
784 QLineEdit::Normal, item->text(), &ok);
786 if (ok && !lnk.isEmpty())
787 item->setText(lnk);