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"
38 #define critCcliveProcessFailed(parent, msg) \
40 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
41 QString( tr("Error while trying to run c/clive:\n%1") ).arg(msg)); \
44 #define critCcliveNotSpecified(parent) \
46 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
47 QString( tr("c/clive executable not found, please check the path.") )); \
50 #define critCcliveExitedWithError(parent,code,msg) \
52 QMessageBox::critical(parent, QCoreApplication::applicationName(), \
53 QString( tr("c/clive exited with error code %1:\n%2") ) \
54 .arg(code).arg(msg)); \
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");
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);
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
) ));
101 connect(linksList
, SIGNAL( itemDoubleClicked(QListWidgetItem
*) ),
102 this, SLOT( onItemDoubleClicked(QListWidgetItem
*) ));
105 if (parseCcliveVersionOutput())
106 parseCcliveHostsOutput();
109 updateWidgets (true);
112 streamBox
->setHidden(true);
113 streamSpin
->setHidden(true);
117 setAcceptDrops(true);
121 MainWindow::parseCcliveHostsOutput() {
124 QString path
= prefs
->ccliveEdit
->text();
126 if (path
.isEmpty()) {
128 QMessageBox::information(
130 QCoreApplication::applicationName(),
131 tr("abby requires `clive' or `cclive'. "
132 "Please define path to either executable.")
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() );
152 const QString output
=
153 QString::fromLocal8Bit( proc
.readAll() );
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
);
178 critCcliveExitedWithError(this, exitCode
, output
);
187 MainWindow::parseCcliveVersionOutput() {
190 prefs
->ccliveEdit
->text();
193 if (path
.isEmpty()) {
201 if (!path
.isEmpty()) {
202 prefs
->ccliveEdit
->setText(path
);
207 Util::verifyCclivePath(
215 catch (const NoCcliveException
& x
) {
216 QMessageBox::warning(this, tr("Warning"), x
.what());
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)
240 MainWindow::updateWidgets(const bool updateCcliveDepends
) {
241 // Enable widgets based on preferences and other settings.
244 s
= prefs
->streamEdit
->text();
245 streamBox
->setEnabled(!s
.isEmpty());
247 streamBox
->setCheckState(Qt::Unchecked
);
249 s
= prefs
->commandEdit
->text();
250 commandBox
->setEnabled(!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.
258 regexpLabel
->show();
261 cclassLabel
->hide();
265 regexpLabel
->hide();
268 cclassLabel
->show();
275 MainWindow::closeEvent(QCloseEvent
*event
) {
276 int rc
= QMessageBox::Yes
;
277 if (process
.state() != QProcess::NotRunning
) {
278 rc
= QMessageBox::warning(
281 tr("c/clive process is still active, really close abby?"),
282 QMessageBox::Yes
|QMessageBox::No
286 if (rc
== QMessageBox::Yes
) {
296 MainWindow::writeSettings() {
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());
309 MainWindow::readSettings() {
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() );
325 MainWindow::onPreferences() {
327 QString old
= prefs
->ccliveEdit
->text();
331 QString _new
= prefs
->ccliveEdit
->text();
334 if (parseCcliveVersionOutput())
335 parseCcliveHostsOutput();
338 updateWidgets(old
!= _new
);
344 MainWindow::setProxy() {
345 if (!prefs
->proxyEdit
->text().isEmpty()
346 && prefs
->proxyCombo
->currentIndex() > 0)
348 QUrl
url(prefs
->proxyEdit
->text());
351 QNetworkProxy::HttpProxy
,
356 QNetworkProxy::setApplicationProxy(proxy
);
359 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy
);
363 MainWindow::onStreamStateChanged(int state
) {
364 streamSpin
->setEnabled(state
!= 0);
368 MainWindow::onStart() {
370 if ( ccliveVersion
.isEmpty() ) {
371 critCcliveNotSpecified(this);
376 if (linksList
->count() == 0) {
378 if (linksList
->count() == 0)
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."));
397 process
.setWorkingDirectory(savedir
);
399 // Construct cclive/clive 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(";"))
415 args
<< QString("--exec=%1").arg(s
);
416 args
<< QString("--exec-run");
419 if (prefs
->proxyCombo
->currentIndex() == 0)
420 args
<< "--no-proxy";
422 s
= prefs
->proxyEdit
->text();
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
);
437 args
<< QString("--connect-timeout-socks=%1").arg(n
);
443 s
= regexpEdit
->text();
445 args
<< QString("--regexp=%1").arg(s
);
446 s
= substEdit
->text();
448 args
<< QString("--substitute=%1").arg(s
);
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();
460 args
<< QString("--cclass=%1").arg(s
);
463 s
= fnamefmtEdit
->text();
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());
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()) {
486 // Use format dialog setting for the host.
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());
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
);
511 MainWindow::onCancel() {
512 cancelledFlag
= true;
517 MainWindow::onAbout() {
518 AboutDialog(this, ccliveVersion
, isCcliveFlag
, libName
, libVersion
).exec();
521 #define fillList(dlg) \
523 QTreeWidgetItemIterator iter(dlg->itemsTree); \
525 if ((*iter)->checkState(0) == Qt::Checked) \
526 addPageLink((*iter)->text(1)); \
532 MainWindow::onRSS() {
533 if (rss
->exec() == QDialog::Accepted
) {
536 rss
->writeSettings();
540 MainWindow::onScan() {
541 if (scan
->exec() == QDialog::Accepted
) {
544 scan
->writeSettings();
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
)
559 MainWindow::onAdd() {
560 addPageLink(QInputDialog::getText(this,
561 QCoreApplication::applicationName(), tr("Add link:")));
565 MainWindow::onRemove() {
567 QList
<QListWidgetItem
*> sel
= linksList
->selectedItems();
572 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
573 tr("Really remove the selected links?"),
574 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
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
);
589 MainWindow::onClear() {
591 if (linksList
->count() == 0)
594 if (QMessageBox::warning(this, QCoreApplication::applicationName(),
595 tr("Really clear list?"),
596 QMessageBox::Yes
|QMessageBox::No
, QMessageBox::No
)
606 MainWindow::addPageLink(QString lnk
) {
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()));
622 QList
<QListWidgetItem
*> found
623 = linksList
->findItems(lnk
, Qt::MatchExactly
);
625 if (found
.size() == 0)
626 linksList
->addItem(lnk
);
630 MainWindow::onFormats() {
631 if ( hosts
.isEmpty() || ccliveVersion
.isEmpty() ) {
632 critCcliveNotSpecified(this);
638 format
->saveCurrent();
639 format
->writeSettings();
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);
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
);
686 MainWindow::onProcStdoutReady() {
688 // NOTE: We read both channels stdout and stderr.
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
);
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");
714 fileLabel
->setText( re
.capturedTexts()[1].simplified() );
715 sb
->showMessage( tr("Downloading video ...") );
718 else if (ln
.startsWith("error:")) {
724 appendLogFlag
= false;
726 // In some parallel world I have written a cleaner regexp.
727 static const char progress_re
[] =
729 "\\s+(\\d+)\\.(\\d+)M\\s+\\/\\s+(\\d+)\\.(\\d+)M" // xxM / yyM
730 "\\s+(\\d+)\\.(\\d+)(\\w)\\/\\w" // speed
733 QRegExp
re(progress_re
);
735 if (re
.indexIn(ln
)) {
736 QStringList cap
= re
.capturedTexts();
740 if (cap
[0].isEmpty())
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")
768 .arg(cap
[SPEED_TYPE
]));
770 etaLabel
->setText(cap
[ETA
].simplified());
775 Util::appendLog(logEdit
, ln
);
777 memset(&data
, 0, sizeof(data
));
782 MainWindow::onProcFinished(int exitCode
, QProcess::ExitStatus exitStatus
) {
786 switch (exitStatus
) {
787 case QProcess::NormalExit
:
790 status
= tr("c/clive exited normally.");
794 QString(tr("c/clive exited with code %1, see log."))
800 status
= cancelledFlag
801 ? tr("c/clive terminated by user.")
802 : tr("c/clive crashed, see log.");
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);
834 MainWindow::onItemDoubleClicked(QListWidgetItem
*item
) {
837 QString lnk
= QInputDialog::getText(this,
838 QCoreApplication::applicationName(), tr("Edit link:"),
839 QLineEdit::Normal
, item
->text(), &ok
);
841 if (ok
&& !lnk
.isEmpty())
846 MainWindow::dragEnterEvent(QDragEnterEvent
*event
) {
847 if (event
->mimeData()->hasText())
848 event
->acceptProposedAction();
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
)
859 event
->acceptProposedAction();