2 * Copyright (C) 2010 Toni Gundogdu.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 #include <QMainWindow>
19 #include <QCloseEvent>
20 #include <QFileDialog>
29 #include "Preferences.h"
32 #include "YoutubeFeed.h"
33 #include "MainWindow.h"
35 #define QSETTINGS_GROUP "MainWindow"
36 #define QSETTINGS_REMINDER_SHOWTIPS "showTips"
40 extern QMap
<QString
,QStringList
> hosts
;
41 extern SharedPreferences shPrefs
;
42 extern History history
;
47 enum { StreamVideo
=0, DownloadVideo
};
51 MainWindow::MainWindow ()
58 #define _wrap(sig,slot) \
60 connect(&proc, SIGNAL(sig), this, SLOT(slot)); \
63 _wrap(started(), onQuviStarted());
65 _wrap(error(QProcess::ProcessError
),
66 onQuviError(QProcess::ProcessError
));
68 _wrap(readyRead(), onQuviReadyRead());
70 _wrap(finished(int,QProcess::ExitStatus
),
71 onQuviFinished(int,QProcess::ExitStatus
));
77 if (shPrefs
.get(SharedPreferences::StayOnTop
).toBool())
78 setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint
);
80 // Create Video instance.
84 // Create context menu.
88 // Create system tray icon.
92 // Create Download dialog.
94 download
= new Download (this);
96 download
->setLabelText (tr ("Copying..."));
98 download
->setCancelButtonText (tr ("&Abort"));
100 connect (download
, SIGNAL (error (QString
)),
101 this, SLOT (onDownloadError (QString
)));
103 proc
.setProcessChannelMode (QProcess::MergedChannels
);
105 log
<< tr ("Program started.") + "\n";
109 MainWindow::createContextMenu () {
111 #define creat_a(t,f,c) \
113 QAction *a = new QAction (t, textBrowser); \
115 a->setCheckable (c); \
116 /*connect (a, SIGNAL(toggled (bool)), SLOT(f (bool)));*/ \
119 connect (a, SIGNAL(triggered ()), SLOT(f ())); \
120 textBrowser->addAction (a); \
126 QAction *a = new QAction (textBrowser); \
127 a->setSeparator (true); \
128 textBrowser->addAction (a); \
131 creat_a (tr("Address..."), onAddress
, false);
132 creat_a (tr("Feed..."), onFeed
, false);
134 creat_a (tr("Rake..."), onRake
, false);
136 creat_a (tr("History..."), onHistory
, false);
138 creat_a (tr("Overwrite"), _
, true);
139 creat_a (tr("Stop"), onStop
, false);
141 creat_a (tr("Log..."), onLog
, false);
142 creat_a (tr("Preferences..."), onPreferences
, false);
144 creat_a (tr("About..."), onAbout
, false);
145 creat_a (tr("Quit"), close
, false );
150 // Add key shortcuts.
152 actions
[tr("Address...")]->setShortcut (QKeySequence(tr("Ctrl+A")));
153 actions
[tr("Feed...")]->setShortcut (QKeySequence(tr("Ctrl+F")));
155 actions
[tr("Rake...")]->setShortcut (QKeySequence(tr("Ctrl+C")));
157 actions
[tr("Overwrite")]->setShortcut(QKeySequence(tr("Ctrl+W")));
158 actions
[tr("Stop")]->setShortcut (QKeySequence(tr("Ctrl+S")));
159 actions
[tr("Log...")]->setShortcut (QKeySequence(tr("Ctrl+L")));
160 actions
[tr("History...")]->setShortcut (QKeySequence(tr("Ctrl+R")));
161 actions
[tr("Preferences...")]->setShortcut (QKeySequence(tr("Ctrl+E")));
162 actions
[tr("Quit")]->setShortcut (QKeySequence(tr("Ctrl+Q")));
164 // Add the context menu.
166 textBrowser
->setContextMenuPolicy (Qt::ActionsContextMenu
);
172 MainWindow::createTray () {
174 trayMenu
= new QMenu(this);
176 #define creat_a(t,f) \
178 QAction *a = new QAction(t, this); \
179 connect(a, SIGNAL(triggered()), this, SLOT(f)); \
181 trayMenu->addAction(a); \
184 creat_a ( tr("Open"), showNormal() );
185 trayMenu
->addSeparator();
186 creat_a ( tr("Quit"), close() );
190 trayIcon
= new QSystemTrayIcon(this);
192 trayIcon
->setContextMenu(trayMenu
);
193 trayIcon
->setIcon(QIcon(":img/nomnom.png"));
195 connect(trayIcon
, SIGNAL( activated(QSystemTrayIcon::ActivationReason
) ),
196 this, SLOT( onTrayActivated(QSystemTrayIcon::ActivationReason
) ) );
202 MainWindow::showEvent (QShowEvent
*e
) {
210 MainWindow::hideEvent (QHideEvent
*e
) {
212 if (QSystemTrayIcon::isSystemTrayAvailable()
213 && shPrefs
.get(SharedPreferences::MinToTray
).toBool())
223 still_running (QWidget
*p
) {
225 QObject::tr ("Stop the running quvi(1) process first, and try again."));
231 MainWindow::closeEvent (QCloseEvent
*e
) {
233 int rc
= QMessageBox::Yes
;
235 if (proc
.state() != QProcess::NotRunning
) {
238 tr("quvi is still running, really quit?")
242 if (rc
== QMessageBox::Yes
)
250 NomNom::save_size(s
, this, QSETTINGS_GROUP
);
251 NomNom::save_pos(s
, this, QSETTINGS_GROUP
);
253 s
.beginGroup(QSETTINGS_GROUP
);
254 s
.setValue("modeCBox", modeCBox
->currentIndex());
265 MainWindow::readSettings () {
269 NomNom::restore_size(s
, this, QSETTINGS_GROUP
, QSize(130,150));
270 NomNom::restore_pos(s
, this, QSETTINGS_GROUP
);
272 s
.beginGroup(QSETTINGS_GROUP
);
273 modeCBox
->setCurrentIndex(s
.value("modeCBox").toInt());
276 Reminder (this, "SharedPreferences").conditionalExec ();
278 // Re-read (now updated) "showReminder" value. This was previously done
279 // in main.cpp, but has to be done here if we expect the Preferences
280 // "show tips" box value to match the one set in the Reminder dialog.
285 // Handle (dropped) URL.
288 MainWindow::handleURL (const QString
& url
) {
290 if (proc
.state() != QProcess::NotRunning
) {
291 still_running (this);
297 const QString quviPath
=
298 shPrefs
.get (SharedPreferences::QuviPath
).toString ().simplified ();
300 if (quviPath
.isEmpty ()) {
301 NomNom::crit (this, tr ("You must specify path to the quvi command."));
306 if (hosts
.isEmpty ()) {
307 const bool ok
= NomNom::parse_quvi_support (this, quviPath
);
308 if (!ok
) return false;
311 const QString playerPath
=
312 shPrefs
.get (SharedPreferences::PlayerPath
).toString ().simplified ();
314 if (playerPath
.isEmpty ()) {
317 tr("You must specify path to a stream-capable media "
332 quviPath
.split (" ").replaceInStrings ("%u", url
);
338 foreach (QString key
, hosts
.keys ()) {
340 if (re
.indexIn (url
) != -1) {
341 formats
= hosts
.value (key
);
346 if (formats
.size () > 1) { // Assume "default" is always present.
348 QString s
= QInputDialog::getItem (
350 tr ("Choose format"),
363 log
<< args
.join (" ") + "\n";
368 proc
.start (args
.takeFirst (), args
);
373 // View video (stream).
376 MainWindow::streamVideo () {
379 shPrefs
.get (SharedPreferences::PlayerPath
)
380 .toString ().simplified ().split (" ");
382 args
= args
.replaceInStrings ("%u", video
->get (Video::Link
).toString ());
384 log
<< args
.join(" ") + "\n";
386 const bool ok
= QProcess::startDetached (args
.takeFirst (), args
);
390 tr ("Unable to start player command, check the Preferences."));
394 // Download video (to a file).
397 MainWindow::downloadVideo () {
400 shPrefs
.get (SharedPreferences::FilenameFormat
).toString ();
402 const QString suffix
=
403 video
->get (Video::Suffix
).toString ().simplified ();
405 bool ok
= NomNom::format_filename (
407 shPrefs
.get (SharedPreferences::Regexp
).toString (),
408 video
->get (Video::Title
).toString ().simplified (),
410 video
->get (Video::Host
).toString ().simplified (),
411 video
->get (Video::ID
).toString ().simplified (),
419 shPrefs
.get (SharedPreferences::SaveDir
).toString ();
421 if (fpath
.isEmpty ())
422 fpath
= QDir::currentPath ();
424 fpath
= QDir::toNativeSeparators (fpath
+"/"+ fname
);
426 const bool dont_prompt
=
427 shPrefs
.get (SharedPreferences::DontPromptFilename
).toBool ();
431 const QFileDialog::Options opts
=
432 QFileDialog::DontConfirmOverwrite
;
434 fpath
= QFileDialog::getSaveFileName (
436 tr ("Save video as"),
439 0, // Selected filter.
443 if (fpath
.isEmpty ())
447 if (actions
[tr("Overwrite")]->isChecked ())
448 QDir ().remove (fpath
);
450 const qint64 expected_bytes
=
451 video
->get (Video::Length
).toLongLong ();
453 if (QFileInfo (fpath
).size () != expected_bytes
) {
456 shPrefs
.get (SharedPreferences::CurlPath
).toString ().simplified ();
458 download
->start (cmd
, fpath
, video
);
461 const bool completeFile
=
462 QFileInfo (fpath
).size () == expected_bytes
;
464 const bool playWhenDone
=
465 shPrefs
.get (SharedPreferences::PlayWhenDone
).toBool ();
467 if (completeFile
&& playWhenDone
) {
470 shPrefs
.get (SharedPreferences::PlayerPath
)
471 .toString ().simplified ().split (" ");
473 args
= args
.replaceInStrings ("%u", fpath
);
475 log
<< args
.join(" ") + "\n";
477 const bool ok
= QProcess::startDetached (args
.takeFirst (), args
);
481 tr ("Unable to start player command, check the Preferences."));
488 // Parse JSON data returned by quvi.
491 MainWindow::parseOK () {
493 QString error
= tr ("Could not find the JSON data in quvi output.");
494 const int n
= json
.indexOf ("{");
497 if (video
->fromJSON (json
.mid (n
), error
))
501 NomNom::crit (this, error
);
506 // Parse error from data returned by quvi.
509 MainWindow::parseError () {
511 QRegExp
re ("error:\\s+(.*)$");
513 QString error
= tr ("Unknown error");
515 if (re
.indexIn (json
) != -1)
518 NomNom::crit (this, error
.simplified ());
521 // Slot: Icon activated.
524 MainWindow::onTrayActivated (QSystemTrayIcon::ActivationReason r
) {
526 case QSystemTrayIcon::DoubleClick
: showNormal(); break;
531 // Slot: Preferences.
534 MainWindow::onPreferences () {
536 Preferences
dlg (this);
538 if (dlg
.exec () == QDialog::Accepted
) {
540 // User requested app restart.
542 if (dlg
.restartAfter ()) {
544 QProcess::startDetached (QCoreApplication::applicationFilePath ());
548 Qt::WindowFlags flags
= windowFlags ();
550 // Update window flags: Stay on top.
552 if (shPrefs
.get (SharedPreferences::StayOnTop
).toBool () ) {
554 if (! (flags
& Qt::WindowStaysOnTopHint
) )
555 flags
|= Qt::WindowStaysOnTopHint
;
559 if (flags
& Qt::WindowStaysOnTopHint
)
560 flags
&= ~Qt::WindowStaysOnTopHint
;
563 if (flags
!= windowFlags ()) {
564 setWindowFlags (flags
);
575 MainWindow::onAbout ()
576 { About(this).exec(); }
581 MainWindow::onHistory () {
583 const QStringList lst
= history
.toStringList();
585 if (lst
.size() == 0) {
586 NomNom::info (this, tr ("No record of previously visited URLs found."));
590 if (proc
.state() != QProcess::NotRunning
) {
591 still_running (this);
597 const QString s
= QInputDialog::getItem(
617 { LogView (this).exec (); }
622 MainWindow::onAddress() {
624 if (proc
.state() != QProcess::NotRunning
) {
625 still_running (this);
630 QInputDialog::getText (this, tr ("Address"), tr ("Video URL:"));
638 // Slot: Stop quvi process.
641 MainWindow::onStop () {
642 if (proc
.state() != QProcess::NotRunning
) {
649 extern QHash
<QString
,QString
> feed
;
654 MainWindow::onFeed () {
657 shPrefs
.get (SharedPreferences::UmphPath
).toString ();
659 if (path
.isEmpty ()) {
661 tr ("Specify path to the umph(1) command in the Preferences."));
665 if (proc
.state() != QProcess::NotRunning
) {
666 still_running (this);
672 if (!feed
.empty ()) {
674 const int rc
= NomNom::ask (this, tr ("Choose from old results?"));
676 if (rc
== QMessageBox::Yes
) {
677 if (!NomNom::choose_from_feed (this, url
))
683 if (url
.isEmpty ()) {
685 YoutubeFeed
d (this);
687 if (d
.exec () != QDialog::Accepted
|| !d
.gotItems ())
690 if (!NomNom::choose_from_feed (this, url
))
701 MainWindow::onRake () {
704 // Slot: Process started.
707 MainWindow::onQuviStarted ()
708 { statusLabel
->setText (tr ("Starting parser ...")); }
710 // Slot: Process error.
713 MainWindow::onQuviError (QProcess::ProcessError n
) {
715 NomNom::crit (this, NomNom::to_process_errmsg (n
));
718 // Slot: Ready read. Note that we use merged channels (stdout + stderr).
721 MainWindow::onQuviReadyRead () {
723 static char data
[1024];
725 while (proc
.readLine (data
, sizeof(data
))) {
727 QString ln
= QString::fromLocal8Bit (data
);
733 if (ln
.startsWith (":: Check"))
734 statusLabel
->setText (tr ("Checking..."));
736 else if (ln
.startsWith (":: Fetch"))
737 statusLabel
->setText (tr ("Fetching..."));
739 else if (ln
.startsWith (":: Verify"))
740 statusLabel
->setText (tr ("Verifying..."));
742 // Leave JSON and error parsing to finished slot.
746 // Slot: Process finished.
749 MainWindow::onQuviFinished (int exitCode
, QProcess::ExitStatus exitStatus
) {
751 statusLabel
->setText (tr ("Ready."));
753 if (exitStatus
== QProcess::NormalExit
&& exitCode
== 0) {
755 if (modeCBox
->currentIndex() == StreamVideo
)
767 // Slot: download error.
770 MainWindow::onDownloadError (QString e
)
771 { NomNom::crit (this, e
); }
776 MainWindow::dragEnterEvent (QDragEnterEvent
*e
) {
778 QUrl
url(e
->mimeData()->text());
780 if (url
.isValid() && url
.scheme().toLower() == "http")
781 e
->acceptProposedAction();
787 MainWindow::dropEvent (QDropEvent
*e
) {
789 const QString url
= e
->mimeData()->text().simplified();
792 e
->acceptProposedAction();