Add: Clear list (button +menu item). Tweak: status msgs.
[abby.git] / src / mainwnd.cpp
blobceb11b4dede1b9351b120ec1ab3507b99c59c33a
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 MainWindow::MainWindow():
38 cancelled(false)
41 The word "English" is not meant to be translated literally.
42 Replace the word "English" with target language, e.g. Suomi
43 or Deutsch. Note that the only purpose of this string is to
44 list it as an available language in the Preferences dialog.
46 const QString lang = tr("English");
48 setupUi(this);
49 readSettings();
51 connect(&process, SIGNAL(started()),
52 this, SLOT(onProcStarted()));
53 connect(&process, SIGNAL(error(QProcess::ProcessError)),
54 this, SLOT(onProcError(QProcess::ProcessError)));
56 // NOTE: We read both channels stdout and stderr.
57 connect(&process, SIGNAL(readyReadStandardOutput()),
58 this, SLOT(onProcStdoutReady()));
59 connect(&process, SIGNAL(readyReadStandardError()),
60 this, SLOT(onProcStdoutReady()));
62 connect(&process, SIGNAL(finished(int,QProcess::ExitStatus)),
63 this, SLOT(onProcFinished(int,QProcess::ExitStatus)));
65 prefs = new PreferencesDialog(this);
66 rss = new RSSDialog(this);
67 scan = new ScanDialog(this);
68 format = new FormatDialog(this);
70 checkCclivePath();
71 updateWidgets(true);
72 parseCcliveHostsOutput();
73 setProxy();
75 scanButton->setVisible(false);
78 bool
79 MainWindow::checkCclivePath() {
80 if (prefs->ccliveEdit->text().isEmpty()) {
81 QMessageBox::information(this, QCoreApplication::applicationName(),
82 tr("abby requires either `clive' or `cclive'.\n"
83 "Please define a path to either command."));
84 onPreferences();
85 return false;
87 return true;
90 bool
91 MainWindow::isCclive(QString& output) {
92 QString path = prefs->ccliveEdit->text();
94 if (path.isEmpty())
95 return false;
97 QProcess process;
98 process.setProcessChannelMode(QProcess::MergedChannels);
99 process.start(path, QStringList() << "--version");
101 bool state = false;
102 if (!process.waitForFinished())
103 qDebug() << path << ": " << process.errorString();
104 else {
105 output = QString::fromLocal8Bit(process.readAll());
106 QStringList lst = output.split(" ", QString::SkipEmptyParts);
107 state = lst[0] == "cclive";
108 ccliveVersion = lst[2];
109 curlVersion = lst[6];
111 return state;
114 void
115 MainWindow::parseCcliveHostsOutput() {
116 QString path = prefs->ccliveEdit->text();
118 if (path.isEmpty())
119 return;
121 QProcess proc;
122 proc.setProcessChannelMode(QProcess::MergedChannels);
123 proc.start(path, QStringList() << "--hosts");
125 if (!proc.waitForFinished())
126 qDebug() << path << ": " << proc.errorString();
127 else {
128 hostsOutput.clear();
129 QString output = QString::fromLocal8Bit(proc.readAll());
130 hostsOutput = output.split("\n", QString::SkipEmptyParts);
134 bool
135 MainWindow::ccliveSupportsFeature(const QString& buildOption) {
136 QString output;
137 const bool _isCclive = isCclive(output);
139 if (!_isCclive && buildOption == "--with-perl")
140 return false; // To keep the titleBox hidden always
142 return output.contains(buildOption);
145 bool
146 MainWindow::ccliveSupportsHost(const QString& lnk) {
148 const QString host = QUrl(lnk).host();
149 const int size = hostsOutput.size();
151 for (register int i=0; i<size; ++i) {
152 QRegExp re(hostsOutput[i]);
153 if (re.indexIn(host) != -1)
154 return true;
156 return false;
159 void
160 MainWindow::updateWidgets(const bool updateCcliveDepends) {
161 // Enable widgets based on preferences and other settings.
162 QString s;
164 s = prefs->streamEdit->text();
165 streamBox->setEnabled(!s.isEmpty());
166 if (s.isEmpty())
167 streamBox->setCheckState(Qt::Unchecked);
169 s = prefs->commandEdit->text();
170 commandBox->setEnabled(!s.isEmpty());
171 if (s.isEmpty())
172 commandBox->setCheckState(Qt::Unchecked);
174 if (updateCcliveDepends) {
175 // The most time consuming check is to run (c)clive.
176 // Run it only when we cannot work around it.
177 if (ccliveSupportsFeature("--with-perl"))
178 titleBox->show();
179 else {
180 titleBox->setCheckState(Qt::Unchecked);
181 titleBox->hide();
186 void
187 MainWindow::closeEvent(QCloseEvent *event) {
188 writeSettings();
189 event->accept();
192 void
193 MainWindow::writeSettings() {
194 QSettings s;
195 s.beginGroup("MainWindow");
196 s.setValue("size", size());
197 s.setValue("pos", pos());
198 s.setValue("titleBox", titleBox->checkState());
199 s.endGroup();
202 void
203 MainWindow::readSettings() {
204 QSettings s;
205 s.beginGroup("MainWindow");
206 resize( s.value("size", QSize(525,265)).toSize() );
207 move( s.value("pos", QPoint(200,200)).toPoint() );
208 titleBox->setCheckState(
209 s.value("titleBox").toBool()
210 ? Qt::Checked
211 : Qt::Unchecked);
212 s.endGroup();
215 void
216 MainWindow::updateLog(const QString& newText) {
217 QString text = logEdit->toPlainText() + newText;
218 logEdit->setPlainText(text);
222 // Slots
224 void
225 MainWindow::onPreferences() {
226 QString old = prefs->ccliveEdit->text();
227 prefs->exec();
228 QString _new = prefs->ccliveEdit->text();
229 updateWidgets(old != _new);
230 if (old != _new)
231 parseCcliveHostsOutput();
232 setProxy();
235 void
236 MainWindow::setProxy() {
237 if (!prefs->proxyEdit->text().isEmpty()
238 && prefs->proxyCombo->currentIndex() > 0)
240 QNetworkProxy proxy;
242 proxy.setType(QNetworkProxy::HttpProxy);
244 QUrl url(prefs->proxyEdit->text());
246 proxy.setHostName(url.host());
247 proxy.setPort(url.port());
249 QNetworkProxy::setApplicationProxy(proxy);
253 void
254 MainWindow::onStreamStateChanged(int state) {
255 streamSpin->setEnabled(state != 0);
258 void
259 MainWindow::onStart() {
261 if (linksList->count() == 0) {
262 QMessageBox::information(this, QCoreApplication::applicationName(),
263 tr("Add video page links to the list."));
264 return;
267 // Check cclive
268 if (!checkCclivePath())
269 return;
271 QString path = prefs->ccliveEdit->text();
273 // Check video save directory
275 // cclive does not support this concept at command line option level.
276 // We work around this by changing the working directory instead.
278 QString savedir = prefs->savedirEdit->text();
279 if (savedir.isEmpty()) {
280 QMessageBox::information(this, QCoreApplication::applicationName(),
281 tr("Please specify a directory to which the downloaded videos\n"
282 "should be saved to."));
283 onPreferences();
284 return;
287 process.setWorkingDirectory(savedir);
289 QString output;
290 const bool _isCclive = isCclive(output);
292 // Construct cclive/clive args
294 QStringList args;
295 QStringList env;
297 if (_isCclive) {
298 args << "--print-fname";
299 } else {
300 args << "--stderr";
301 // Set environment variables for clive
302 env << "COLUMNS=80" << "LINES=24" // Term::ReadKey
303 << QString("HOME=%1").arg(QDir::homePath()); // $env{HOME}
306 QString s = prefs->streamEdit->text();
307 if (!s.isEmpty() && streamBox->isChecked()) {
308 args << QString("--stream-exec=%1").arg(s);
309 args << QString("--stream=%1").arg(streamSpin->value());
312 s = prefs->commandEdit->text();
313 if (!s.isEmpty() && commandBox->isChecked()) {
314 if (!s.endsWith(";"))
315 s += ";";
316 args << QString("--exec=%1").arg(s);
319 if (prefs->proxyCombo->currentIndex() == 0)
320 args << "--no-proxy";
321 else {
322 s = prefs->proxyEdit->text();
323 if (!s.isEmpty())
324 args << QString("--proxy=%1").arg(s);
327 if (prefs->limitBox->checkState()) {
328 int n = prefs->limitSpin->value();
329 args << QString("--limit-rate=%1").arg(n);
332 if (prefs->timeoutBox->checkState()) {
333 int n = prefs->timeoutSpin->value();
334 if (!prefs->socksBox->checkState())
335 args << QString("--connect-timeout=%1").arg(n);
336 else
337 args << QString("--connect-timeout-socks=%1").arg(n);
340 args << "--continue"; // default to continue
342 if (_isCclive) { // clive defaults to this
343 if (titleBox->isChecked()) {
344 args << "--title";
345 s = prefs->cclassEdit->text();
346 if (!s.isEmpty())
347 args << QString("--cclass=%1").arg(s);
349 } else { // clive can use this
350 s = prefs->cclassEdit->text();
351 if (!s.isEmpty())
352 args << QString("--cclass=%1").arg(s);
355 // Check if all video page links are of the same host.
357 QUrl first(linksList->item(0)->text());
358 bool allSame = true;
359 for (register int i=0; i<linksList->count(); ++i) {
360 QUrl url(linksList->item(i)->text());
361 if (url.host() != first.host()) {
362 allSame = false;
363 break;
367 s = "flv";
369 if (allSame) {
371 // Use format dialog setting for the host.
372 // Otherwise we will use "flv" as default for all.
374 QMap<QString, QComboBox*> map;
376 map["youtube.com"] = format->youtubeBox;
377 map["video.google."] = format->googleBox;
378 map["dailymotion.com"] = format->dailymotionBox;
379 map["vimeo.com"] = format->vimeoBox;
381 for (QMap<QString, QComboBox*>::const_iterator iter=map.begin();
382 iter != map.end(); ++iter)
384 QRegExp re(iter.key());
385 if (re.indexIn(first.host()) != -1) {
386 s = iter.value()->currentText()
387 .split(" ", QString::SkipEmptyParts)[0];
388 break;
393 args << QString("--format=%1").arg(s);
395 for (register int i=0; i<linksList->count(); ++i)
396 args << QString("%1").arg(linksList->item(i)->text());
398 totalProgressbar->setMaximum(linksList->count());
400 // Prepare log
402 logEdit->clear();
403 updateLog("% " +path+ " " +args.join(" ")+ "\n");
405 // And finally start the process
407 cancelled = false;
408 process.setEnvironment(env);
409 process.setProcessChannelMode(QProcess::MergedChannels);
410 process.start(path,args);
413 void
414 MainWindow::onCancel() {
415 cancelled = true;
416 process.kill();
419 void
420 MainWindow::onAbout() {
421 AboutDialog(this, ccliveVersion, curlVersion).exec();
424 void
425 MainWindow::onRSS() {
426 if (rss->exec() == QDialog::Accepted) {
427 QTreeWidgetItemIterator iter(rss->itemsTree);
428 while (*iter) {
429 if ((*iter)->checkState(0) == Qt::Checked)
430 addPageLink((*iter)->text(1));
431 ++iter;
434 rss->writeSettings();
437 void
438 MainWindow::onScan() {
439 QMessageBox::information(this, QCoreApplication::applicationName(),
440 "TODO: Implement video link scan");
441 return;
442 if (scan->exec() == QDialog::Accepted) {
444 scan->writeSettings();
447 void
448 MainWindow::onPasteURL() {
449 QClipboard *cb = QApplication::clipboard();
450 QStringList lst = cb->text().split("\n");
451 for (register int i=0; i<lst.size(); ++i)
452 addPageLink(lst[i]);
455 void
456 MainWindow::onAdd() {
457 QString lnk = QInputDialog::getText(this,
458 tr("Add new video page link"), tr("Enter link:"));
459 addPageLink(lnk);
462 void
463 MainWindow::onRemove() {
465 QList<QListWidgetItem*> sel = linksList->selectedItems();
467 if (sel.size() == 0)
468 return;
470 if (QMessageBox::warning(this, tr("Remove links"),
471 tr("Really remove the selected links?"),
472 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
473 == QMessageBox::No)
475 return;
478 for (register int i=0; i<sel.size(); ++i) {
479 const int row = linksList->row(sel[i]);
480 delete linksList->takeItem(row);
484 void
485 MainWindow::onClear() {
487 if (linksList->count() == 0)
488 return;
490 if (QMessageBox::warning(this, tr("Clear list"),
491 tr("Really clear list?"),
492 QMessageBox::Yes|QMessageBox::No, QMessageBox::No)
493 == QMessageBox::No)
495 return;
498 linksList->clear();
501 void
502 MainWindow::addPageLink(QString lnk) {
503 if (lnk.isEmpty())
504 return;
506 lnk = lnk.trimmed();
508 if (!lnk.startsWith("http://", Qt::CaseInsensitive))
509 lnk.insert(0,"http://");
511 if (!ccliveSupportsHost(lnk)) {
512 QMessageBox::critical(this, QCoreApplication::applicationName(),
513 QString(tr("%1: unsupported")).arg(QUrl(lnk).host()));
514 return;
517 QList<QListWidgetItem *> found
518 = linksList->findItems(lnk, Qt::MatchExactly);
520 if (found.size() == 0)
521 linksList->addItem(lnk);
524 void
525 MainWindow::onFormats() {
526 format->exec();
527 format->writeSettings();
530 void
531 MainWindow::onProcStarted() {
532 statusBar() ->clearMessage();
533 fileLabel ->setText("-");
534 sizeLabel ->setText("-- / --");
535 rateLabel ->setText("--.-");
536 etaLabel ->setText("--:--");
537 progressBar ->setValue(0);
538 totalProgressbar->setValue(0);
540 startButton ->setEnabled(false);
541 cancelButton->setEnabled(true);
543 errorOccurred = false;
546 void
547 MainWindow::onProcError(QProcess::ProcessError err) {
548 if (err == QProcess::FailedToStart) {
549 QString msg = tr("Error: Failed to start the process.");
550 statusBar()->showMessage(msg);
551 updateLog(msg);
555 void
556 MainWindow::onProcStdoutReady() {
557 // NOTE: We read both channels stdout and stderr.
558 QString newText =
559 QString::fromLocal8Bit(process.readAll());
561 QStringList tmp = newText.split("\n", QString::SkipEmptyParts);
562 if (tmp.isEmpty())
563 return;
565 QString status, last = tmp.last();
567 if (last.startsWith("fetch http://")) {
568 status = tr("Fetching link...");
569 totalProgressbar->setValue(totalProgressbar->value()+1);
571 else if (last.startsWith("verify") || last.startsWith("query length") )
572 status = tr("Verifying video link...");
573 else if (last.startsWith("error:"))
574 errorOccurred = true;
576 if (newText.contains("file:")) {
577 QStringList tmp = newText.split(" ", QString::SkipEmptyParts);
578 fileLabel->setText(tmp[1].remove("\n"));
579 status = tr("Extracting video...");
582 if (!status.isEmpty())
583 statusBar()->showMessage(status.remove("\n"));
585 if (last.contains("%")) {
586 newText.replace("\n", " ");
588 QStringList tmp = newText.split(" ", QString::SkipEmptyParts);
589 QString percent = tmp[1].remove(QChar('%'));
590 QString now = tmp[2];
591 QString expected= tmp[4];
592 QString rate = tmp[5];
593 QString eta = tmp[6];
595 sizeLabel->setText(now +" / "+ expected);
596 progressBar->setValue(percent.toInt());
597 rateLabel->setText(rate);
598 etaLabel->setText(eta);
600 else
601 updateLog(newText);
604 void
605 MainWindow::onProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
606 QString status;
607 if (!errorOccurred) {
608 if (exitStatus == QProcess::NormalExit) {
609 status = exitCode != 0
610 ? tr("Process exited with an error. See Log for details.")
611 : tr("Process exited normally.");
612 } else {
613 status = cancelled
614 ? tr("Process terminated.")
615 : tr("Process crashed. See Log for details.");
617 updateLog(status + ".");
619 else
620 status = tr("Error(s) occurred. See Log for details.");
622 statusBar()->showMessage(status);
624 startButton ->setEnabled(true);
625 cancelButton->setEnabled(false);