/* * This file Copyright (C) 2008 Charles Kerr * * This file is licensed by the GPL version 2. Works owned by the * Transmission project are granted a special exemption to clause 2(b) * so that the bulk of its code can remain under the MIT license. * This exemption does not extend to derived works not owned by * the Transmission project. * * $Id$ */ #include #include #include #include /* strcmp */ #include /* getcwd */ #include #include #include #include #include #include #include #include #include #define MY_NAME "transmission-remote" #define DEFAULT_HOST "localhost" #define DEFAULT_PORT TR_DEFAULT_RPC_PORT enum { TAG_LIST, TAG_DETAILS, TAG_FILES }; static const char* getUsage( void ) { return "Transmission "LONG_VERSION_STRING" http://www.transmissionbt.com/\n" "A fast and easy BitTorrent client\n" "\n" "Usage: "MY_NAME" [host] [options]\n" " "MY_NAME" [port] [options]\n" " "MY_NAME" [host:port] [options]\n" "\n" "Notes:\n" " can be 'all', a single index, or a comma-separated list.\n" " can be 'all', a torrent id or hash string, or a comma-separated list of ids and hash strings.\n" "\n" "Examples:\n" " \""MY_NAME" -l\" (list all torrents)\n" " \""MY_NAME" -tall --start\" (start all torrents)\n" " \""MY_NAME" --add ~/Desktop/*torrent\" (add all the torrent files in $HOME/Desktop)\n" " \""MY_NAME" -t1 -i\" (get detailed information on the torrent whose id is '1')\n" " \""MY_NAME" -t1 -Gall -g2,4,6\" (same torrent; only download the second, fourth, and sixth files)\n" " \""MY_NAME" -tall -ph1,2\" (set all torrent's first two files' priorities to high)\n" " \""MY_NAME" -tall -pnall\" (set all torrent's files' priorities to normal)"; } static tr_option opts[] = { { 'a', "add", "Add torrent files", "a", 0, NULL }, { 'b', "debug", "Print debugging information", "b", 0, NULL }, { 'd', "downlimit", "Set the maximum download speed in KB/s", "d", 1, "" }, { 'D', "no-downlimit", "Don't limit the download speed", "D", 0, NULL }, { 'e', "encryption", "Set encryption mode [required, preferred, tolerated]", "e", 1, "" }, { 'f', "files", "Get a file list for the current torrent(s)", "f", 0, NULL }, { 'g', "get", "Mark files for download", "g", 1, "" }, { 'G', "no-get", "Mark files for not downloading", "G", 1, "" }, { 'h', "help", "Show this help page and exit", "h", 0, NULL }, { 'i', "info", "Show details of the current torrent(s)", "i", 0, NULL }, { 'l', "list", "List all torrents", "l", 0, NULL }, { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", 0, NULL }, { 'M', "no-portmap", "Disable portmapping", "M", 0, NULL }, { 'n', "auth", "Set username for authentication", "n", 1, ":" }, { 'p', "port", "Port to listen for incoming peers", "p", 1, "" }, { 900, "priority-high", "Set one or more files' priority as high", "ph", 1, "" }, { 901, "priority-normal", "Set one or more files' priority as normal", "pn", 1, "" }, { 902, "priority-normal", "Set one or more files' priority as low", "pl", 1, "" }, { 'r', "remove", "Remove the current torrent(s)", "r", 0, NULL }, { 's', "start", "Start the current torrent(s)", "s", 0, NULL }, { 'S', "stop", "Stop the current torrent(s)", "S", 0, NULL }, { 't', "torrent", "Set the current torrent(s)", "t", 1, "" }, { 'u', "uplimit", "Set the maximum upload speed in KB/s", "u", 1, "" }, { 'U', "no-uplimit", "Don't limit the upload speed", "U", 0, NULL }, { 'v', "verify", "Verify the current torrent(s)", "v", 0, NULL }, { 'w', "download-dir", "Set the download folder for new torrents", "w", 1, "" }, { 'x', "pex", "Enable peer exchange (PEX)", "x", 0, NULL }, { 'X', "no-pex", "Disable peer exchange (PEX)", "X", 0, NULL }, { 0, NULL, NULL, NULL, 0, NULL } }; static void showUsage( void ) { tr_getopt_usage( MY_NAME, getUsage(), opts ); exit( 0 ); } static int numarg( const char * arg ) { char * end = NULL; const long num = strtol( arg, &end, 10 ); if( *end ) { fprintf( stderr, "Not a number: \"%s\"\n", arg ); showUsage( ); } return num; } static char * reqs[256]; /* arbitrary max */ static int reqCount = 0; static int debug = 0; static char * auth = NULL; static char* absolutify( char * buf, size_t len, const char * path ) { if( *path == '/' ) tr_strlcpy( buf, path, len ); else { char cwd[MAX_PATH_LENGTH]; getcwd( cwd, sizeof( cwd ) ); tr_buildPath( buf, len, cwd, path, NULL ); } return buf; } static char* getEncodedMetainfo( const char * filename ) { size_t len = 0; uint8_t * buf = tr_loadFile( filename, &len ); char * b64 = tr_base64_encode( buf, len, NULL ); tr_free( buf ); return b64; } static void addIdArg( tr_benc * args, const char * id ) { if( !*id ) { fprintf( stderr, "No torrent specified! Please use the -t option first.\n" ); id = "-1"; /* no torrent will have this ID, so should be a no-op */ } if( strcmp( id, "all" ) ) { tr_rpc_parse_list_str( tr_bencDictAdd( args, "ids" ), id, strlen(id) ); } } static void addFiles( tr_benc * args, const char * key, const char * arg ) { tr_benc * files = tr_bencDictAddList( args, key, 100 ); if( !*arg ) { fprintf( stderr, "No files specified!\n" ); arg = "-1"; /* no file will have this index, so should be a no-op */ } if( strcmp( arg, "all" ) ) { const char * walk = arg; while( *walk ) { char * p; unsigned long l; errno = 0; l = strtol( walk, &p, 10 ); if( errno ) break; tr_bencListAddInt( files, l - 1 ); if( *p != ',' ) break; walk = p + 1; } } } static void readargs( int argc, const char ** argv ) { int c; int addingTorrents = 0; const char * optarg; char id[4096]; *id = '\0'; while(( c = tr_getopt( getUsage(), argc, argv, opts, &optarg ))) { char buf[MAX_PATH_LENGTH]; int addArg = TRUE; tr_benc top, *args; tr_bencInitDict( &top, 3 ); int64_t fields = 0; args = tr_bencDictAddDict( &top, "arguments", 0 ); switch( c ) { case TR_OPT_UNK: if( addingTorrents ) { char * tmp = getEncodedMetainfo( optarg ); tr_bencDictAddStr( &top, "method", "torrent-add" ); tr_bencDictAddStr( args, "metainfo", tmp ); tr_free( tmp ); } else { fprintf( stderr, "Unknown option: %s\n", optarg ); addArg = FALSE; } break; case 'a': addingTorrents = 1; addArg = FALSE; break; case 'b': debug = 1; addArg = FALSE; break; case 'd': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "speed-limit-down", numarg( optarg ) ); tr_bencDictAddInt( args, "speed-limit-down-enabled", 1 ); break; case 'D': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "speed-limit-down-enabled", 0 ); break; case 'e': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddStr( args, "encryption", optarg ); break; case 'f': tr_bencDictAddStr( &top, "method", "torrent-get" ); tr_bencDictAddInt( &top, "tag", TAG_FILES ); addIdArg( args, id ); fields = TR_RPC_TORRENT_ID | TR_RPC_TORRENT_FILES | TR_RPC_TORRENT_PRIORITIES; tr_bencDictAddInt( args, "fields", fields ); break; case 'g': tr_bencDictAddStr( &top, "method", "torrent-set" ); addIdArg( args, id ); addFiles( args, "files-wanted", optarg ); break; case 'G': tr_bencDictAddStr( &top, "method", "torrent-set" ); addIdArg( args, id ); addFiles( args, "files-unwanted", optarg ); break; case 'i': tr_bencDictAddStr( &top, "method", "torrent-get" ); tr_bencDictAddInt( &top, "tag", TAG_DETAILS ); addIdArg( args, id ); fields = TR_RPC_TORRENT_ACTIVITY | TR_RPC_TORRENT_ANNOUNCE | TR_RPC_TORRENT_ERROR | TR_RPC_TORRENT_HISTORY | TR_RPC_TORRENT_ID | TR_RPC_TORRENT_INFO | TR_RPC_TORRENT_SCRAPE | TR_RPC_TORRENT_SIZE | TR_RPC_TORRENT_TRACKER_STATS; tr_bencDictAddInt( args, "fields", fields ); break; case 'l': tr_bencDictAddStr( &top, "method", "torrent-get" ); tr_bencDictAddInt( &top, "tag", TAG_LIST ); fields = TR_RPC_TORRENT_ID | TR_RPC_TORRENT_ACTIVITY | TR_RPC_TORRENT_HISTORY | TR_RPC_TORRENT_SIZE; tr_bencDictAddInt( args, "fields", fields ); break; case 'm': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "port-forwarding-enabled", 1 ); break; case 'M': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "port-forwarding-enabled", 0 ); break; case 'n': auth = tr_strdup( optarg ); addArg = FALSE; break; case 'p': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "port", numarg( optarg ) ); break; case 'r': tr_bencDictAddStr( &top, "method", "torrent-remove" ); addIdArg( args, id ); break; case 's': tr_bencDictAddStr( &top, "method", "torrent-start" ); addIdArg( args, id ); break; case 'S': tr_bencDictAddStr( &top, "method", "torrent-stop" ); addIdArg( args, id ); break; case 't': tr_strlcpy( id, optarg, sizeof( id ) ); addArg = FALSE; break; case 'u': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "speed-limit-up", numarg( optarg ) ); tr_bencDictAddInt( args, "speed-limit-up-enabled", 1 ); break; case 'U': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "speed-limit-up-enabled", 0 ); break; case 'v': tr_bencDictAddStr( &top, "method", "torrent-verify" ); addIdArg( args, id ); break; case 'w': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddStr( args, "download-dir", absolutify(buf,sizeof(buf),optarg) ); break; case 'x': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "pex-allowed", 1 ); break; case 'X': tr_bencDictAddStr( &top, "method", "session-set" ); tr_bencDictAddInt( args, "pex-allowed", 0 ); break; case 900: tr_bencDictAddStr( &top, "method", "torrent-set" ); addIdArg( args, id ); addFiles( args, "priority-high", optarg ); break; case 901: tr_bencDictAddStr( &top, "method", "torrent-set" ); addIdArg( args, id ); addFiles( args, "priority-normal", optarg ); break; case 902: tr_bencDictAddStr( &top, "method", "torrent-set" ); addIdArg( args, id ); addFiles( args, "priority-low", optarg ); break; default: fprintf( stderr, "got opt [%d]\n", (int)c ); showUsage( ); break; } if( addArg ) reqs[reqCount++] = tr_bencSaveAsJSON( &top, NULL ); tr_bencFree( &top ); } } /* [host:port] or [host] or [port] */ static void getHostAndPort( int * argc, char ** argv, char ** host, int * port ) { if( *argv[1] != '-' ) { int i; const char * s = argv[1]; const char * delim = strchr( s, ':' ); if( delim ) { /* user passed in both host and port */ *host = tr_strndup( s, delim-s ); *port = atoi( delim+1 ); } else { char * end; const int i = strtol( s, &end, 10 ); if( !*end ) /* user passed in a port */ *port = i; else /* user passed in a host */ *host = tr_strdup( s ); } *argc -= 1; for( i=1; i<*argc; ++i ) argv[i] = argv[i+1]; } } static size_t writeFunc( void * ptr, size_t size, size_t nmemb, void * buf ) { const size_t byteCount = size * nmemb; evbuffer_add( buf, ptr, byteCount ); return byteCount; } static void etaToString( char * buf, size_t buflen, int64_t eta ) { if( eta < 0 ) snprintf( buf, buflen, "Unknown" ); else if( eta < 60 ) snprintf( buf, buflen, "%"PRId64"sec", eta ); else if( eta < (60*60) ) snprintf( buf, buflen, "%"PRId64" min", eta/60 ); else if( eta < (60*60*24) ) snprintf( buf, buflen, "%"PRId64" hrs", eta/(60*60) ); else snprintf( buf, buflen, "%"PRId64" days", eta/(60*60*24) ); } #define KILOBYTE_FACTOR 1024.0 #define MEGABYTE_FACTOR (1024.0 * 1024.0) #define GIGABYTE_FACTOR (1024.0 * 1024.0 * 1024.0) static char* strlratio( char * buf, double numerator, double denominator, size_t buflen ) { if( denominator ) { const double ratio = numerator / denominator; if( ratio < 10.0 ) snprintf( buf, buflen, "%'.2f", ratio ); else if( ratio < 100.0 ) snprintf( buf, buflen, "%'.1f", ratio ); else snprintf( buf, buflen, "%'.0f", ratio ); } else if( numerator ) tr_strlcpy( buf, "Infinity", buflen ); else tr_strlcpy( buf, "None", buflen ); return buf; } static char* strlsize( char * buf, int64_t size, size_t buflen ) { if( !size ) tr_strlcpy( buf, "None", buflen ); else if( size < (int64_t)KILOBYTE_FACTOR ) snprintf( buf, buflen, "%'"PRId64" bytes", (int64_t)size ); else { double displayed_size; if (size < (int64_t)MEGABYTE_FACTOR) { displayed_size = (double) size / KILOBYTE_FACTOR; snprintf( buf, buflen, "%'.1f KB", displayed_size ); } else if (size < (int64_t)GIGABYTE_FACTOR) { displayed_size = (double) size / MEGABYTE_FACTOR; snprintf( buf, buflen, "%'.1f MB", displayed_size ); } else { displayed_size = (double) size / GIGABYTE_FACTOR; snprintf( buf, buflen, "%'.1f GB", displayed_size ); } } return buf; } static const char* torrentStatusToString( int i ) { switch( i ) { case TR_STATUS_CHECK_WAIT: return "Will Verify"; case TR_STATUS_CHECK: return "Verifying"; case TR_STATUS_DOWNLOAD: return "Downloading"; case TR_STATUS_SEED: return "Seeding"; case TR_STATUS_STOPPED: return "Stopped"; default: return "Error"; } } static int isVerifying( int status ) { return ( ( status == TR_STATUS_CHECK_WAIT ) || ( status == TR_STATUS_CHECK ) ); } static void printDetails( tr_benc * top ) { tr_benc *args, *torrents; if( ( tr_bencDictFindDict( top, "arguments", &args ) ) && ( tr_bencDictFindList( args, "torrents", &torrents ) ) ) { int ti, tCount; for( ti=0, tCount=tr_bencListSize( torrents ); ti