1 /*
2 * This file Copyright (C) 2009-2015 Mnemosyne LLC
3 *
4 * It may be used under the GNU GPL versions 2 or 3
5 * or any future license endorsed by Mnemosyne LLC.
6 *
7 */
8
9 #include <ctime>
10 #include <iostream>
11
12 #include <QIcon>
13 #include <QLibraryInfo>
14 #include <QMessageBox>
15 #include <QProcess>
16 #include <QRect>
17 #include <QSystemTrayIcon>
18
19 #ifdef QT_DBUS_LIB
20 #include <QDBusConnection>
21 #include <QDBusMessage>
22 #include <QDBusReply>
23 #endif
24
25 #include <libtransmission/transmission.h>
26 #include <libtransmission/tr-getopt.h>
27 #include <libtransmission/utils.h>
28 #include <libtransmission/version.h>
29
30 #include "AddData.h"
31 #include "Application.h"
32 #include "Formatter.h"
33 #include "InteropHelper.h"
34 #include "MainWindow.h"
35 #include "OptionsDialog.h"
36 #include "Prefs.h"
37 #include "Session.h"
38 #include "TorrentModel.h"
39 #include "WatchDir.h"
40
41 namespace
42 {
43
44 QLatin1String const MY_CONFIG_NAME("transmission");
45 QLatin1String const MY_READABLE_NAME("transmission-qt");
46
47 tr_option const opts[] =
48 {
49 { 'g', "config-dir", "Where to look for configuration files", "g", true, "<path>" },
50 { 'm', "minimized", "Start minimized in system tray", "m", false, nullptr },
51 { 'p', "port", "Port to use when connecting to an existing session", "p", true, "<port>" },
52 { 'r', "remote", "Connect to an existing session at the specified hostname", "r", true, "<host>" },
53 { 'u', "username", "Username to use when connecting to an existing session", "u", true, "<username>" },
54 { 'v', "version", "Show version number and exit", "v", false, nullptr },
55 { 'w', "password", "Password to use when connecting to an existing session", "w", true, "<password>" },
56 { 0, nullptr, nullptr, nullptr, false, nullptr }
57 };
58
getUsage()59 char const* getUsage()
60 {
61 return "Usage:\n"
62 " transmission [OPTIONS...] [torrent files]";
63 }
64
65 enum
66 {
67 STATS_REFRESH_INTERVAL_MSEC = 3000,
68 SESSION_REFRESH_INTERVAL_MSEC = 3000,
69 MODEL_REFRESH_INTERVAL_MSEC = 3000
70 };
71
loadTranslation(QTranslator & translator,QString const & name,QLocale const & locale,QStringList const & searchDirectories)72 bool loadTranslation(QTranslator& translator, QString const& name, QLocale const& locale, QStringList const& searchDirectories)
73 {
74 for (QString const& directory : searchDirectories)
75 {
76 if (translator.load(locale, name, QLatin1String("_"), directory))
77 {
78 return true;
79 }
80 }
81
82 return false;
83 }
84
85 } // namespace
86
Application(int & argc,char ** argv)87 Application::Application(int& argc, char** argv) :
88 QApplication(argc, argv),
89 myPrefs(nullptr),
90 mySession(nullptr),
91 myModel(nullptr),
92 myWindow(nullptr),
93 myWatchDir(nullptr),
94 myLastFullUpdateTime(0)
95 {
96 setApplicationName(MY_CONFIG_NAME);
97 loadTranslations();
98
99 Formatter::initUnits();
100
101 #if defined(_WIN32) || defined(__APPLE__)
102
103 if (QIcon::themeName().isEmpty())
104 {
105 QIcon::setThemeName(QLatin1String("Faenza"));
106 }
107
108 #endif
109
110 // set the default icon
111 QIcon icon = QIcon::fromTheme(QLatin1String("transmission"));
112
113 if (icon.isNull())
114 {
115 QList<int> sizes;
116 sizes << 16 << 22 << 24 << 32 << 48 << 64 << 72 << 96 << 128 << 192 << 256;
117
118 for (int const size : sizes)
119 {
120 icon.addPixmap(QPixmap(QString::fromLatin1(":/icons/transmission-%1.png").arg(size)));
121 }
122 }
123
124 setWindowIcon(icon);
125
126 #ifdef __APPLE__
127 setAttribute(Qt::AA_DontShowIconsInMenus);
128 #endif
129
130 // parse the command-line arguments
131 int c;
132 bool minimized = false;
133 char const* optarg;
134 QString host;
135 QString port;
136 QString username;
137 QString password;
138 QString configDir;
139 QStringList filenames;
140
141 while ((c = tr_getopt(getUsage(), argc, const_cast<char const**>(argv), opts, &optarg)) != TR_OPT_DONE)
142 {
143 switch (c)
144 {
145 case 'g':
146 configDir = QString::fromUtf8(optarg);
147 break;
148
149 case 'p':
150 port = QString::fromUtf8(optarg);
151 break;
152
153 case 'r':
154 host = QString::fromUtf8(optarg);
155 break;
156
157 case 'u':
158 username = QString::fromUtf8(optarg);
159 break;
160
161 case 'w':
162 password = QString::fromUtf8(optarg);
163 break;
164
165 case 'm':
166 minimized = true;
167 break;
168
169 case 'v':
170 std::cerr << MY_READABLE_NAME.latin1() << ' ' << LONG_VERSION_STRING << std::endl;
171 quitLater();
172 return;
173
174 case TR_OPT_ERR:
175 std::cerr << qPrintable(QObject::tr("Invalid option")) << std::endl;
176 tr_getopt_usage(MY_READABLE_NAME.latin1(), getUsage(), opts);
177 quitLater();
178 return;
179
180 default:
181 filenames.append(QString::fromUtf8(optarg));
182 break;
183 }
184 }
185
186 // try to delegate the work to an existing copy of Transmission
187 // before starting ourselves...
188 InteropHelper interopClient;
189
190 if (interopClient.isConnected())
191 {
192 bool delegated = false;
193
194 for (QString const& filename : filenames)
195 {
196 QString metainfo;
197
198 AddData a(filename);
199
200 switch (a.type)
201 {
202 case AddData::URL:
203 metainfo = a.url.toString();
204 break;
205
206 case AddData::MAGNET:
207 metainfo = a.magnet;
208 break;
209
210 case AddData::FILENAME:
211 metainfo = QString::fromLatin1(a.toBase64());
212 break;
213
214 case AddData::METAINFO:
215 metainfo = QString::fromLatin1(a.toBase64());
216 break;
217
218 default:
219 break;
220 }
221
222 if (!metainfo.isEmpty() && interopClient.addMetainfo(metainfo))
223 {
224 delegated = true;
225 }
226 }
227
228 if (delegated)
229 {
230 quitLater();
231 return;
232 }
233 }
234
235 // set the fallback config dir
236 if (configDir.isNull())
237 {
238 configDir = QString::fromUtf8(tr_getDefaultConfigDir("transmission"));
239 }
240
241 // ensure our config directory exists
242 QDir dir(configDir);
243
244 if (!dir.exists())
245 {
246 dir.mkpath(configDir);
247 }
248
249 // is this the first time we've run transmission?
250 bool const firstTime = !dir.exists(QLatin1String("settings.json"));
251
252 // initialize the prefs
253 myPrefs = new Prefs(configDir);
254
255 if (!host.isNull())
256 {
257 myPrefs->set(Prefs::SESSION_REMOTE_HOST, host);
258 }
259
260 if (!port.isNull())
261 {
262 myPrefs->set(Prefs::SESSION_REMOTE_PORT, port.toUInt());
263 }
264
265 if (!username.isNull())
266 {
267 myPrefs->set(Prefs::SESSION_REMOTE_USERNAME, username);
268 }
269
270 if (!password.isNull())
271 {
272 myPrefs->set(Prefs::SESSION_REMOTE_PASSWORD, password);
273 }
274
275 if (!host.isNull() || !port.isNull() || !username.isNull() || !password.isNull())
276 {
277 myPrefs->set(Prefs::SESSION_IS_REMOTE, true);
278 }
279
280 if (myPrefs->getBool(Prefs::START_MINIMIZED))
281 {
282 minimized = true;
283 }
284
285 // start as minimized only if the system tray present
286 if (!myPrefs->getBool(Prefs::SHOW_TRAY_ICON))
287 {
288 minimized = false;
289 }
290
291 mySession = new Session(configDir, *myPrefs);
292 myModel = new TorrentModel(*myPrefs);
293 myWindow = new MainWindow(*mySession, *myPrefs, *myModel, minimized);
294 myWatchDir = new WatchDir(*myModel);
295
296 connect(myModel, &TorrentModel::torrentsAdded, this, &Application::onTorrentsAdded);
297 connect(myModel, &TorrentModel::torrentsCompleted, this, &Application::onTorrentsCompleted);
298 connect(myModel, &TorrentModel::torrentsNeedInfo, this, &Application::onTorrentsNeedInfo);
299 connect(myPrefs, &Prefs::changed, this, &Application::refreshPref);
300 connect(mySession, &Session::sourceChanged, this, &Application::onSessionSourceChanged);
301 connect(mySession, &Session::torrentsRemoved, myModel, &TorrentModel::removeTorrents);
302 connect(mySession, &Session::torrentsUpdated, myModel, &TorrentModel::updateTorrents);
303 connect(myWatchDir, &WatchDir::torrentFileAdded, this, &Application::addTorrent);
304
305 // init from preferences
306 for (auto const key : { Prefs::DIR_WATCH })
307 {
308 refreshPref(key);
309 }
310
311 QTimer* timer = &myModelTimer;
312 connect(timer, &QTimer::timeout, this, &Application::refreshTorrents);
313 timer->setSingleShot(false);
314 timer->setInterval(MODEL_REFRESH_INTERVAL_MSEC);
315 timer->start();
316
317 timer = &myStatsTimer;
318 connect(timer, &QTimer::timeout, mySession, &Session::refreshSessionStats);
319 timer->setSingleShot(false);
320 timer->setInterval(STATS_REFRESH_INTERVAL_MSEC);
321 timer->start();
322
323 timer = &mySessionTimer;
324 connect(timer, &QTimer::timeout, mySession, &Session::refreshSessionInfo);
325 timer->setSingleShot(false);
326 timer->setInterval(SESSION_REFRESH_INTERVAL_MSEC);
327 timer->start();
328
329 maybeUpdateBlocklist();
330
331 if (!firstTime)
332 {
333 mySession->restart();
334 }
335 else
336 {
337 myWindow->openSession();
338 }
339
340 if (!myPrefs->getBool(Prefs::USER_HAS_GIVEN_INFORMED_CONSENT))
341 {
342 QMessageBox* dialog = new QMessageBox(QMessageBox::Information, QString(),
343 tr("<b>Transmission is a file sharing program.</b>"), QMessageBox::Ok | QMessageBox::Cancel, myWindow);
344 dialog->setInformativeText(tr("When you run a torrent, its data will be made available to others by means of upload. "
345 "Any content you share is your sole responsibility."));
346 dialog->button(QMessageBox::Ok)->setText(tr("I &Agree"));
347 dialog->setDefaultButton(QMessageBox::Ok);
348 dialog->setModal(true);
349
350 connect(dialog, SIGNAL(finished(int)), this, SLOT(consentGiven(int)));
351
352 dialog->setAttribute(Qt::WA_DeleteOnClose);
353 dialog->show();
354 }
355
356 for (QString const& filename : filenames)
357 {
358 addTorrent(filename);
359 }
360
361 InteropHelper::registerObject(this);
362 }
363
loadTranslations()364 void Application::loadTranslations()
365 {
366 QStringList const qtQmDirs = QStringList() << QLibraryInfo::location(QLibraryInfo::TranslationsPath) <<
367 #ifdef TRANSLATIONS_DIR
368 QString::fromUtf8(TRANSLATIONS_DIR) <<
369 #endif
370 (applicationDirPath() + QLatin1String("/translations"));
371
372 QStringList const appQmDirs = QStringList() <<
373 #ifdef TRANSLATIONS_DIR
374 QString::fromUtf8(TRANSLATIONS_DIR) <<
375 #endif
376 (applicationDirPath() + QLatin1String("/translations"));
377
378 QString const qtFileName = QLatin1String("qtbase");
379
380 QLocale const locale;
381 QLocale const englishLocale(QLocale::English, QLocale::UnitedStates);
382
383 if (loadTranslation(myQtTranslator, qtFileName, locale, qtQmDirs) ||
384 loadTranslation(myQtTranslator, qtFileName, englishLocale, qtQmDirs))
385 {
386 installTranslator(&myQtTranslator);
387 }
388
389 if (loadTranslation(myAppTranslator, MY_CONFIG_NAME, locale, appQmDirs) ||
390 loadTranslation(myAppTranslator, MY_CONFIG_NAME, englishLocale, appQmDirs))
391 {
392 installTranslator(&myAppTranslator);
393 }
394 }
395
quitLater()396 void Application::quitLater()
397 {
398 QTimer::singleShot(0, this, SLOT(quit()));
399 }
400
onTorrentsEdited(torrent_ids_t const & ids)401 void Application::onTorrentsEdited(torrent_ids_t const& ids)
402 {
403 // the backend's tr_info has changed, so reload those fields
404 mySession->initTorrents(ids);
405 }
406
getNames(torrent_ids_t const & ids) const407 QStringList Application::getNames(torrent_ids_t const& ids) const
408 {
409 QStringList names;
410 for (auto const& id : ids)
411 {
412 names.push_back(myModel->getTorrentFromId(id)->name());
413 }
414
415 names.sort();
416 return names;
417 }
418
onTorrentsAdded(torrent_ids_t const & ids)419 void Application::onTorrentsAdded(torrent_ids_t const& ids)
420 {
421 if (myPrefs->getBool(Prefs::SHOW_NOTIFICATION_ON_ADD))
422 {
423 auto const title = tr("Torrent(s) Added", nullptr, ids.size());
424 auto const body = getNames(ids).join(QStringLiteral("\n"));
425 notifyApp(title, body);
426 }
427 }
428
onTorrentsCompleted(torrent_ids_t const & ids)429 void Application::onTorrentsCompleted(torrent_ids_t const& ids)
430 {
431 if (myPrefs->getBool(Prefs::SHOW_NOTIFICATION_ON_COMPLETE))
432 {
433 auto const title = tr("Torrent Completed", nullptr, ids.size());
434 auto const body = getNames(ids).join(QStringLiteral("\n"));
435 notifyApp(title, body);
436 }
437
438 if (myPrefs->getBool(Prefs::COMPLETE_SOUND_ENABLED))
439 {
440 #if defined(Q_OS_WIN) || defined(Q_OS_MAC)
441 beep();
442 #else
443 QProcess::execute(myPrefs->getString(Prefs::COMPLETE_SOUND_COMMAND));
444 #endif
445 }
446 }
447
onTorrentsNeedInfo(torrent_ids_t const & ids)448 void Application::onTorrentsNeedInfo(torrent_ids_t const& ids)
449 {
450 if (!ids.empty())
451 {
452 mySession->initTorrents(ids);
453 }
454 }
455
456 /***
457 ****
458 ***/
459
consentGiven(int result)460 void Application::consentGiven(int result)
461 {
462 if (result == QMessageBox::Ok)
463 {
464 myPrefs->set<bool>(Prefs::USER_HAS_GIVEN_INFORMED_CONSENT, true);
465 }
466 else
467 {
468 quit();
469 }
470 }
471
~Application()472 Application::~Application()
473 {
474 if (myPrefs != nullptr && myWindow != nullptr)
475 {
476 QRect const mainwinRect(myWindow->geometry());
477 myPrefs->set(Prefs::MAIN_WINDOW_HEIGHT, std::max(100, mainwinRect.height()));
478 myPrefs->set(Prefs::MAIN_WINDOW_WIDTH, std::max(100, mainwinRect.width()));
479 myPrefs->set(Prefs::MAIN_WINDOW_X, mainwinRect.x());
480 myPrefs->set(Prefs::MAIN_WINDOW_Y, mainwinRect.y());
481 }
482
483 delete myWatchDir;
484 delete myWindow;
485 delete myModel;
486 delete mySession;
487 delete myPrefs;
488 }
489
490 /***
491 ****
492 ***/
493
refreshPref(int key)494 void Application::refreshPref(int key)
495 {
496 switch (key)
497 {
498 case Prefs::BLOCKLIST_UPDATES_ENABLED:
499 maybeUpdateBlocklist();
500 break;
501
502 case Prefs::DIR_WATCH:
503 case Prefs::DIR_WATCH_ENABLED:
504 {
505 QString const path(myPrefs->getString(Prefs::DIR_WATCH));
506 bool const isEnabled(myPrefs->getBool(Prefs::DIR_WATCH_ENABLED));
507 myWatchDir->setPath(path, isEnabled);
508 break;
509 }
510
511 default:
512 break;
513 }
514 }
515
maybeUpdateBlocklist()516 void Application::maybeUpdateBlocklist()
517 {
518 if (!myPrefs->getBool(Prefs::BLOCKLIST_UPDATES_ENABLED))
519 {
520 return;
521 }
522
523 QDateTime const lastUpdatedAt = myPrefs->getDateTime(Prefs::BLOCKLIST_DATE);
524 QDateTime const nextUpdateAt = lastUpdatedAt.addDays(7);
525 QDateTime const now = QDateTime::currentDateTime();
526
527 if (now < nextUpdateAt)
528 {
529 mySession->updateBlocklist();
530 myPrefs->set(Prefs::BLOCKLIST_DATE, now);
531 }
532 }
533
onSessionSourceChanged()534 void Application::onSessionSourceChanged()
535 {
536 mySession->initTorrents();
537 mySession->refreshSessionStats();
538 mySession->refreshSessionInfo();
539 }
540
refreshTorrents()541 void Application::refreshTorrents()
542 {
543 // usually we just poll the torrents that have shown recent activity,
544 // but we also periodically ask for updates on the others to ensure
545 // nothing's falling through the cracks.
546 time_t const now = time(nullptr);
547
548 if (myLastFullUpdateTime + 60 >= now)
549 {
550 mySession->refreshActiveTorrents();
551 }
552 else
553 {
554 myLastFullUpdateTime = now;
555 mySession->refreshAllTorrents();
556 }
557 }
558
559 /***
560 ****
561 ***/
562
addTorrent(AddData const & addme)563 void Application::addTorrent(AddData const& addme)
564 {
565 if (addme.type == addme.NONE)
566 {
567 return;
568 }
569
570 if (!myPrefs->getBool(Prefs::OPTIONS_PROMPT))
571 {
572 mySession->addTorrent(addme);
573 }
574 else
575 {
576 auto o = new OptionsDialog(*mySession, *myPrefs, addme, myWindow);
577 o->show();
578 }
579
580 raise();
581 }
582
583 /***
584 ****
585 ***/
586
raise()587 void Application::raise()
588 {
589 alert(myWindow);
590 }
591
notifyApp(QString const & title,QString const & body) const592 bool Application::notifyApp(QString const& title, QString const& body) const
593 {
594 #ifdef QT_DBUS_LIB
595
596 QLatin1String const dbusServiceName("org.freedesktop.Notifications");
597 QLatin1String const dbusInterfaceName("org.freedesktop.Notifications");
598 QLatin1String const dbusPath("/org/freedesktop/Notifications");
599
600 QDBusConnection bus = QDBusConnection::sessionBus();
601
602 if (bus.isConnected())
603 {
604 QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QLatin1String("Notify"));
605 QVariantList args;
606 args.append(QLatin1String("Transmission")); // app_name
607 args.append(0U); // replaces_id
608 args.append(QLatin1String("transmission")); // icon
609 args.append(title); // summary
610 args.append(body); // body
611 args.append(QStringList()); // actions - unused for plain passive popups
612 args.append(QVariantMap()); // hints - unused atm
613 args.append(static_cast<int32_t>(-1)); // use the default timeout period
614 m.setArguments(args);
615 QDBusReply<quint32> const replyMsg = bus.call(m);
616
617 if (replyMsg.isValid() && replyMsg.value() > 0)
618 {
619 return true;
620 }
621 }
622
623 #endif
624
625 myWindow->trayIcon().showMessage(title, body);
626 return true;
627 }
628
faviconCache()629 FaviconCache& Application::faviconCache()
630 {
631 return myFavicons;
632 }
633
634 /***
635 ****
636 ***/
637
tr_main(int argc,char * argv[])638 int tr_main(int argc, char* argv[])
639 {
640 InteropHelper::initialize();
641
642 #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
643 Application::setAttribute(Qt::AA_EnableHighDpiScaling);
644 #endif
645
646 Application::setAttribute(Qt::AA_UseHighDpiPixmaps);
647
648 Application app(argc, argv);
649 return app.exec();
650 }
651