Start work on merging clivefeed and clivescan into abby.
[abby.git] / src / mainwnd.cpp
blobf17fa0156189322d8082bc2c0edd784ec2ff816e
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 <QIcon>
24 #include <QClipboard>
25 #include <QDebug>
27 #include "mainwnd.h"
28 #include "prefsdlg.h"
29 #include "aboutdlg.h"
31 MainWindow::MainWindow():
32 cancelled(false)
35 The word "English" is not meant to be translated literally.
36 Replace the word "English" with target language, e.g. Suomi
37 or Deutsch. Note that the only purpose of this string is to
38 list it as an available language in the Preferences dialog.
40 const QString lang = tr("English");
42 setupUi(this);
43 readSettings();
45 setWindowIcon(QIcon(":/rc/abby.png"));
47 connect(&process, SIGNAL(started()),
48 this, SLOT(onProcStarted()));
49 connect(&process, SIGNAL(error(QProcess::ProcessError)),
50 this, SLOT(onProcError(QProcess::ProcessError)));
52 // NOTE: We read both channels stdout and stderr.
53 connect(&process, SIGNAL(readyReadStandardOutput()),
54 this, SLOT(onProcStdoutReady()));
55 connect(&process, SIGNAL(readyReadStandardError()),
56 this, SLOT(onProcStdoutReady()));
58 connect(&process, SIGNAL(finished(int,QProcess::ExitStatus)),
59 this, SLOT(onProcFinished(int,QProcess::ExitStatus)));
61 prefs = new PreferencesDialog(this);
62 updateWidgets();
65 bool
66 MainWindow::isCclive(const QString& path, QString& output) {
67 QProcess process;
68 process.setProcessChannelMode(QProcess::MergedChannels);
69 process.start(path, QStringList() << "--version");
71 bool state = false;
72 if (!process.waitForFinished())
73 qDebug() << path << ": " << process.errorString();
74 else {
75 output = QString::fromLocal8Bit(process.readAll());
76 QStringList lst = output.split(" ", QString::SkipEmptyParts);
77 state = lst[0] == "cclive";
79 return state;
82 bool
83 MainWindow::ccliveSupports(const QString& buildOption) {
84 QString output, path = prefs->ccliveEdit->text();
85 const bool _isCclive = isCclive(path,output);
87 if (!_isCclive && buildOption == "--with-perl")
88 return false; // To keep the titleBox hidden always
90 return output.contains(buildOption);
93 void
94 MainWindow::updateWidgets() {
95 // Enable widgets based on preferences and other settings.
96 QString s;
98 s = prefs->streamEdit->text();
99 streamBox->setEnabled(!s.isEmpty());
100 if (s.isEmpty())
101 streamBox->setCheckState(Qt::Unchecked);
103 s = prefs->commandEdit->text();
104 commandBox->setEnabled(!s.isEmpty());
105 if (s.isEmpty())
106 commandBox->setCheckState(Qt::Unchecked);
108 if (ccliveSupports("--with-perl")) {
109 titleBox->show();
110 } else {
111 titleBox->setCheckState(Qt::Unchecked);
112 titleBox->hide();
116 void
117 MainWindow::closeEvent(QCloseEvent *event) {
118 writeSettings();
119 event->accept();
122 void
123 MainWindow::writeSettings() {
124 QSettings s;
125 s.beginGroup("MainWindow");
126 s.setValue("size",size());
127 s.setValue("pos",pos());
128 s.setValue("titleBox",titleBox->checkState());
129 s.endGroup();
132 void
133 MainWindow::readSettings() {
134 QSettings s;
135 s.beginGroup("MainWindow");
136 resize(s.value("size",QSize(525,265)).toSize());
137 move(s.value("pos",QPoint(200,200)).toPoint());
138 titleBox->setCheckState(
139 s.value("titleBox").toBool()
140 ? Qt::Checked
141 : Qt::Unchecked);
142 s.endGroup();
145 void
146 MainWindow::updateLog(const QString& newText) {
147 QString text = logEdit->toPlainText() + newText;
148 logEdit->setPlainText(text);
151 void
152 MainWindow::updateFormats() {
154 // Alter widgets dynamically based on the video URL.
156 QString url = urlEdit->toPlainText();
158 if (url.isEmpty())
159 return;
161 struct lookup_s {
162 const char *host;
163 const char *formats;
165 static const struct lookup_s lookup[] = {
166 {"youtube.com", "fmt17|fmt18|fmt22|fmt35"},
167 // {"video.google.", "mp4"}, // missing in cclive 0.4.3+
168 {"dailymotion.com", "spak-mini|vp6-hq|vp6-hd|vp6|h264"},
171 const int c = sizeof(lookup)/sizeof(struct lookup_s);
173 QStringList formats;
174 formats << "flv" << "best";
176 for (register int i=0; i<c; ++i) {
177 if (url.contains(lookup[i].host)) {
178 QString s = lookup[i].formats;
179 formats << s.split("|");
180 break;
184 QString last = formatCombo->currentText();
186 formatCombo->clear();
187 formatCombo->addItems(formats);
189 int n = formatCombo->findText(last);
190 if (n != -1)
191 formatCombo->setCurrentIndex(n);
195 // Slots
197 void
198 MainWindow::onPreferences() {
199 prefs->exec();
200 updateWidgets();
203 void
204 MainWindow::onStreamStateChanged(int state) {
205 streamSpin->setEnabled(state != 0);
208 void
209 MainWindow::onStart() {
210 // Check video URL
212 QString url = urlEdit->toPlainText();
214 if (url.isEmpty()) {
215 statusBar()->showMessage(tr("Enter a video link."));
216 return;
219 url = url.trimmed();
221 if (!url.startsWith("http://",Qt::CaseInsensitive))
222 url.insert(0,"http://");
224 // Check cclive
226 QString path = prefs->ccliveEdit->text();
227 if (path.isEmpty()) {
228 QMessageBox::information(this,QCoreApplication::applicationName(),
229 tr("Path to cclive (or clive) command undefined. "
230 "See preferences."));
231 onPreferences();
232 return;
235 QString output;
236 const bool _isCclive = isCclive(path,output);
238 // Check video save directory
240 // NOTE: cclive itself does not support this concept so we
241 // work around it by changing the working directory to the
242 // save directory.
244 QString savedir = prefs->savedirEdit->text();
245 if (savedir.isEmpty()) {
246 QMessageBox::information(this,QCoreApplication::applicationName(),
247 tr("Save directory undefined. See preferences."));
248 onPreferences();
249 return;
252 // clive can use this same approach even if --savedir option exists.
253 process.setWorkingDirectory(savedir);
255 // Construct cclive/clive args
257 QStringList args;
258 QStringList env;
260 if (_isCclive) {
261 args << "--print-fname";
262 } else {
263 args << "--stderr";
264 // Set environment variables for clive
265 env << "COLUMNS=80" << "LINES=24" // Term::ReadKey
266 << QString("HOME=%1").arg(QDir::homePath()); // $env{HOME}
269 QString s = prefs->additionalEdit->text();
270 if (!s.isEmpty())
271 args << s;
273 s = prefs->streamEdit->text();
274 if (!s.isEmpty() && streamBox->isChecked()) {
275 args << QString("--stream-exec=%1").arg(s);
276 args << QString("--stream=%1").arg(streamSpin->value());
279 s = prefs->commandEdit->text();
280 if (!s.isEmpty() && commandBox->isChecked()) {
281 if (!s.endsWith(";"))
282 s += ";";
283 args << QString("--exec=%1").arg(s);
286 if (prefs->proxyCombo->currentIndex() == 0)
287 args << "--no-proxy";
288 else {
289 s = prefs->proxyEdit->text();
290 if (!s.isEmpty())
291 args << QString("--proxy=%1").arg(s);
294 if (prefs->limitBox->checkState()) {
295 int n = prefs->limitSpin->value();
296 args << QString("--limit-rate=%1").arg(n);
299 if (prefs->timeoutBox->checkState()) {
300 int n = prefs->timeoutSpin->value();
301 if (!prefs->socksBox->checkState())
302 args << QString("--connect-timeout=%1").arg(n);
303 else
304 args << QString("--connect-timeout-socks=%1").arg(n);
307 #ifdef MAKE_CONTINUE_CONDITIONAL
308 if (continueBox->isChecked())
309 args << "--continue";
310 #else
311 args << "--continue";
312 #endif
314 if (_isCclive) { // clive defaults to this
315 if (titleBox->isChecked()) {
316 args << "--title";
317 s = prefs->cclassEdit->text();
318 if (!s.isEmpty())
319 args << QString("--cclass=%1").arg(s);
321 } else { // this clive can use
322 s = prefs->cclassEdit->text();
323 if (!s.isEmpty())
324 args << QString("--cclass=%1").arg(s);
327 s = formatCombo->currentText();
328 if (s.isEmpty())
329 s = "flv";
331 args << QString("--format=%1").arg(s);
332 args << QString("%1").arg(url);
334 // Prepare log
336 logEdit->clear();
337 updateLog("% " +path+ " " +args.join(" ")+ "\n");
339 // And finally start the process
341 cancelled = false;
342 process.setEnvironment(env);
343 process.setProcessChannelMode(QProcess::MergedChannels);
344 process.start(path,args);
347 void
348 MainWindow::onCancel() {
349 cancelled = true;
350 process.kill();
353 void
354 MainWindow::onAbout() {
355 AboutDialog about(this,prefs->ccliveEdit->text());
356 about.exec();
359 void
360 MainWindow::onURLEditingFinished() {
361 updateFormats();
364 void
365 MainWindow::onURLReturnPressed() {
366 onStart();
369 void
370 MainWindow::onFormatStateChanged(int) {
371 QString url = urlEdit->toPlainText();
373 if (url.isEmpty())
374 return;
376 #ifdef FLV_CANNOT_RESUME
378 // Disable --continue for the specified hosts if format is "flv".
379 // Make changes based on the video URL.
381 struct lookup_s {
382 const char *host;
384 static const struct lookup_s lookup[] = {
385 {"youtube.com"},
386 {"video.google."},
389 const int c = sizeof(lookup)/sizeof(struct lookup_s);
390 bool enable = true;
392 for (register int i=0; i<c; ++i) {
393 if (url.contains(lookup[i].host)) {
394 if (formatCombo->currentText() == "flv") {
395 enable = false;
396 break;
401 continueBox->setEnabled(enable);
403 if (continueBox->isChecked() && !enable)
404 continueBox->setCheckState(Qt::Unchecked);
405 #endif // FLV_CANNOT_RESUME
408 void
409 MainWindow::onPasteURL() {
410 QClipboard *cb = QApplication::clipboard();
411 urlEdit->setText(cb->text());
412 updateFormats();
415 void
416 MainWindow::onProcStarted() {
417 statusBar() ->clearMessage();
418 fileLabel ->setText("-");
419 sizeLabel ->setText("-- / --");
420 rateLabel ->setText("--.-");
421 etaLabel ->setText("--:--");
422 progressBar ->setValue(0);
424 startButton ->setEnabled(false);
425 cancelButton->setEnabled(true);
427 errorOccurred = false;
430 void
431 MainWindow::onProcError(QProcess::ProcessError err) {
432 if (err == QProcess::FailedToStart) {
433 QString msg = tr("error: failed to start process");
434 statusBar()->showMessage(msg);
435 updateLog(msg);
439 void
440 MainWindow::onProcStdoutReady() {
441 // NOTE: We read both channels stdout and stderr.
442 QString newText =
443 QString::fromLocal8Bit(process.readAll());
445 QStringList tmp = newText.split("\n", QString::SkipEmptyParts);
446 if (tmp.isEmpty())
447 return;
449 QString status, last = tmp.last();
450 //qDebug() << last;
452 if (last.startsWith("fetch"))
453 status = tr("Fetching link...");
454 else if (last.startsWith("verify") || last.startsWith("query length") )
455 status = tr("Verifying video link...");
456 else if (last.startsWith("error:"))
457 errorOccurred = true;
459 if (newText.contains("file:")) {
460 QStringList tmp = newText.split(" ", QString::SkipEmptyParts);
461 fileLabel->setText(tmp[1].remove("\n"));
462 status = tr("Extracting video...");
465 if (!status.isEmpty())
466 statusBar()->showMessage(status.remove("\n"));
468 if (last.contains("%")) {
469 newText.replace("\n", " ");
471 QStringList tmp = newText.split(" ", QString::SkipEmptyParts);
472 QString percent = tmp[1].remove(QChar('%'));
473 QString now = tmp[2];
474 QString expected= tmp[4];
475 QString rate = tmp[5];
476 QString eta = tmp[6];
478 sizeLabel->setText(now +" / "+ expected);
479 progressBar->setValue(percent.toInt());
480 rateLabel->setText(rate);
481 etaLabel->setText(eta);
483 else
484 updateLog(newText);
487 void
488 MainWindow::onProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
489 QString status;
490 if (!errorOccurred) {
491 if (exitStatus == QProcess::NormalExit) {
492 status = exitCode != 0
493 ? tr("Process exited with an error; see log")
494 : tr("Process exited normally");
495 } else {
496 status = cancelled
497 ? tr("Process terminated")
498 : tr("Process crashed; see log");
500 updateLog(status + ".");
502 else
503 status = tr("error: see log for details");
505 statusBar()->showMessage(status);
507 startButton ->setEnabled(true);
508 cancelButton->setEnabled(false);