(trunk) #2983: add command-line utilities for creating .torrent files, for editing passkeys, for adding/removing trackers, etc.

This commit is contained in:
Charles Kerr
2010-06-16 14:27:24 +00:00
parent 71c1919cfc
commit eda211e5ce
17 changed files with 920 additions and 260 deletions

36
utils/Makefile.am Normal file
View File

@@ -0,0 +1,36 @@
AM_CPPFLAGS = -I@top_srcdir@
AM_CFLAGS = \
@LIBCURL_CFLAGS@ \
@OPENSSL_CFLAGS@ \
@ZLIB_CFLAGS@ \
@PTHREAD_CFLAGS@
AM_LDFLAGS = \
@ZLIB_LDFLAGS@
bin_PROGRAMS = \
transmission-create \
transmission-edit \
transmission-show
transmission_create_SOURCES = create.c
transmission_edit_SOURCES = edit.c
transmission_show_SOURCES = show.c
dist_man_MANS = \
transmission-create.1 \
transmission-edit.1
transmission_create_LDADD = \
$(top_builddir)/libtransmission/libtransmission.a \
$(top_builddir)/third-party/miniupnp/libminiupnp.a \
$(top_builddir)/third-party/libnatpmp/libnatpmp.a \
@DHT_LIBS@ \
@LIBEVENT_LIBS@ \
@LIBCURL_LIBS@ \
@OPENSSL_LIBS@ \
@ZLIB_LIBS@ \
@PTHREAD_LIBS@
transmission_edit_LDADD = $(transmission_create_LDADD)
transmission_show_LDADD = $(transmission_create_LDADD)

152
utils/create.c Normal file
View File

@@ -0,0 +1,152 @@
/*
* This file Copyright (C) 2010 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 <stdio.h>
#include <unistd.h> /* getcwd() */
#include <libtransmission/transmission.h>
#include <libtransmission/makemeta.h>
#include <libtransmission/tr-getopt.h>
#include <libtransmission/utils.h>
#define MY_NAME "transmission-create"
#define MAX_TRACKERS 128
static tr_tracker_info trackers[MAX_TRACKERS];
static int trackerCount = 0;
static tr_bool isPrivate = FALSE;
const char * comment = NULL;
const char * outfile = NULL;
const char * infile = NULL;
static tr_option options[] =
{
{ 'p', "private", "Allow this torrent to only be used with the specified tracker(s)", "p", 0, NULL },
{ 'o', "outfile", "Save the generated .torrent to this filename", "o", 1, "<file>" },
{ 'c', "comment", "Add a comment", "c", 1, "<comment>" },
{ 't', "tracker", "Add a tracker's announce URL", "t", 1, "<url>" },
{ 0, NULL, NULL, NULL, 0, NULL }
};
static const char *
getUsage( void )
{
return "Usage: " MY_NAME " [options] <file|directory>";
}
static int
parseCommandLine( int argc, const char ** argv )
{
int c;
const char * optarg;
while(( c = tr_getopt( getUsage( ), argc, argv, options, &optarg )))
{
switch( c )
{
case 'p': isPrivate = TRUE; break;
case 'o': outfile = optarg; break;
case 'c': comment = optarg; break;
case 't': if( trackerCount + 1 < MAX_TRACKERS ) {
trackers[trackerCount].tier = trackerCount;
trackers[trackerCount].announce = (char*) optarg;
++trackerCount;
}
break;
case TR_OPT_UNK: infile = optarg; break;
default: return 1;
}
}
return 0;
}
static char*
tr_getcwd( void )
{
char buf[2048];
*buf = '\0';
#ifdef WIN32
_getcwd( buf, sizeof( buf ) );
#else
getcwd( buf, sizeof( buf ) );
#endif
return tr_strdup( buf );
}
int
main( int argc, char * argv[] )
{
char * out2 = NULL;
tr_metainfo_builder * b = NULL;
tr_setMessageLevel( TR_MSG_ERR );
if( parseCommandLine( argc, (const char**)argv ) )
return EXIT_FAILURE;
if( !infile )
{
fprintf( stderr, "ERROR: No input file or directory specified.\n" );
tr_getopt_usage( MY_NAME, getUsage( ), options );
fprintf( stderr, "\n" );
return EXIT_FAILURE;
}
if( outfile == NULL )
{
char * base = tr_basename( infile );
char * end = tr_strdup_printf( "%s.torrent", base );
char * cwd = tr_getcwd( );
outfile = out2 = tr_buildPath( cwd, end, NULL );
tr_free( cwd );
tr_free( end );
tr_free( base );
}
if( !trackerCount )
{
if( isPrivate )
{
fprintf( stderr, "ERROR: no trackers specified for a private torrent\n" );
return EXIT_FAILURE;
}
else
{
printf( "WARNING: no trackers specified\n" );
}
}
printf( "Creating torrent \"%s\" ...", outfile );
fflush( stdout );
b = tr_metaInfoBuilderCreate( infile );
tr_makeMetaInfo( b, outfile, trackers, trackerCount, comment, isPrivate );
while( !b->isDone ) {
tr_wait_msec( 500 );
putc( '.', stdout );
fflush( stdout );
}
putc( ' ', stdout );
switch( b->result ) {
case TR_MAKEMETA_OK: printf( "done!" ); break;
case TR_MAKEMETA_URL: printf( "bad announce URL: \"%s\"", b->errfile ); break;
case TR_MAKEMETA_IO_READ: printf( "error reading \"%s\": %s", b->errfile, tr_strerror(b->my_errno) ); break;
case TR_MAKEMETA_IO_WRITE: printf( "error writing \"%s\": %s", b->errfile, tr_strerror(b->my_errno) ); break;
case TR_MAKEMETA_CANCELLED: printf( "cancelled" ); break;
}
putc( '\n', stdout );
tr_free( out2 );
return 0;
}

313
utils/edit.c Normal file
View File

@@ -0,0 +1,313 @@
/*
* This file Copyright (C) 2010 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 <stdio.h>
#include <event.h> /* evbuffer */
#include <libtransmission/transmission.h>
#include <libtransmission/bencode.h>
#include <libtransmission/tr-getopt.h>
#include <libtransmission/utils.h>
#define MY_NAME "transmission-edit"
int fileCount = 0;
const char ** files = NULL;
const char * add = NULL;
const char * deleteme = NULL;
const char * replace[2] = { NULL, NULL };
static tr_option options[] =
{
{ 'a', "add", "Add a tracker's announce URL", "a", 1, "<url>" },
{ 'd', "delete", "Delete a tracker's announce URL", "d", 1, "<url>" },
{ 'r', "replace", "Search and replace a substring in the announce URLs", "r", 1, "<old> <new>" },
{ 0, NULL, NULL, NULL, 0, NULL }
};
static const char *
getUsage( void )
{
return "Usage: " MY_NAME " [options] torrent-file(s)";
}
static int
parseCommandLine( int argc, const char ** argv )
{
int c;
const char * optarg;
while(( c = tr_getopt( getUsage( ), argc, argv, options, &optarg )))
{
switch( c )
{
case 'a': add = optarg;
break;
case 'd': deleteme = optarg;
break;
case 'r': replace[0] = optarg;
c = tr_getopt( getUsage( ), argc, argv, options, &optarg );
if( c != TR_OPT_UNK ) return 1;
replace[1] = optarg;
break;
case TR_OPT_UNK: files[fileCount++] = optarg; break;
default: return 1;
}
}
return 0;
}
static tr_bool
removeURL( tr_benc * metainfo, const char * url )
{
const char * str;
tr_benc * announce_list;
tr_bool changed = FALSE;
if( tr_bencDictFindStr( metainfo, "announce", &str ) && !strcmp( str, url ) )
{
printf( "\tRemoved \"%s\" from \"announce\"\n", str );
tr_bencDictRemove( metainfo, "announce" );
changed = TRUE;
}
if( tr_bencDictFindList( metainfo, "announce-list", &announce_list ) )
{
tr_benc * tier;
int tierIndex = 0;
while(( tier = tr_bencListChild( announce_list, tierIndex )))
{
tr_benc * node;
int nodeIndex = 0;
while(( node = tr_bencListChild( tier, nodeIndex )))
{
if( tr_bencGetStr( node, &str ) && !strcmp( str, url ) )
{
printf( "\tRemoved \"%s\" from \"announce-list\" tier #%d\n", str, (tierIndex+1) );
tr_bencListRemove( tier, nodeIndex );
changed = TRUE;
}
else ++nodeIndex;
}
if( tr_bencListSize( tier ) == 0 )
{
printf( "\tNo URLs left in tier #%d... removing tier\n", (tierIndex+1) );
tr_bencListRemove( announce_list, tierIndex );
}
else ++tierIndex;
}
if( tr_bencListSize( announce_list ) == 0 )
{
printf( "\tNo tiers left... removing announce-list\n" );
tr_bencDictRemove( metainfo, "announce-list" );
}
}
/* if we removed the "announce" field and there's still another track left,
* use it as the "announce" field */
if( changed && !tr_bencDictFindStr( metainfo, "announce", &str ) )
{
tr_benc * tier;
tr_benc * node;
if(( tier = tr_bencListChild( announce_list, 0 ))) {
if(( node = tr_bencListChild( tier, 0 ))) {
if( tr_bencGetStr( node, &str ) ) {
tr_bencDictAddStr( metainfo, "announce", str );
printf( "\tAdded \"%s\" to announce\n", str );
}
}
}
}
return changed;
}
static char*
replaceSubstr( const char * str, const char * in, const char * out )
{
char * walk;
struct evbuffer * buf = evbuffer_new( );
const size_t inlen = strlen( in );
const size_t outlen = strlen( out );
while(( walk = strstr( str, in )))
{
evbuffer_add( buf, str, walk-str );
evbuffer_add( buf, out, outlen );
str = walk + inlen;
}
walk = tr_strndup( EVBUFFER_DATA( buf ), EVBUFFER_LENGTH( buf ) );
evbuffer_free( buf );
return walk;
}
static tr_bool
replaceURL( tr_benc * metainfo, const char * in, const char * out )
{
const char * str;
tr_benc * announce_list;
tr_bool changed = FALSE;
if( tr_bencDictFindStr( metainfo, "announce", &str ) && strstr( str, in ) )
{
char * newstr = replaceSubstr( str, in, out );
printf( "\tReplaced in \"announce\": \"%s\" --> \"%s\"\n", str, newstr );
tr_bencDictAddStr( metainfo, "announce", newstr );
tr_free( newstr );
changed = TRUE;
}
if( tr_bencDictFindList( metainfo, "announce-list", &announce_list ) )
{
tr_benc * tier;
int tierCount = 0;
while(( tier = tr_bencListChild( announce_list, tierCount++ )))
{
tr_benc * node;
int nodeCount = 0;
while(( node = tr_bencListChild( tier, nodeCount++ )))
{
if( tr_bencGetStr( node, &str ) && strstr( str, in ) )
{
char * newstr = replaceSubstr( str, in, out );
printf( "\tReplaced in \"announce-list\" tier %d: \"%s\" --> \"%s\"\n", tierCount, str, newstr );
tr_bencFree( node );
tr_bencInitStr( node, newstr, -1 );
tr_free( newstr );
changed = TRUE;
}
}
}
}
return changed;
}
static tr_bool
addURL( tr_benc * metainfo, const char * url )
{
const char * str;
tr_benc * announce_list;
tr_bool changed = FALSE;
tr_bool match = FALSE;
/* maybe add it to "announce" */
if( !tr_bencDictFindStr( metainfo, "announce", &str ) )
{
printf( "\tAdded \"%s\" in \"announce\"\n", url );
tr_bencDictAddStr( metainfo, "announce", url );
changed = TRUE;
}
/* see if it's already in announce-list */
if( tr_bencDictFindList( metainfo, "announce-list", &announce_list ) ) {
tr_benc * tier;
int tierCount = 0;
while(( tier = tr_bencListChild( announce_list, tierCount++ ))) {
tr_benc * node;
int nodeCount = 0;
while(( node = tr_bencListChild( tier, nodeCount++ )))
if( tr_bencGetStr( node, &str ) && !strcmp( str, url ) )
match = TRUE;
}
}
/* if it's not in announce-list, add it now */
if( !match )
{
tr_benc * tier;
if( !tr_bencDictFindList( metainfo, "announce-list", &announce_list ) )
announce_list = tr_bencDictAddList( metainfo, "announce-list", 1 );
tier = tr_bencListAddList( announce_list, 1 );
tr_bencListAddStr( tier, url );
printf( "\tAdded \"%s\" to \"announce-list\" tier %d\n", url, tr_bencListSize( announce_list ) );
changed = TRUE;
}
return changed;
}
int
main( int argc, char * argv[] )
{
int i;
int changedCount = 0;
files = tr_new0( const char*, argc );
tr_setMessageLevel( TR_MSG_ERR );
if( parseCommandLine( argc, (const char**)argv ) )
return EXIT_FAILURE;
if( fileCount < 1 )
{
fprintf( stderr, "ERROR: No torrent files specified.\n" );
tr_getopt_usage( MY_NAME, getUsage( ), options );
fprintf( stderr, "\n" );
return EXIT_FAILURE;
}
if( !add && !deleteme && !replace[0] )
{
fprintf( stderr, "ERROR: Must specify -a, -d or -r\n" );
tr_getopt_usage( MY_NAME, getUsage( ), options );
fprintf( stderr, "\n" );
return EXIT_FAILURE;
}
for( i=0; i<fileCount; ++i )
{
tr_benc top;
tr_bool changed = FALSE;
const char * filename = files[i];
printf( "%s\n", filename );
if( tr_bencLoadFile( &top, TR_FMT_BENC, filename ) )
{
printf( "\tError reading file\n" );
continue;
}
if( deleteme != NULL )
changed |= removeURL( &top, deleteme );
if( add != NULL )
changed = addURL( &top, add );
if( replace[0] && replace[1] )
changed |= replaceURL( &top, replace[0], replace[1] );
if( changed )
{
++changedCount;
tr_bencToFile( &top, TR_FMT_BENC, filename );
}
tr_bencFree( &top );
}
printf( "Changed %d files\n", changedCount );
tr_free( files );
return 0;
}

298
utils/show.c Normal file
View File

@@ -0,0 +1,298 @@
/*
* This file Copyright (C) 2010 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 <stdio.h>
#include <time.h>
#define CURL_DISABLE_TYPECHECK /* otherwise -Wunreachable-code goes insane */
#include <curl/curl.h>
#include <event.h> /* struct evbuffer */
#include <libtransmission/transmission.h>
#include <libtransmission/bencode.h>
#include <libtransmission/tr-getopt.h>
#include <libtransmission/utils.h>
#include <libtransmission/web.h> /* tr_webGetResponseStr */
#include <libtransmission/version.h>
#define MY_NAME "transmission-show"
#define TIMEOUT_SECS 30
static tr_option options[] =
{
{ 's', "scrape", "Ask the torrent's trackers how many peers are in the torrent's swarm", "s", 0, NULL },
{ 0, NULL, NULL, NULL, 0, NULL }
};
static const char *
getUsage( void )
{
return "Usage: " MY_NAME " [options] <.torrent file>";
}
static tr_bool scrapeFlag = FALSE;
const char * filename = NULL;
static int
parseCommandLine( int argc, const char ** argv )
{
int c;
const char * optarg;
while(( c = tr_getopt( getUsage( ), argc, argv, options, &optarg )))
{
switch( c )
{
case 's': scrapeFlag = TRUE; break;
case TR_OPT_UNK: filename = optarg; break;
default: return 1;
}
}
return 0;
}
static const double KiB = 1024.0;
static const double MiB = 1024.0 * 1024.0;
static const double GiB = 1024.0 * 1024.0 * 1024.0;
static char*
strlsize( char * buf, int64_t size, size_t buflen )
{
if( !size )
tr_strlcpy( buf, "None", buflen );
else if( size < (int64_t)KiB )
tr_snprintf( buf, buflen, "%'" PRId64 " bytes", (int64_t)size );
else
{
double displayed_size;
if( size < (int64_t)MiB )
{
displayed_size = (double) size / KiB;
tr_snprintf( buf, buflen, "%'.1f KiB", displayed_size );
}
else if( size < (int64_t)GiB )
{
displayed_size = (double) size / MiB;
tr_snprintf( buf, buflen, "%'.1f MiB", displayed_size );
}
else
{
displayed_size = (double) size / GiB;
tr_snprintf( buf, buflen, "%'.1f GiB", displayed_size );
}
}
return buf;
}
static void
showInfo( const tr_info * inf )
{
int i;
char buf[128];
int prevTier = -1;
/**
*** General Info
**/
printf( "GENERAL\n\n" );
printf( " Name: %s\n", inf->name );
printf( " Hash: %s\n", inf->hashString );
printf( " Created by: %s\n", inf->creator ? inf->creator : "Unknown" );
if( !inf->dateCreated )
printf( " Created on: Unknown\n" );
else {
struct tm tm = *localtime( &inf->dateCreated );
printf( " Created on: %s", asctime( &tm ) );
}
if( inf->comment && *inf->comment )
printf( " Comment: %s\n", inf->comment );
printf( " Piece Count: %d\n", inf->pieceCount );
printf( " Piece Size: %s\n", strlsize( buf, inf->pieceSize, sizeof( buf ) ) );
printf( " Total Size: %s\n", strlsize( buf, inf->totalSize, sizeof( buf ) ) );
printf( " Privacy: %s\n", inf->isPrivate ? "Private torrent" : "Public torrent" );
/**
*** Trackers
**/
printf( "\nTRACKERS\n" );
for( i=0; i<(int)inf->trackerCount; ++i )
{
if( prevTier != inf->trackers[i].tier )
{
prevTier = inf->trackers[i].tier;
printf( "\n Tier #%d\n", prevTier + 1 );
}
printf( " %s\n", inf->trackers[i].announce );
}
/**
*** Files
**/
printf( "\nFILES\n\n" );
for( i=0; i<(int)inf->fileCount; ++i )
printf( " %s (%s)\n", inf->files[i].name, strlsize( buf, inf->files[i].length, sizeof( buf ) ) );
}
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 CURL*
tr_curl_easy_init( struct evbuffer * writebuf )
{
CURL * curl = curl_easy_init( );
curl_easy_setopt( curl, CURLOPT_USERAGENT, MY_NAME "/" LONG_VERSION_STRING );
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, writeFunc );
curl_easy_setopt( curl, CURLOPT_WRITEDATA, writebuf );
curl_easy_setopt( curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY );
curl_easy_setopt( curl, CURLOPT_VERBOSE, getenv( "TR_CURL_VERBOSE" ) != NULL );
curl_easy_setopt( curl, CURLOPT_ENCODING, "" );
return curl;
}
static void
doScrape( const tr_info * inf )
{
int i;
for( i=0; i<inf->trackerCount; ++i )
{
CURL * curl;
CURLcode res;
struct evbuffer * buf;
const char * url = inf->trackers[i].scrape;
if( url == NULL )
continue;
printf( "%s ... ", url );
fflush( stdout );
buf = evbuffer_new( );
curl = tr_curl_easy_init( buf );
curl_easy_setopt( curl, CURLOPT_URL, url );
curl_easy_setopt( curl, CURLOPT_TIMEOUT, TIMEOUT_SECS );
if(( res = curl_easy_perform( curl )))
{
printf( "error: %s\n", curl_easy_strerror( res ) );
}
else
{
long response;
curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response );
if( response != 200 )
{
printf( "error: unexpected response %ld \"%s\"\n",
response,
tr_webGetResponseStr( response ) );
}
else /* HTTP OK */
{
tr_benc top;
tr_benc * files;
tr_bool matched = FALSE;
const char * begin = (const char*) EVBUFFER_DATA( buf );
const char * end = begin + EVBUFFER_LENGTH( buf );
if( !tr_bencParse( begin, end, &top, NULL ) )
{
if( tr_bencDictFindDict( &top, "files", &files ) )
{
int i = 0;
tr_benc * val;
const char * key;
while( tr_bencDictChild( files, i++, &key, &val ))
{
if( !memcmp( inf->hash, key, SHA_DIGEST_LENGTH ) )
{
int64_t seeders = -1;
int64_t leechers = -1;
tr_bencDictFindInt( val, "complete", &seeders );
tr_bencDictFindInt( val, "incomplete", &leechers );
printf( "%d seeders, %d leechers\n", (int)seeders, (int)leechers );
matched = TRUE;
}
}
}
tr_bencFree( &top );
}
if( !matched )
printf( "no match\n" );
}
}
evbuffer_free( buf );
curl_easy_cleanup( curl );
}
}
int
main( int argc, char * argv[] )
{
int err;
tr_info inf;
tr_ctor * ctor;
tr_setMessageLevel( TR_MSG_ERR );
if( parseCommandLine( argc, (const char**)argv ) )
return EXIT_FAILURE;
/* make sure the user specified a filename */
if( !filename )
{
fprintf( stderr, "ERROR: No .torrent file specified.\n" );
tr_getopt_usage( MY_NAME, getUsage( ), options );
fprintf( stderr, "\n" );
return EXIT_FAILURE;
}
/* try to parse the .torrent file */
ctor = tr_ctorNew( NULL );
tr_ctorSetMetainfoFromFile( ctor, filename );
err = tr_torrentParse( ctor, &inf );
tr_ctorFree( ctor );
if( err )
{
fprintf( stderr, "Error parsing .torrent file \"%s\"\n", filename );
return 1;
}
printf( "Name: %s\n", inf.name );
printf( "File: %s\n", filename );
printf( "\n" );
fflush( stdout );
if( scrapeFlag )
doScrape( &inf );
else
showInfo( &inf );
/* cleanup */
putc( '\n', stdout );
tr_metainfoFree( &inf );
return 0;
}

View File

@@ -0,0 +1,44 @@
.Dd June 9, 2010
.Dt TRANSMISSION-CREATE 1
.Os
.Sh NAME
.Nm transmission-create
.Nd command-line utility to create .torrent files
.Sh SYNOPSIS
.Bk -words
.Nm
.Op Fl h
.Op Fl p
.Op Fl o Ar file
.Op Fl c Ar comment
.Op Fl t Ar tracker
.Op Ar source file or directory
.Ek
.Sh DESCRIPTION
.Nm
creates BitTorrent .torrent files from the command line
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl h Fl -help
Show a short help page and exit.
.It Fl p Fl -private
Flag the torrent as intended for use on private trackers.
.It Fl c Fl -comment
Add a comment to the torrent file.
.It Fl t Fl -tracker
Add a tracker's
.Ar announce URL
to the .torrent. Most torrents will have at least one
.Ar announce URL.
To add more than one, use this option multiple times.
.Sh AUTHORS
.An -nosplit
.An Charles Kerr
.Sh SEE ALSO
.Xr transmission-daemon 1 ,
.Xr transmission-gtk 1 ,
.Xr transmission-edit 1 ,
.Xr transmission-show 1
.Pp
http://www.transmissionbt.com/

43
utils/transmission-edit.1 Normal file
View File

@@ -0,0 +1,43 @@
.Dd June 9, 2010
.Dt TRANSMISSION-EDIT 1
.Os
.Sh NAME
.Nm transmission-edit
.Nd command-line utility to modify .torrent files' announce URLs
.Sh SYNOPSIS
.Bk -words
.Nm
.Op Fl h
.Op Fl a Ar url
.Op Fl d Ar url
.Op Fl r Ar search Ar replace
.Ar torrentfile(s)
.Ek
.Sh DESCRIPTION
.Nm
command-line utility to modify .torrent files' announce URLs
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl h Fl -help
Show a short help page and exit.
.It Fl a Fl -add Ar URL
Add an announce URL to the torrent's announce-list if it's not already in the list
.It Fl d Fl -delete Ar URL
Remove an announce URL from the torrent's announce-list
.It Fl r Fl -replace Ar search Ar replace
Substring search-and-replace inside a torrent's announce URLs. This can be used to change an announce URL when the tracker moves or your passcode changes.
.Sh EXAMPLES
Update a tracker passcode in all your torrents:
.Bd -literal -offset indent
$ transmission-edit -r old-passcode new-passcode ~/.config/transmission/torrents/*\\.torrent
.Ed
.Sh AUTHORS
.An -nosplit
.An Charles Kerr
.Sh SEE ALSO
.Xr transmission-daemon 1 ,
.Xr transmission-gtk 1,
.Xr transmission-create 1 ,
.Xr transmission-show 1
.Pp
http://www.transmissionbt.com/