Добрый день.
На своём домашнем серваке сменил систему, и собственно софт тоже нужно было переставлять.
Поэтому ради теста просмотрел несколько самых популярных торрент-клиентов, работающих на *nix (rTorrent, Deluge, MLDonkey, Transmission).
Последний понравился мне больше всего, однако для меня нашёлся существенный минус — невозможно переименовывать зашитые в .torrent-файл имена торрентов.
То есть у нас на диске будут всякие разные папки, например — «Krovavaja gora», «Место Преступления Нью-Йорк», а то и просто «7 Сезон».
Мне это не по нраву, я люблю порядок, соотвестсвенно свою фильмотеку (точнее её сериальную часть) организую в виде "%SERIAL_NAME%/Season N".
Transmission увы не позволяет такого. Но так как в основном всё было хорошо, я взялся подгонять клиент под себя.
Первая подзадача — возможность переименования папки с содержимым торрента в файловой системе, и продолжение корректной работы.
Меня не первого посетила такая простая мысль, что это нужно для клиента. В багтрекинговой системе существует тикет трехгодичной давности #1220. К сожалению, разработчики как-то вяло реагируют на него, однако камрад juxda любезно написал патч, который добавляет данный функционал к сорцам.
Однако радиус кривизны моих рук не позволил корректно наложить патч даже на ту ревизию (11895) для которой он изготовлен. Кроме того хотелось всё же иметь наиболее свежую версию торрент-клиента с данной фишкой, ибо с той ревизии прошло ~500 коммитов.
Поэтому я избрал путь вдумчивого патчинга, с разбором того, что конкретно мы делаем, дабы можно было и самому подправить в случае чего, да и допилить клиенты к демону.
Начинается всё просто:
Забираем HEAD-ревизию с SVN-сервера. Список нужных пакетов есть в траке, замечу что если не нужен gtk-клиент, то и часть либ можно не ставить (libgtk2.0-dev, libnotify-dev, libglib2.0-dev можно проигнорить). libevent-dev нужен свежий (2.0.10 на данный момент), мне пришлось компилировать из исходников, так как -dev пакеты в репозитории я не нашёл. Подготовительная часть закончена, можно идти копаться в исходники.
Начинаем с основы — ядра (папка libtransmission). Правим заголовок — transmission.h.
Добавляем поле в структуру описания метаданных торрента tr_info поле "char * rename". Эта структура содержит данные, полученные из .torrent. Собственно если таких данных нет, то и переименование не возможно, поэтому логично затесаться в эту структуру.Добавленное поле будет содержать имя торрента в файловой системе после переименования.
Кроме того добавляем описание нашей новой функции:
Самое время упомянуть об одной сложности — имена файлов строятся на основе оригинального имени.
Конечно можно всё пропатчить, но таких мест довольно много, и лучше просто после загрузки «перегрузить» эти имена.
Поэтому мы используем встроенный механизм восстановления информации — добавляем поле, которое содержит этот самый список путей, для сохранение, и при загрузке «вспоминаем» всё то, что нам нужно.
Пока отложим этот момент ненадолго, но не забудем о нём.
Перейдём к главному — самой функции перемещения. Редактировать будем torrent.h/torrent.c.
Сначала добавим в заголовочный файл функцию, которая и будет записывать перезаписывать пути для файлов во внутренней структуре торрента, эта функции будет нужна в том самом механизме:
Ну и в сам torrents.c её тело:
Всё примитивно — освобождаем старый путь, записываем новый. Такой небольшой хелпер.
Далее находим функцию fileExists() и после неё пишем основной код:
Большая часть кода это простые проверки, существенная часть это собственно сам вызов rename(), и правка info->files. Ну и не забываем заполнить info->rename.
Теперь нужно дать знать всем о том, что имя сменилось. Фактически это можно сделать прямыми правками. Несмотря на то что автор большей части кода пошёл по пути модификации tr_torrentName() я выбрал другой путь. Модификация той функции полезна только если собираетесь использовать gtk-клиент, тогда да, лучше заменить единственную строчку кода на:
Дабы всё было в ажуре для gtk, но так как я не использую это gui, то посчитал излишним портить кучу других вещей типа построения magnet-ссылки (оригинальный патч, само собой, портит). Фактически мне поле rename нужно только для построения пути к файлам, и для того чтобы отдавать по RPC, дабы RPC-клиент мог, например, открыть папку с торрентом. Первое у нас есть (пока что половинка), второе решается тоже несложно (переходим к правке имплементации RPC — rpcimpl.c).
Ищем функцию addField(), которая отвечает за формирование информационных полей торрента для ответа. То есть мы можем запросить некий набор полей о торренте, и с помощью этой функции Transmission сформирует данную информацию. Нас интересует поле "name", заменяем параметр "tr_torrentName( tor )" на
Готово. Теперь и RPC знает о нашем новом статусе.
Раз уж взялись править RPC, то нужно добавить собственно команду переименования.
Функция-прослойка, которую нужно вставить до torrentSet():
Теперь добавим саму команду, для этого и отредактируем функцию torrentSet():
Добавим в блок описания переменных — const char * str;
И проверку на команду rename:
Что же мы не сделали? А мы забыли о механизме сохранения состояния торрента!
Нужно восполнить этот пробел, этот модуль содержится в файлах (resume.c / resume.h).
Сначала добавим флажки сохраняемых полей. В заголовочном файле только одно перечисление, запутаться трудно.
Нам нужно будет сохранять информацию о настоящем местоположении торрента (inf->rename) и список файлов, о котором я говорил ранее.
Значит 2 флажка:
Несмотря на присутствие заголовочного файла, в resume.c есть список ключей-дефайнов (ключ описывает каждую сущность состояния сохраняющуюся на диск).
Туда нам тоже необходимо «вписаться»:
Оригинальное поле "name" не сохраняется, так как берется непосредственно из .torrent. Поэтому вполне можем в качестве ключа использовать этот идентификатор, и это не внесёт никакой путаницы в дальнейшем.
Добавим функции сохранения путей:
И добавим в интерфейсные функции наше пожелание о сохранении.
tr_torrentSaveResume(), сразу после проверки if( tr_torrentHasMetadata( tor )):
А код загрузки в loadFromFile():
Можно заметить что мы нигде не выставляем эти флажки (TR_FR_*), а только проверяем их, как же менеджер узнает о том что грузить нужно?
Ответ заключается в том, что модуль использует правило «всё разрешено, что не запрещено», то есть примерно так: flags &= ~deniedFieilds;
Булева логика любезно нам подсказывает то что наши 20 и 21 биты будут установлены в любом случае.
Фактически это всё. В указанном в начале топика патче есть код правки gtk и transmission-remote, с добавлением этой функции, но там всё тривиально ибо это фактически простые клиенты перенаправляющие запросы к серверу (непосредственно к libtransmission для gtk, и через rpc для -remote) с каплей бизнес-логики.
Так, с главной проблемой разобрались, теперь осталась вторая подзадача.
Я хочу видеть в клиенте не кучу «Season N» в качестве названий (а именно их передаст нам rpc-сервер, ибо как я объяснял в начале топика торренты у меня хранятся именно по такой схеме), а вполне осмысленные строки. Поэтому внесём совсем маленькую правку — просто добавим новое свойство "displayName" и набор геттеров/сеттеров в интерфейсе RPC.
Это очень маленькая и простая правка — нам нужно сделать всё тоже самое что и с полем rename, только без бизнес-логики и с модификацией rpc-отдачи.
По пунктам:
Когда меняем это поле, необходимо пометить то, что торрент — «грязный», то есть его состояние было изменено со времени последнего сохранения.
В общем-то всё. Можно компилировать.
Теперь transmission научилось выполнять данную задачу, однако вот клиенты не знают об этом.
Но это не беда. Модификаций нужно не так много, и я вполне успешно внёс исправления в Transmission GUI dotNet.
Не думаю что с остальными клиентами будет сложнее.
На своём домашнем серваке сменил систему, и собственно софт тоже нужно было переставлять.
Поэтому ради теста просмотрел несколько самых популярных торрент-клиентов, работающих на *nix (rTorrent, Deluge, MLDonkey, Transmission).
Последний понравился мне больше всего, однако для меня нашёлся существенный минус — невозможно переименовывать зашитые в .torrent-файл имена торрентов.
То есть у нас на диске будут всякие разные папки, например — «Krovavaja gora», «Место Преступления Нью-Йорк», а то и просто «7 Сезон».
Мне это не по нраву, я люблю порядок, соотвестсвенно свою фильмотеку (точнее её сериальную часть) организую в виде "%SERIAL_NAME%/Season N".
Transmission увы не позволяет такого. Но так как в основном всё было хорошо, я взялся подгонять клиент под себя.
Transmission. Rename
Первая подзадача — возможность переименования папки с содержимым торрента в файловой системе, и продолжение корректной работы.
Меня не первого посетила такая простая мысль, что это нужно для клиента. В багтрекинговой системе существует тикет трехгодичной давности #1220. К сожалению, разработчики как-то вяло реагируют на него, однако камрад juxda любезно написал патч, который добавляет данный функционал к сорцам.
Однако радиус кривизны моих рук не позволил корректно наложить патч даже на ту ревизию (11895) для которой он изготовлен. Кроме того хотелось всё же иметь наиболее свежую версию торрент-клиента с данной фишкой, ибо с той ревизии прошло ~500 коммитов.
Поэтому я избрал путь вдумчивого патчинга, с разбором того, что конкретно мы делаем, дабы можно было и самому подправить в случае чего, да и допилить клиенты к демону.
Начинается всё просто:
svn co svn://svn.transmissionbt.com/Transmission/trunk Transmission
Забираем HEAD-ревизию с SVN-сервера. Список нужных пакетов есть в траке, замечу что если не нужен gtk-клиент, то и часть либ можно не ставить (libgtk2.0-dev, libnotify-dev, libglib2.0-dev можно проигнорить). libevent-dev нужен свежий (2.0.10 на данный момент), мне пришлось компилировать из исходников, так как -dev пакеты в репозитории я не нашёл. Подготовительная часть закончена, можно идти копаться в исходники.
Начинаем с основы — ядра (папка libtransmission). Правим заголовок — transmission.h.
Добавляем поле в структуру описания метаданных торрента tr_info поле "char * rename". Эта структура содержит данные, полученные из .torrent. Собственно если таких данных нет, то и переименование не возможно, поэтому логично затесаться в эту структуру.Добавленное поле будет содержать имя торрента в файловой системе после переименования.
Кроме того добавляем описание нашей новой функции:
int tr_torrentRename( tr_torrent * torrent, const char * newname );
Самое время упомянуть об одной сложности — имена файлов строятся на основе оригинального имени.
Конечно можно всё пропатчить, но таких мест довольно много, и лучше просто после загрузки «перегрузить» эти имена.
Поэтому мы используем встроенный механизм восстановления информации — добавляем поле, которое содержит этот самый список путей, для сохранение, и при загрузке «вспоминаем» всё то, что нам нужно.
Пока отложим этот момент ненадолго, но не забудем о нём.
Перейдём к главному — самой функции перемещения. Редактировать будем torrent.h/torrent.c.
Сначала добавим в заголовочный файл функцию, которая и будет записывать перезаписывать пути для файлов во внутренней структуре торрента, эта функции будет нужна в том самом механизме:
void tr_torrentInitFileName( tr_torrent * tor,
tr_file_index_t fileIndex,
const char * name );
Ну и в сам torrents.c её тело:
void
tr_torrentInitFileName( tr_torrent * tor,
tr_file_index_t fileIndex,
const char * name )
{
tr_file * file;
assert( tr_isTorrent( tor ) );
assert( fileIndex < tor->info.fileCount );
assert( name != NULL );
assert( name[0] != '\0' );
file = &tor->info.files[fileIndex];
tr_free( file->name );
file->name = tr_strdup( name );
}
Всё примитивно — освобождаем старый путь, записываем новый. Такой небольшой хелпер.
Далее находим функцию fileExists() и после неё пишем основной код:
static bool
dirExists( const char * path )
{
struct stat sb;
return stat( path, &sb ) == 0 && S_ISDIR( sb.st_mode );
}
int
tr_torrentRename( tr_torrent * tor, const char * newname )
{
tr_info * info;
const char * root, * p, * oldname, * base;
char * oldpath = NULL, * newpath = NULL, * subpath = NULL;
int err = 0;
assert( tr_isTorrent( tor ) );
tr_torrentLock( tor );
if( !tr_torrentHasMetadata( tor ) )
{
err = ENOENT;
goto OUT;
}
if( !newname || !newname[0] || strchr( newname, TR_PATH_DELIMITER )
|| !strcmp( newname, "." ) || !strcmp( newname, ".." ) )
{
err = EINVAL;
goto OUT;
}
info = &tor->info;
if (info->rename)
oldname = info->rename;
else
oldname = info->name;
if( ( p = strchr( oldname, TR_PATH_DELIMITER ) ) )
{
/* Should not happen, but just in case. */
err = EISDIR;
goto OUT;
}
if( !strcmp( newname, oldname ) )
goto OUT;
root = tr_torrentGetCurrentDir( tor );
if( info->fileCount > 1 )
{
tr_file_index_t fi;
oldpath = tr_buildPath( root, oldname, NULL );
if( dirExists( oldpath ) )
{
newpath = tr_buildPath( root, newname, NULL );
if( fileExists( newpath, NULL) )
{
err = EEXIST;
goto OUT;
}
if( rename( oldpath, newpath ) == -1 )
{
err = errno;
goto OUT;
}
}
for( fi = 0; fi < info->fileCount; ++fi )
{
tr_file * file = &info->files[fi];
char * newfnam;
if( !( p = strchr( file->name, TR_PATH_DELIMITER ) ) )
continue;
newfnam = tr_buildPath( newname, p + 1, NULL );
tr_free( file->name );
file->name = newfnam;
}
}
else
{
if( tr_torrentFindFile2( tor, 0, &base, &subpath, NULL) )
{
oldpath = tr_buildPath( base, subpath, NULL );
newpath = tr_buildPath( base, newname, NULL );
if( fileExists( newpath, NULL) )
{
err = EEXIST;
goto OUT;
}
if( rename( oldpath, newpath ) == -1 )
{
err = errno;
goto OUT;
}
}
tr_free( info->files[0].name );
info->files[0].name = tr_strdup( newname );
}
tr_free( info->rename );
if( !strcmp( newname, info->name ) )
info->rename = NULL;
else
info->rename = tr_strdup( newname );
tr_torrentSetDirty( tor );
OUT:
if( err )
{
const char * es = tr_strerror( err ), * fmt;
if( oldpath && newpath )
{
/* %1$s is the original file path.
* %2$s is the new file path.
* %3$s is the error message. */
fmt = _( "Cannot rename \"%1$s\" to \"%2$s\": %3$s" );
tr_torerr( tor, fmt, oldpath, newpath, es );
}
else if( oldpath )
{
/* %1$s is the existing file name.
* %2$s is the error message. */
fmt = _( "Cannot rename \"%1$s\": %2$s" );
tr_torerr( tor, fmt, oldpath, es );
}
else
{
fmt = _( "Cannot rename torrent: %s" );
tr_torerr( tor, fmt, es );
}
}
tr_torrentUnlock( tor );
tr_free( oldpath );
tr_free( newpath );
tr_free( subpath );
return err;
}
Большая часть кода это простые проверки, существенная часть это собственно сам вызов rename(), и правка info->files. Ну и не забываем заполнить info->rename.
Теперь нужно дать знать всем о том, что имя сменилось. Фактически это можно сделать прямыми правками. Несмотря на то что автор большей части кода пошёл по пути модификации tr_torrentName() я выбрал другой путь. Модификация той функции полезна только если собираетесь использовать gtk-клиент, тогда да, лучше заменить единственную строчку кода на:
return tor->info.rename ? tor->info.rename : tor->info.name;
Дабы всё было в ажуре для gtk, но так как я не использую это gui, то посчитал излишним портить кучу других вещей типа построения magnet-ссылки (оригинальный патч, само собой, портит). Фактически мне поле rename нужно только для построения пути к файлам, и для того чтобы отдавать по RPC, дабы RPC-клиент мог, например, открыть папку с торрентом. Первое у нас есть (пока что половинка), второе решается тоже несложно (переходим к правке имплементации RPC — rpcimpl.c).
Ищем функцию addField(), которая отвечает за формирование информационных полей торрента для ответа. То есть мы можем запросить некий набор полей о торренте, и с помощью этой функции Transmission сформирует данную информацию. Нас интересует поле "name", заменяем параметр "tr_torrentName( tor )" на
tor->info.rename ? tor->info.rename : tor->info.name
Готово. Теперь и RPC знает о нашем новом статусе.
Раз уж взялись править RPC, то нужно добавить собственно команду переименования.
Функция-прослойка, которую нужно вставить до torrentSet():
static const char *
renameTorrent( tr_torrent * tor, const char * str )
{
int err = tr_torrentRename( tor, str );
return err == 0 ? NULL : tr_strerror( err );
}
Теперь добавим саму команду, для этого и отредактируем функцию torrentSet():
Добавим в блок описания переменных — const char * str;
И проверку на команду rename:
if( !errmsg && tr_bencDictFindStr( args_in, "rename", &str ) )
errmsg = renameTorrent( tor, str );
Что же мы не сделали? А мы забыли о механизме сохранения состояния торрента!
Нужно восполнить этот пробел, этот модуль содержится в файлах (resume.c / resume.h).
Сначала добавим флажки сохраняемых полей. В заголовочном файле только одно перечисление, запутаться трудно.
Нам нужно будет сохранять информацию о настоящем местоположении торрента (inf->rename) и список файлов, о котором я говорил ранее.
Значит 2 флажка:
TR_FR_FILE_NAMES = ( 1 << 20 ),
TR_FR_RENAME = ( 1 << 21 )
Несмотря на присутствие заголовочного файла, в resume.c есть список ключей-дефайнов (ключ описывает каждую сущность состояния сохраняющуюся на диск).
Туда нам тоже необходимо «вписаться»:
#define KEY_FILE_NAMES "name"
#define KEY_RENAME "rename"
Оригинальное поле "name" не сохраняется, так как берется непосредственно из .torrent. Поэтому вполне можем в качестве ключа использовать этот идентификатор, и это не внесёт никакой путаницы в дальнейшем.
Добавим функции сохранения путей:
static void
saveFileNames( tr_benc * dict, const tr_torrent * tor )
{
const tr_info * inf = tr_torrentInfo( tor );
const tr_file_index_t n = inf->fileCount;
tr_file_index_t i;
tr_benc * list;
list = tr_bencDictAddList( dict, KEY_FILE_NAMES, n );
for( i = 0; i < n; ++i )
tr_bencListAddStr( list, inf->files[i].name );
}
static uint64_t
loadFileNames( tr_benc * dict, tr_torrent * tor )
{
uint64_t ret = 0;
tr_info * inf = &tor->info;
const tr_file_index_t n = inf->fileCount;
tr_benc * list;
if( tr_bencDictFindList( dict, KEY_FILE_NAMES, &list )
&& tr_bencListSize( list ) == n )
{
const char * name;
tr_file_index_t i;
for( i = 0; i < n; ++i )
if( tr_bencGetStr( tr_bencListChild( list, i ), &name ) )
tr_torrentInitFileName( tor, i, name );
ret = TR_FR_FILE_NAMES;
}
return ret;
}
И добавим в интерфейсные функции наше пожелание о сохранении.
tr_torrentSaveResume(), сразу после проверки if( tr_torrentHasMetadata( tor )):
if( tor->info.rename )
tr_bencDictAddStr( &top, KEY_RENAME, tor->info.rename );
saveFileNames( &top, tor );
А код загрузки в loadFromFile():
if( fieldsToLoad & TR_FR_FILE_NAMES )
fieldsLoaded |= loadFileNames( &top, tor );
if( ( fieldsToLoad & TR_FR_RENAME )
&& tr_bencDictFindStr( &top, KEY_RENAME, &str )
&& str && str[0] )
{
tr_free( tor->info.rename );
tor->info.rename = tr_strdup( str );
}
Можно заметить что мы нигде не выставляем эти флажки (TR_FR_*), а только проверяем их, как же менеджер узнает о том что грузить нужно?
Ответ заключается в том, что модуль использует правило «всё разрешено, что не запрещено», то есть примерно так: flags &= ~deniedFieilds;
Булева логика любезно нам подсказывает то что наши 20 и 21 биты будут установлены в любом случае.
Фактически это всё. В указанном в начале топика патче есть код правки gtk и transmission-remote, с добавлением этой функции, но там всё тривиально ибо это фактически простые клиенты перенаправляющие запросы к серверу (непосредственно к libtransmission для gtk, и через rpc для -remote) с каплей бизнес-логики.
Transmission. Display Name.
Так, с главной проблемой разобрались, теперь осталась вторая подзадача.
Я хочу видеть в клиенте не кучу «Season N» в качестве названий (а именно их передаст нам rpc-сервер, ибо как я объяснял в начале топика торренты у меня хранятся именно по такой схеме), а вполне осмысленные строки. Поэтому внесём совсем маленькую правку — просто добавим новое свойство "displayName" и набор геттеров/сеттеров в интерфейсе RPC.
Это очень маленькая и простая правка — нам нужно сделать всё тоже самое что и с полем rename, только без бизнес-логики и с модификацией rpc-отдачи.
По пунктам:
- Добавляем поле в структуру tr_torrent в transmission.h
- В функцию torrentSet() добавляем простенькую модификацию tor->displayName используя tr_strdup() и команду "displayName", например.
- Добавляем новое поле в addField().
- В менеджер состояний так же вносим новый ключ "displayName" со всеми вытекающими.
Когда меняем это поле, необходимо пометить то, что торрент — «грязный», то есть его состояние было изменено со времени последнего сохранения.
В общем-то всё. Можно компилировать.
./autogen.sh --disable-gtk
make
make install prefix=/usr
Теперь transmission научилось выполнять данную задачу, однако вот клиенты не знают об этом.
Но это не беда. Модификаций нужно не так много, и я вполне успешно внёс исправления в Transmission GUI dotNet.
Не думаю что с остальными клиентами будет сложнее.