/* * This file Copyright (C) Mnemosyne LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * $Id$ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "add-data.h" #include "app.h" #include "dbus-adaptor.h" #include "formatter.h" #include "mainwin.h" #include "options.h" #include "prefs.h" #include "session.h" #include "session-dialog.h" #include "torrent-model.h" #include "utils.h" #include "watchdir.h" namespace { const QString DBUS_SERVICE = QString::fromAscii( "com.transmissionbt.Transmission" ); const QString DBUS_OBJECT_PATH = QString::fromAscii( "/com/transmissionbt/Transmission" ); const QString DBUS_INTERFACE = QString::fromAscii( "com.transmissionbt.Transmission" ); const char * MY_READABLE_NAME( "transmission-qt" ); const tr_option opts[] = { { 'g', "config-dir", "Where to look for configuration files", "g", 1, "" }, { 'm', "minimized", "Start minimized in system tray", "m", 0, NULL }, { 'p', "port", "Port to use when connecting to an existing session", "p", 1, "" }, { 'r', "remote", "Connect to an existing session at the specified hostname", "r", 1, "" }, { 'u', "username", "Username to use when connecting to an existing session", "u", 1, "" }, { 'v', "version", "Show version number and exit", "v", 0, NULL }, { 'w', "password", "Password to use when connecting to an existing session", "w", 1, "" }, { 0, NULL, NULL, NULL, 0, NULL } }; const char* getUsage( void ) { return "Usage:\n" " transmission [OPTIONS...] [torrent files]"; } void showUsage( void ) { tr_getopt_usage( MY_READABLE_NAME, getUsage( ), opts ); exit( 0 ); } enum { STATS_REFRESH_INTERVAL_MSEC = 3000, SESSION_REFRESH_INTERVAL_MSEC = 3000, MODEL_REFRESH_INTERVAL_MSEC = 3000 }; } MyApp :: MyApp( int& argc, char ** argv ): QApplication( argc, argv ), myLastFullUpdateTime( 0 ) { const QString MY_CONFIG_NAME = QString::fromAscii( "transmission" ); setApplicationName( MY_CONFIG_NAME ); // install the qt translator qtTranslator.load( "qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); installTranslator( &qtTranslator ); // install the transmission translator appTranslator.load( QString(MY_CONFIG_NAME) + "_" + QLocale::system().name(), QCoreApplication::applicationDirPath() + "/translations" ); installTranslator( &appTranslator ); Formatter::initUnits( ); // set the default icon QIcon icon; QList sizes; sizes << 16 << 22 << 24 << 32 << 48; foreach( int size, sizes ) icon.addPixmap( QPixmap( QString::fromAscii(":/icons/transmission-%1.png" ).arg(size) ) ); setWindowIcon( icon ); // parse the command-line arguments int c; bool minimized = false; const char * optarg; const char * host = 0; const char * port = 0; const char * username = 0; const char * password = 0; const char * configDir = 0; QStringList filenames; while( ( c = tr_getopt( getUsage( ), argc, (const char**)argv, opts, &optarg ) ) ) { switch( c ) { case 'g': configDir = optarg; break; case 'p': port = optarg; break; case 'r': host = optarg; break; case 'u': username = optarg; break; case 'w': password = optarg; break; case 'm': minimized = true; break; case 'v': std::cerr << MY_READABLE_NAME << ' ' << LONG_VERSION_STRING << std::endl; ::exit( 0 ); break; case TR_OPT_ERR: Utils::toStderr( QObject::tr( "Invalid option" ) ); showUsage( ); break; default: filenames.append( optarg ); break; } } // set the fallback config dir if( configDir == 0 ) configDir = tr_getDefaultConfigDir( "transmission" ); // ensure our config directory exists QDir dir( configDir ); if( !dir.exists() ) dir.mkpath( configDir ); // is this the first time we've run transmission? const bool firstTime = !QFile(QDir(configDir).absoluteFilePath("settings.json")).exists(); // initialize the prefs myPrefs = new Prefs ( configDir ); if( host != 0 ) myPrefs->set( Prefs::SESSION_REMOTE_HOST, host ); if( port != 0 ) myPrefs->set( Prefs::SESSION_REMOTE_PORT, port ); if( username != 0 ) myPrefs->set( Prefs::SESSION_REMOTE_USERNAME, username ); if( password != 0 ) myPrefs->set( Prefs::SESSION_REMOTE_PASSWORD, password ); if( ( host != 0 ) || ( port != 0 ) || ( username != 0 ) || ( password != 0 ) ) myPrefs->set( Prefs::SESSION_IS_REMOTE, true ); if ( myPrefs->getBool( Prefs::START_MINIMIZED) ) minimized = true; // start as minimized only if the system tray present if ( !myPrefs->getBool( Prefs::SHOW_TRAY_ICON ) ) minimized = false; mySession = new Session( configDir, *myPrefs ); myModel = new TorrentModel( *myPrefs ); myWindow = new TrMainWindow( *mySession, *myPrefs, *myModel, minimized ); myWatchDir = new WatchDir( *myModel ); // when the session gets torrent info, update the model connect( mySession, SIGNAL(torrentsUpdated(tr_variant*,bool)), myModel, SLOT(updateTorrents(tr_variant*,bool)) ); connect( mySession, SIGNAL(torrentsUpdated(tr_variant*,bool)), myWindow, SLOT(refreshActionSensitivity()) ); connect( mySession, SIGNAL(torrentsRemoved(tr_variant*)), myModel, SLOT(removeTorrents(tr_variant*)) ); // when the session source gets changed, request a full refresh connect( mySession, SIGNAL(sourceChanged()), this, SLOT(onSessionSourceChanged()) ); // when the model sees a torrent for the first time, ask the session for full info on it connect( myModel, SIGNAL(torrentsAdded(QSet)), mySession, SLOT(initTorrents(QSet)) ); connect( myModel, SIGNAL(torrentsAdded(QSet)), this, SLOT(onTorrentsAdded(QSet)) ); mySession->initTorrents( ); mySession->refreshSessionStats( ); // when torrents are added to the watch directory, tell the session connect( myWatchDir, SIGNAL(torrentFileAdded(QString)), this, SLOT(addTorrent(QString)) ); // init from preferences QList initKeys; initKeys << Prefs::DIR_WATCH; foreach( int key, initKeys ) refreshPref( key ); connect( myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(const int)) ); QTimer * timer = &myModelTimer; connect( timer, SIGNAL(timeout()), this, SLOT(refreshTorrents()) ); timer->setSingleShot( false ); timer->setInterval( MODEL_REFRESH_INTERVAL_MSEC ); timer->start( ); timer = &myStatsTimer; connect( timer, SIGNAL(timeout()), mySession, SLOT(refreshSessionStats()) ); timer->setSingleShot( false ); timer->setInterval( STATS_REFRESH_INTERVAL_MSEC ); timer->start( ); timer = &mySessionTimer; connect( timer, SIGNAL(timeout()), mySession, SLOT(refreshSessionInfo()) ); timer->setSingleShot( false ); timer->setInterval( SESSION_REFRESH_INTERVAL_MSEC ); timer->start( ); maybeUpdateBlocklist( ); if( !firstTime ) mySession->restart( ); else { QDialog * d = new SessionDialog( *mySession, *myPrefs, myWindow ); d->show( ); } if( !myPrefs->getBool( Prefs::USER_HAS_GIVEN_INFORMED_CONSENT )) { QDialog * dialog = new QDialog( myWindow ); dialog->setModal( true ); QVBoxLayout * v = new QVBoxLayout( dialog ); 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. Any content you share is your sole responsibility.")); l->setWordWrap( true ); v->addWidget( l ); QDialogButtonBox * box = new QDialogButtonBox; box->addButton( new QPushButton( tr( "&Cancel" ) ), QDialogButtonBox::RejectRole ); QPushButton * agree = new QPushButton( tr( "I &Agree" ) ); agree->setDefault( true ); box->addButton( agree, QDialogButtonBox::AcceptRole ); box->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed ); box->setOrientation( Qt::Horizontal ); v->addWidget( box ); connect( box, SIGNAL(rejected()), this, SLOT(quit()) ); connect( box, SIGNAL(accepted()), dialog, SLOT(deleteLater()) ); connect( box, SIGNAL(accepted()), this, SLOT(consentGiven()) ); dialog->show(); } for( QStringList::const_iterator it=filenames.begin(), end=filenames.end(); it!=end; ++it ) addTorrent( *it ); // register as the dbus handler for Transmission new TrDBusAdaptor( this ); QDBusConnection bus = QDBusConnection::sessionBus(); if( !bus.registerService( DBUS_SERVICE ) ) std::cerr << "couldn't register " << qPrintable(DBUS_SERVICE) << std::endl; if( !bus.registerObject( DBUS_OBJECT_PATH, this ) ) std::cerr << "couldn't register " << qPrintable(DBUS_OBJECT_PATH) << std::endl; } /* these functions are for popping up desktop notifications */ void MyApp :: onTorrentsAdded( QSet torrents ) { if( !myPrefs->getBool( Prefs::SHOW_NOTIFICATION_ON_ADD ) ) return; foreach( int id, torrents ) { Torrent * tor = myModel->getTorrentFromId( id ); if( tor->name().isEmpty( ) ) // wait until the torrent's INFO fields are loaded connect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onNewTorrentChanged(int)) ); else { onNewTorrentChanged( id ); if( !tor->isSeed( ) ) connect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) ); } } } void MyApp :: onTorrentCompleted( int id ) { Torrent * tor = myModel->getTorrentFromId (id); if (tor) { if (myPrefs->getBool (Prefs::SHOW_NOTIFICATION_ON_COMPLETE)) notify (tr("Torrent Completed"), tor->name()); if (myPrefs->getBool (Prefs::COMPLETE_SOUND_ENABLED)) { #if defined( Q_OS_WIN ) || defined( Q_OS_MAC ) QApplication::beep(); #else QProcess::execute (myPrefs->getString(Prefs::COMPLETE_SOUND_COMMAND)); #endif } disconnect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) ); } } void MyApp :: onNewTorrentChanged( int id ) { Torrent * tor = myModel->getTorrentFromId( id ); if( tor && !tor->name().isEmpty() ) { const int age_secs = tor->dateAdded().secsTo(QDateTime::currentDateTime()); if( age_secs < 30 ) notify( tr( "Torrent Added" ), tor->name( ) ); disconnect( tor, SIGNAL(torrentChanged(int)), this, SLOT(onNewTorrentChanged(int)) ); if( !tor->isSeed( ) ) connect( tor, SIGNAL(torrentCompleted(int)), this, SLOT(onTorrentCompleted(int)) ); } } /*** **** ***/ void MyApp :: consentGiven( ) { myPrefs->set( Prefs::USER_HAS_GIVEN_INFORMED_CONSENT, true ); } MyApp :: ~MyApp( ) { const QRect mainwinRect( myWindow->geometry( ) ); delete myWatchDir; delete myWindow; delete myModel; delete mySession; myPrefs->set( Prefs :: MAIN_WINDOW_HEIGHT, std::max( 100, mainwinRect.height( ) ) ); myPrefs->set( Prefs :: MAIN_WINDOW_WIDTH, std::max( 100, mainwinRect.width( ) ) ); myPrefs->set( Prefs :: MAIN_WINDOW_X, mainwinRect.x( ) ); myPrefs->set( Prefs :: MAIN_WINDOW_Y, mainwinRect.y( ) ); delete myPrefs; } /*** **** ***/ void MyApp :: refreshPref( int key ) { switch( key ) { case Prefs :: BLOCKLIST_UPDATES_ENABLED: maybeUpdateBlocklist( ); break; case Prefs :: DIR_WATCH: case Prefs :: DIR_WATCH_ENABLED: { const QString path( myPrefs->getString( Prefs::DIR_WATCH ) ); const bool isEnabled( myPrefs->getBool( Prefs::DIR_WATCH_ENABLED ) ); myWatchDir->setPath( path, isEnabled ); break; } default: break; } } void MyApp :: maybeUpdateBlocklist( ) { if( !myPrefs->getBool( Prefs :: BLOCKLIST_UPDATES_ENABLED ) ) return; const QDateTime lastUpdatedAt = myPrefs->getDateTime( Prefs :: BLOCKLIST_DATE ); const QDateTime nextUpdateAt = lastUpdatedAt.addDays( 7 ); const QDateTime now = QDateTime::currentDateTime( ); if( now < nextUpdateAt ) { mySession->updateBlocklist( ); myPrefs->set( Prefs :: BLOCKLIST_DATE, now ); } } void MyApp :: onSessionSourceChanged( ) { mySession->initTorrents( ); mySession->refreshSessionStats( ); mySession->refreshSessionInfo( ); } void MyApp :: refreshTorrents( ) { // usually we just poll the torrents that have shown recent activity, // but we also periodically ask for updates on the others to ensure // nothing's falling through the cracks. const time_t now = time( NULL ); if( myLastFullUpdateTime + 60 >= now ) mySession->refreshActiveTorrents( ); else { myLastFullUpdateTime = now; mySession->refreshAllTorrents( ); } } /*** **** ***/ void MyApp :: addTorrent( const QString& key ) { const AddData addme( key ); if( addme.type != addme.NONE ) addTorrent( addme ); } void MyApp :: addTorrent( const AddData& addme ) { if( !myPrefs->getBool( Prefs :: OPTIONS_PROMPT ) ) { mySession->addTorrent( addme ); } else if( addme.type == addme.URL ) { myWindow->openURL( addme.url.toString( ) ); } else if( addme.type == addme.MAGNET ) { myWindow->openURL( addme.magnet ); } else { Options * o = new Options( *mySession, *myPrefs, addme, myWindow ); o->show( ); } raise( ); } /*** **** ***/ void MyApp :: raise( ) { QApplication :: alert ( myWindow ); } bool MyApp :: notify( const QString& title, const QString& body ) const { const QString dbusServiceName = QString::fromAscii( "org.freedesktop.Notifications" ); const QString dbusInterfaceName = QString::fromAscii( "org.freedesktop.Notifications" ); const QString dbusPath = QString::fromAscii( "/org/freedesktop/Notifications" ); QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QString::fromAscii("Notify")); QList args; args.append( QString::fromAscii( "Transmission" ) ); // app_name args.append( 0U ); // replaces_id args.append( QString::fromAscii( "transmission" ) ); // icon args.append( title ); // summary args.append( body ); // body args.append( QStringList( ) ); // actions - unused for plain passive popups args.append( QVariantMap( ) ); // hints - unused atm args.append( int32_t(-1) ); // use the default timeout period m.setArguments( args ); QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m); //std::cerr << qPrintable(replyMsg.errorName()) << std::endl; //std::cerr << qPrintable(replyMsg.errorMessage()) << std::endl; return (replyMsg.type() == QDBusMessage::ReplyMessage) && !replyMsg.arguments().isEmpty(); } /*** **** ***/ int main( int argc, char * argv[] ) { // find .torrents, URLs, magnet links, etc in the command-line args int c; QStringList addme; const char * optarg; char ** argvv = argv; while( ( c = tr_getopt( getUsage( ), argc, (const char **)argvv, opts, &optarg ) ) ) if( c == TR_OPT_UNK ) addme.append( optarg ); // try to delegate the work to an existing copy of Transmission // before starting ourselves... bool delegated = false; QDBusConnection bus = QDBusConnection::sessionBus(); for( int i=0, n=addme.size(); i arguments; AddData a( addme[i] ); switch( a.type ) { case AddData::URL: arguments.push_back( a.url.toString( ) ); break; case AddData::MAGNET: arguments.push_back( a.magnet ); break; case AddData::FILENAME: arguments.push_back( a.toBase64().constData() ); break; case AddData::METAINFO: arguments.push_back( a.toBase64().constData() ); break; default: break; } request.setArguments( arguments ); QDBusMessage response = bus.call( request ); //std::cerr << qPrintable(response.errorName()) << std::endl; //std::cerr << qPrintable(response.errorMessage()) << std::endl; arguments = response.arguments( ); delegated |= (arguments.size()==1) && arguments[0].toBool(); } if( delegated ) return 0; tr_optind = 1; MyApp app( argc, argv ); return app.exec( ); }