1/* 2 * This file Copyright (C) Mnemosyne LLC 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License version 2 6 * as published by the Free Software Foundation. 7 * 8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 9 * 10 * $Id: app.cc 13385 2012-07-13 00:29:40Z jordan $ 11 */ 12 13#include <cassert> 14#include <ctime> 15#include <iostream> 16 17#include <QDBusConnection> 18#include <QDBusConnectionInterface> 19#include <QDBusError> 20#include <QDBusMessage> 21#include <QDialogButtonBox> 22#include <QIcon> 23#include <QLabel> 24#include <QLibraryInfo> 25#include <QRect> 26 27#include <libtransmission/transmission.h> 28#include <libtransmission/tr-getopt.h> 29#include <libtransmission/utils.h> 30#include <libtransmission/version.h> 31 32#include "add-data.h" 33#include "app.h" 34#include "dbus-adaptor.h" 35#include "formatter.h" 36#include "mainwin.h" 37#include "options.h" 38#include "prefs.h" 39#include "session.h" 40#include "session-dialog.h" 41#include "torrent-model.h" 42#include "utils.h" 43#include "watchdir.h" 44 45namespace 46{ 47 const QString DBUS_SERVICE = QString::fromAscii( "com.transmissionbt.Transmission" ); 48 const QString DBUS_OBJECT_PATH = QString::fromAscii( "/com/transmissionbt/Transmission" ); 49 const QString DBUS_INTERFACE = QString::fromAscii( "com.transmissionbt.Transmission" ); 50 51 const char * MY_READABLE_NAME( "transmission-qt" ); 52 53 const tr_option opts[] = 54 { 55 { 'g', "config-dir", "Where to look for configuration files", "g", 1, "<path>" }, 56 { 'm', "minimized", "Start minimized in system tray", "m", 0, NULL }, 57 { 'p', "port", "Port to use when connecting to an existing session", "p", 1, "<port>" }, 58 { 'r', "remote", "Connect to an existing session at the specified hostname", "r", 1, "<host>" }, 59 { 'u', "username", "Username to use when connecting to an existing session", "u", 1, "<username>" }, 60 { 'v', "version", "Show version number and exit", "v", 0, NULL }, 61 { 'w', "password", "Password to use when connecting to an existing session", "w", 1, "<password>" }, 62 { 0, NULL, NULL, NULL, 0, NULL } 63 }; 64 65 const char* 66 getUsage( void ) 67 { 68 return "Usage:\n" 69 " transmission [OPTIONS...] [torrent files]"; 70 } 71 72 void 73 showUsage( void ) 74 { 75 tr_getopt_usage( MY_READABLE_NAME, getUsage( ), opts ); 76 exit( 0 ); 77 } 78 79 enum 80 { 81 STATS_REFRESH_INTERVAL_MSEC = 3000, 82 SESSION_REFRESH_INTERVAL_MSEC = 3000, 83 MODEL_REFRESH_INTERVAL_MSEC = 3000 84 }; 85} 86 87MyApp :: MyApp( int& argc, char ** argv ): 88 QApplication( argc, argv ), 89 myLastFullUpdateTime( 0 ) 90{ 91 const QString MY_CONFIG_NAME = QString::fromAscii( "transmission" ); 92 93 setApplicationName( MY_CONFIG_NAME ); 94 95 // install the qt translator 96 qtTranslator.load( "qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); 97 installTranslator( &qtTranslator ); 98 99 // install the transmission translator 100 appTranslator.load( QString(MY_CONFIG_NAME) + "_" + QLocale::system().name(), QCoreApplication::applicationDirPath() + "/translations" ); 101 installTranslator( &appTranslator ); 102 103 Formatter::initUnits( ); 104 105 // set the default icon 106 QIcon icon; 107 QList<int> sizes; 108 sizes << 16 << 22 << 24 << 32 << 48; 109 foreach( int size, sizes ) 110 icon.addPixmap( QPixmap( QString::fromAscii(":/icons/transmission-%1.png" ).arg(size) ) ); 111 setWindowIcon( icon ); 112 113 // parse the command-line arguments 114 int c; 115 bool minimized = false; 116 const char * optarg; 117 const char * host = 0; 118 const char * port = 0; 119 const char * username = 0; 120 const char * password = 0; 121 const char * configDir = 0; 122 QStringList filenames; 123 while( ( c = tr_getopt( getUsage( ), argc, (const char**)argv, opts, &optarg ) ) ) { 124 switch( c ) { 125 case 'g': configDir = optarg; break; 126 case 'p': port = optarg; break; 127 case 'r': host = optarg; break; 128 case 'u': username = optarg; break; 129 case 'w': password = optarg; break; 130 case 'm': minimized = true; break; 131 case 'v': std::cerr << MY_READABLE_NAME << ' ' << LONG_VERSION_STRING << std::endl; ::exit( 0 ); break; 132 case TR_OPT_ERR: Utils::toStderr( QObject::tr( "Invalid option" ) ); showUsage( ); break; 133 default: filenames.append( optarg ); break; 134 } 135 } 136 137 // set the fallback config dir 138 if( configDir == 0 ) 139 configDir = tr_getDefaultConfigDir( "transmission" ); 140 141 // ensure our config directory exists 142 QDir dir( configDir ); 143 if( !dir.exists() ) 144 dir.mkpath( configDir ); 145 146 // is this the first time we've run transmission? 147 const bool firstTime = !QFile(QDir(configDir).absoluteFilePath("settings.json")).exists(); 148 149 // initialize the prefs 150 myPrefs = new Prefs ( configDir ); 151 if( host != 0 ) 152 myPrefs->set( Prefs::SESSION_REMOTE_HOST, host ); 153 if( port != 0 ) 154 myPrefs->set( Prefs::SESSION_REMOTE_PORT, port ); 155 if( username != 0 ) 156 myPrefs->set( Prefs::SESSION_REMOTE_USERNAME, username ); 157 if( password != 0 ) 158 myPrefs->set( Prefs::SESSION_REMOTE_PASSWORD, password ); 159 if( ( host != 0 ) || ( port != 0 ) || ( username != 0 ) || ( password != 0 ) ) 160 myPrefs->set( Prefs::SESSION_IS_REMOTE, true ); 161 162 mySession = new Session( configDir, *myPrefs ); 163 myModel = new TorrentModel( *myPrefs ); 164 myWindow = new TrMainWindow( *mySession, *myPrefs, *myModel, minimized ); 165 myWatchDir = new WatchDir( *myModel ); 166 167 // when the session gets torrent info, update the model 168 connect( mySession, SIGNAL(torrentsUpdated(tr_benc*,bool)), myModel, SLOT(updateTorrents(tr_benc*,bool)) ); 169 connect( mySession, SIGNAL(torrentsUpdated(tr_benc*,bool)), myWindow, SLOT(refreshActionSensitivity()) ); 170 connect( mySession, SIGNAL(torrentsRemoved(tr_benc*)), myModel, SLOT(removeTorrents(tr_benc*)) ); 171 // when the session source gets changed, request a full refresh 172 connect( mySession, SIGNAL(sourceChanged()), this, SLOT(onSessionSourceChanged()) ); 173 // when the model sees a torrent for the first time, ask the session for full info on it 174 connect( myModel, SIGNAL(torrentsAdded(QSet<int>)), mySession, SLOT(initTorrents(QSet<int>)) ); 175 connect( myModel, SIGNAL(torrentsAdded(QSet<int>)), this, SLOT(onTorrentsAdded(QSet<int>)) ); 176 177 mySession->initTorrents( ); 178 mySession->refreshSessionStats( ); 179 180 // when torrents are added to the watch directory, tell the session 181 connect( myWatchDir, SIGNAL(torrentFileAdded(QString)), this, SLOT(addTorrent(QString)) ); 182 183 // init from preferences 184 QList<int> initKeys; 185 initKeys << Prefs::DIR_WATCH; 186 foreach( int key, initKeys ) 187 refreshPref( key ); 188 connect( myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(const int)) ); 189 190 QTimer * timer = &myModelTimer; 191 connect( timer, SIGNAL(timeout()), this, SLOT(refreshTorrents()) ); 192 timer->setSingleShot( false ); 193 timer->setInterval( MODEL_REFRESH_INTERVAL_MSEC ); 194 timer->start( ); 195 196 timer = &myStatsTimer; 197 connect( timer, SIGNAL(timeout()), mySession, SLOT(refreshSessionStats()) ); 198 timer->setSingleShot( false ); 199 timer->setInterval( STATS_REFRESH_INTERVAL_MSEC ); 200 timer->start( ); 201 202 timer = &mySessionTimer; 203 connect( timer, SIGNAL(timeout()), mySession, SLOT(refreshSessionInfo()) ); 204 timer->setSingleShot( false ); 205 timer->setInterval( SESSION_REFRESH_INTERVAL_MSEC ); 206 timer->start( ); 207 208 maybeUpdateBlocklist( ); 209 210 if( !firstTime ) 211 mySession->restart( ); 212 else { 213 QDialog * d = new SessionDialog( *mySession, *myPrefs, myWindow ); 214 d->show( ); 215 } 216 217 if( !myPrefs->getBool( Prefs::USER_HAS_GIVEN_INFORMED_CONSENT )) 218 { 219 QDialog * dialog = new QDialog( myWindow ); 220 dialog->setModal( true ); 221 QVBoxLayout * v = new QVBoxLayout( dialog ); 222 QLabel * l = new QLabel( tr( "Transmission is a file-sharing program. When you run a torrent, its data will be made available to others by means of upload. You and you alone are fully responsible for exercising proper judgement and abiding by your local laws." ) ); 223 l->setWordWrap( true ); 224 v->addWidget( l ); 225 QDialogButtonBox * box = new QDialogButtonBox; 226 box->addButton( new QPushButton( tr( "&Cancel" ) ), QDialogButtonBox::RejectRole ); 227 QPushButton * agree = new QPushButton( tr( "I &Agree" ) ); 228 agree->setDefault( true ); 229 box->addButton( agree, QDialogButtonBox::AcceptRole ); 230 box->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed ); 231 box->setOrientation( Qt::Horizontal ); 232 v->addWidget( box ); 233 connect( box, SIGNAL(rejected()), this, SLOT(quit()) ); 234 connect( box, SIGNAL(accepted()), dialog, SLOT(deleteLater()) ); 235 connect( box, SIGNAL(accepted()), this, SLOT(consentGiven()) ); 236 dialog->show(); 237 } 238 239 for( QStringList::const_iterator it=filenames.begin(), end=filenames.end(); it!=end; ++it ) 240 addTorrent( *it ); 241 242 // register as the dbus handler for Transmission 243 new TrDBusAdaptor( this ); 244 QDBusConnection bus = QDBusConnection::sessionBus(); 245 if( !bus.registerService( DBUS_SERVICE ) ) 246 std::cerr << "couldn't register " << qPrintable(DBUS_SERVICE) << std::endl; 247 if( !bus.registerObject( DBUS_OBJECT_PATH, this ) ) 248 std::cerr << "couldn't register " << qPrintable(DBUS_OBJECT_PATH) << std::endl; 249} 250 251/* these functions are for popping up desktop notifications */ 252 253void 254MyApp :: onTorrentsAdded( QSet<int> torrents ) 255{ 256 if( !myPrefs->getBool( Prefs::SHOW_DESKTOP_NOTIFICATION ) ) 257 return; 258 259 foreach( int id, torrents ) 260 { 261 Torrent * tor = myModel->getTorrentFromId( id ); 262 263 if( tor->name().isEmpty( ) ) // wait until the torrent's INFO fields are loaded 264 connect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onNewTorrentChanged(int)) ); 265 else { 266 onNewTorrentChanged( id ); 267 if( !tor->isSeed( ) ) 268 connect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) ); 269 } 270 } 271} 272 273void 274MyApp :: onTorrentCompleted( int id ) 275{ 276 Torrent * tor = myModel->getTorrentFromId( id ); 277 278 if( tor && !tor->name().isEmpty() ) 279 { 280 notify( tr( "Torrent Completed" ), tor->name( ) ); 281 282 disconnect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) ); 283 } 284} 285 286void 287MyApp :: onNewTorrentChanged( int id ) 288{ 289 Torrent * tor = myModel->getTorrentFromId( id ); 290 291 if( tor && !tor->name().isEmpty() ) 292 { 293 const int age_secs = tor->dateAdded().secsTo(QDateTime::currentDateTime()); 294 if( age_secs < 30 ) 295 notify( tr( "Torrent Added" ), tor->name( ) ); 296 297 disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onNewTorrentChanged(int)) ); 298 299 if( !tor->isSeed( ) ) 300 connect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) ); 301 } 302} 303 304/*** 305**** 306***/ 307 308void 309MyApp :: consentGiven( ) 310{ 311 myPrefs->set<bool>( Prefs::USER_HAS_GIVEN_INFORMED_CONSENT, true ); 312} 313 314MyApp :: ~MyApp( ) 315{ 316 const QRect mainwinRect( myWindow->geometry( ) ); 317 delete myWatchDir; 318 delete myWindow; 319 delete myModel; 320 delete mySession; 321 322 myPrefs->set( Prefs :: MAIN_WINDOW_HEIGHT, std::max( 100, mainwinRect.height( ) ) ); 323 myPrefs->set( Prefs :: MAIN_WINDOW_WIDTH, std::max( 100, mainwinRect.width( ) ) ); 324 myPrefs->set( Prefs :: MAIN_WINDOW_X, mainwinRect.x( ) ); 325 myPrefs->set( Prefs :: MAIN_WINDOW_Y, mainwinRect.y( ) ); 326 delete myPrefs; 327} 328 329/*** 330**** 331***/ 332 333void 334MyApp :: refreshPref( int key ) 335{ 336 switch( key ) 337 { 338 case Prefs :: BLOCKLIST_UPDATES_ENABLED: 339 maybeUpdateBlocklist( ); 340 break; 341 342 case Prefs :: DIR_WATCH: 343 case Prefs :: DIR_WATCH_ENABLED: { 344 const QString path( myPrefs->getString( Prefs::DIR_WATCH ) ); 345 const bool isEnabled( myPrefs->getBool( Prefs::DIR_WATCH_ENABLED ) ); 346 myWatchDir->setPath( path, isEnabled ); 347 break; 348 } 349 350 default: 351 break; 352 } 353} 354 355void 356MyApp :: maybeUpdateBlocklist( ) 357{ 358 if( !myPrefs->getBool( Prefs :: BLOCKLIST_UPDATES_ENABLED ) ) 359 return; 360 361 const QDateTime lastUpdatedAt = myPrefs->getDateTime( Prefs :: BLOCKLIST_DATE ); 362 const QDateTime nextUpdateAt = lastUpdatedAt.addDays( 7 ); 363 const QDateTime now = QDateTime::currentDateTime( ); 364 if( now < nextUpdateAt ) 365 { 366 mySession->updateBlocklist( ); 367 myPrefs->set( Prefs :: BLOCKLIST_DATE, now ); 368 } 369} 370 371void 372MyApp :: onSessionSourceChanged( ) 373{ 374 mySession->initTorrents( ); 375 mySession->refreshSessionStats( ); 376 mySession->refreshSessionInfo( ); 377} 378 379void 380MyApp :: refreshTorrents( ) 381{ 382 // usually we just poll the torrents that have shown recent activity, 383 // but we also periodically ask for updates on the others to ensure 384 // nothing's falling through the cracks. 385 const time_t now = time( NULL ); 386 if( myLastFullUpdateTime + 60 >= now ) 387 mySession->refreshActiveTorrents( ); 388 else { 389 myLastFullUpdateTime = now; 390 mySession->refreshAllTorrents( ); 391 } 392} 393 394/*** 395**** 396***/ 397 398void 399MyApp :: addTorrent( const QString& key ) 400{ 401 const AddData addme( key ); 402 403 if( addme.type != addme.NONE ) 404 addTorrent( addme ); 405} 406 407void 408MyApp :: addTorrent( const AddData& addme ) 409{ 410 if( !myPrefs->getBool( Prefs :: OPTIONS_PROMPT ) ) 411 { 412 mySession->addTorrent( addme ); 413 } 414 else if( addme.type == addme.URL ) 415 { 416 myWindow->openURL( addme.url.toString( ) ); 417 } 418 else if( addme.type == addme.MAGNET ) 419 { 420 myWindow->openURL( addme.magnet ); 421 } 422 else 423 { 424 Options * o = new Options( *mySession, *myPrefs, addme, myWindow ); 425 o->show( ); 426 } 427 428 raise( ); 429} 430 431/*** 432**** 433***/ 434 435void 436MyApp :: raise( ) 437{ 438 QApplication :: alert ( myWindow ); 439} 440 441bool 442MyApp :: notify( const QString& title, const QString& body ) const 443{ 444 const QString dbusServiceName = QString::fromAscii( "org.freedesktop.Notifications" ); 445 const QString dbusInterfaceName = QString::fromAscii( "org.freedesktop.Notifications" ); 446 const QString dbusPath = QString::fromAscii( "/org/freedesktop/Notifications" ); 447 448 QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QString::fromAscii("Notify")); 449 QList<QVariant> args; 450 args.append( QString::fromAscii( "Transmission" ) ); // app_name 451 args.append( 0U ); // replaces_id 452 args.append( QString::fromAscii( "transmission" ) ); // icon 453 args.append( title ); // summary 454 args.append( body ); // body 455 args.append( QStringList( ) ); // actions - unused for plain passive popups 456 args.append( QVariantMap( ) ); // hints - unused atm 457 args.append( int32_t(-1) ); // use the default timeout period 458 m.setArguments( args ); 459 QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m); 460 //std::cerr << qPrintable(replyMsg.errorName()) << std::endl; 461 //std::cerr << qPrintable(replyMsg.errorMessage()) << std::endl; 462 return (replyMsg.type() == QDBusMessage::ReplyMessage) && !replyMsg.arguments().isEmpty(); 463} 464 465/*** 466**** 467***/ 468 469int 470main( int argc, char * argv[] ) 471{ 472 // find .torrents, URLs, magnet links, etc in the command-line args 473 int c; 474 QStringList addme; 475 const char * optarg; 476 char ** argvv = argv; 477 while( ( c = tr_getopt( getUsage( ), argc, (const char **)argvv, opts, &optarg ) ) ) 478 if( c == TR_OPT_UNK ) 479 addme.append( optarg ); 480 481 // try to delegate the work to an existing copy of Transmission 482 // before starting ourselves... 483 bool delegated = false; 484 QDBusConnection bus = QDBusConnection::sessionBus(); 485 for( int i=0, n=addme.size(); i<n; ++i ) 486 { 487 QDBusMessage request = QDBusMessage::createMethodCall( DBUS_SERVICE, 488 DBUS_OBJECT_PATH, 489 DBUS_INTERFACE, 490 QString::fromAscii("AddMetainfo") ); 491 QList<QVariant> arguments; 492 AddData a( addme[i] ); 493 switch( a.type ) { 494 case AddData::URL: arguments.push_back( a.url.toString( ) ); break; 495 case AddData::MAGNET: arguments.push_back( a.magnet ); break; 496 case AddData::FILENAME: arguments.push_back( a.toBase64().constData() ); break; 497 case AddData::METAINFO: arguments.push_back( a.toBase64().constData() ); break; 498 default: break; 499 } 500 request.setArguments( arguments ); 501 502 QDBusMessage response = bus.call( request ); 503 //std::cerr << qPrintable(response.errorName()) << std::endl; 504 //std::cerr << qPrintable(response.errorMessage()) << std::endl; 505 arguments = response.arguments( ); 506 delegated |= (arguments.size()==1) && arguments[0].toBool(); 507 } 508 509 if( delegated ) 510 return 0; 511 512 tr_optind = 1; 513 MyApp app( argc, argv ); 514 return app.exec( ); 515} 516