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>
20 #include <QCloseEvent>
21 #include <QMessageBox>
22 #include <QFileDialog>
25 #include <QInputDialog>
26 #include <QNetworkProxy>
34 #include "formatdlg.h"
37 #define critCcliveNotFound \
39 QMessageBox::critical(this, QCoreApplication::applicationName(), \
40 QString( tr("c/clive executable not found, please check the path.") )); \
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");
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);
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
) ));
86 connect(linksList
, SIGNAL( itemDoubleClicked(QListWidgetItem
*) ),
87 this, SLOT( onItemDoubleClicked(QListWidgetItem
*) ));
90 if (parseCcliveHostsOutput())
91 parseCcliveVersionOutput();
97 streamBox
->setHidden(true);
98 streamSpin
->setHidden(true);
103 MainWindow::parseCcliveHostsOutput() {
106 QString path
= prefs
->ccliveEdit
->text();
108 if (path
.isEmpty()) {
110 QMessageBox::information(
112 QCoreApplication::applicationName(),
113 tr("abby requires `clive' or `cclive'. "
114 "Please define path to either executable.")
124 proc
.setProcessChannelMode(QProcess::MergedChannels
);
125 proc
.start(path
, QStringList() << "--hosts");
127 if (!proc
.waitForFinished()) {
128 qDebug() << path
<< ": " << proc
.errorString();
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
);
155 MainWindow::parseCcliveVersionOutput() {
157 versionOutput
.clear();
158 ccliveVersion
.clear();
161 QString path
= prefs
->ccliveEdit
->text();
164 proc
.setProcessChannelMode(QProcess::MergedChannels
);
165 proc
.start(path
, QStringList() << "--version");
167 isCcliveFlag
= false;
169 if (!proc
.waitForFinished())
170 qDebug() << path
<< ": " << proc
.errorString();
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];
185 MainWindow::ccliveSupportsFeature(const QString
& buildOption
) {
186 return versionOutput
.contains(buildOption
);
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)
207 MainWindow::updateWidgets(const bool updateCcliveDepends
) {
208 // Enable widgets based on preferences and other settings.
211 s
= prefs
->streamEdit
->text();
212 streamBox
->setEnabled(!s
.isEmpty());
214 streamBox
->setCheckState(Qt::Unchecked
);
216 s
= prefs
->commandEdit
->text();
217 commandBox
->setEnabled(!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.
225 regexpLabel
->show();
228 cclassLabel
->hide();
232 regexpLabel
->hide();
235 cclassLabel
->show();
242 MainWindow::closeEvent(QCloseEvent
*event
) {
248 MainWindow::writeSettings() {
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());
261 MainWindow::readSettings() {
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()
271 cclassEdit
->setText( s
.value("cclassEdit").toString() );
272 fnamefmtEdit
->setText( s
.value("fnamefmtEdit").toString() );
277 MainWindow::updateLog(const QString
& newText
) {
278 QString text
= logEdit
->toPlainText() + newText
;
279 logEdit
->setPlainText(text
);
286 MainWindow::onPreferences() {
288 QString old
= prefs
->ccliveEdit
->text();
292 QString _new
= prefs
->ccliveEdit
->text();
295 parseCcliveHostsOutput();
296 parseCcliveVersionOutput();
299 updateWidgets(old
!= _new
);
305 MainWindow::setProxy() {
306 if (!prefs
->proxyEdit
->text().isEmpty()
307 && prefs
->proxyCombo
->currentIndex() > 0)
309 QUrl
url(prefs
->proxyEdit
->text());
312 QNetworkProxy::HttpProxy
,
317 QNetworkProxy::setApplicationProxy(proxy
);
320 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy
);
324 MainWindow::onStreamStateChanged(int state
) {
325 streamSpin
->setEnabled(state
!= 0);
329 MainWindow::onStart() {
331 if (linksList
->count() == 0) {
333 if (linksList
->count() == 0)
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."));
352 process
.setWorkingDirectory(savedir
);
354 // Construct cclive/clive 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(";"))
370 args
<< QString("--exec=%1").arg(s
);
371 args
<< QString("--exec-run");
374 if (prefs
->proxyCombo
->currentIndex() == 0)
375 args
<< "--no-proxy";
377 s
= prefs
->proxyEdit
->text();
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
);
392 args
<< QString("--connect-timeout-socks=%1").arg(n
);
398 s
= regexpEdit
->text();
400 args
<< QString("--regexp=%1").arg(s
);
401 if (findallBox
->checkState())
402 args
<< QString("--find-all");
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();
413 args
<< QString("--cclass=%1").arg(s
);
416 s
= fnamefmtEdit
->text();
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());
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()) {
439 // Use format dialog setting for the host.
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());
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
);
464 MainWindow::onCancel() {
465 cancelledFlag
= true;
470 MainWindow::onAbout() {
471 AboutDialog(this, ccliveVersion
, curlVersion
).exec();
474 #define fillList(dlg) \
476 QTreeWidgetItemIterator iter(dlg->itemsTree); \
478 if ((*iter)->checkState(0) == Qt::Checked) \
479 addPageLink((*iter)->text(1)); \
485 MainWindow::onRSS() {
486 if (rss
->exec() == QDialog::Accepted
) {
489 rss
->writeSettings();
493 MainWindow::onScan() {
494 if (scan
->exec() == QDialog::Accepted
) {
497 scan
->writeSettings();
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
)
512 MainWindow::onAdd() {
513 addPageLink(QInputDialog::getText(this,
514 QCoreApplication::applicationName(), tr("Add link:")));
518 MainWindow::onRemove() {
520 QList
<QListWidgetItem
*> sel
= linksList
->selectedItems();
525 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
526 tr("Really remove the selected links?"),
527 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
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
);
542 MainWindow::onClear() {
544 if (linksList
->count() == 0)
547 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
548 tr("Really clear list?"),
549 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
559 MainWindow::addPageLink(QString lnk
) {
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()));
575 QList
<QListWidgetItem
*> found
576 = linksList
->findItems(lnk
, Qt::MatchExactly
);
578 if (found
.size() == 0)
579 linksList
->addItem(lnk
);
583 MainWindow::onFormats() {
584 if (hosts
.isEmpty()) {
590 format
->saveCurrent();
591 format
->writeSettings();
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);
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
);
640 MainWindow::onProcStdoutReady() {
642 // NOTE: We read both channels stdout and stderr.
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
);
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");
668 fileLabel
->setText( re
.capturedTexts()[1].simplified() );
669 sb
->showMessage( tr("Downloading video ...") );
672 else if (ln
.startsWith("error:"))
677 appendLogFlag
= false;
679 // In an parallel world I have written a cleaner regexp.
680 static const char progress_re
[] =
682 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
683 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
686 QRegExp
re(progress_re
);
688 if (re
.indexIn(ln
)) {
689 QStringList cap
= re
.capturedTexts();
693 if (cap
[0].isEmpty())
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")
721 .arg(cap
[SPEED_TYPE
]));
723 etaLabel
->setText(cap
[ETA
].simplified());
728 updateLog(ln
+ "\n");
730 memset(&data
, 0, sizeof(data
));
735 MainWindow::onProcFinished(int exitCode
, QProcess::ExitStatus exitStatus
) {
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");
743 status
= cancelledFlag
744 ? tr("Process terminated")
745 : tr("Process crashed. See Log for details");
747 updateLog(status
+ ".");
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);
779 MainWindow::onItemDoubleClicked(QListWidgetItem
*item
) {
782 QString lnk
= QInputDialog::getText(this,
783 QCoreApplication::applicationName(), tr("Edit link:"),
784 QLineEdit::Normal
, item
->text(), &ok
);
786 if (ok
&& !lnk
.isEmpty())