Pull to refresh

Transmission — внедряем в него плюшки

Reading time 8 min
Views 22K
Добрый день.
На своём домашнем серваке сменил систему, и собственно софт тоже нужно было переставлять.
Поэтому ради теста просмотрел несколько самых популярных торрент-клиентов, работающих на *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.
Не думаю что с остальными клиентами будет сложнее.

Ссылки


Tags:
Hubs:
+54
Comments 43
Comments Comments 43

Articles