Pull to refresh

Ещё один способ обновления торрентов

Reading time7 min
Views28K
На одном трекере я являюсь активным сидером. Но когда приходит время обновлять раздачи, для меня начинается ужас: некоторые раздачи имеют разные название в торрент-клиенте и на трекере, раздач с идентичным названием на трекере очень много, да и искать какую-то конкретную раздачу очень трудно. К тому же у меня нет столько времени, чтобы заниматься таким рутинным делом. Поэтому мне понадобился небольшой скрипт, который бы обновлял раздачи в клиенте, при обновлении оных на трекере.


Что же делать?


Передо мной встала задача: найти какое-нибудь готовое решение или же попробовать написать нужный скрипт самому. На хабре встречались способы, в некотором роде выполняющие мою задачу, но способ либо мне не подходил, либо он мне не совсем нравился. При этом я никогда не писал программ или даже скриптов, поэтому вариант собственноручно написанного скрипта мне нравился ещё больше. Сначала надо было выбрать инструмент, язык, который был бы лёгок для освоения и погружения в программирование, и моё внимание привлёк python.

Python мне сразу понравился. Кажется, что он придаёт некую «легкость» в написании кода. Как первое чтиво по python'у я выбрал книгу Марка Лутца «Изучаем Python (4-е издание)». Что ж, инструмент есть, какая-никакая помощь в виде книги есть, поехали!

Постановка задачи и её решение


Итак, для начала нужно определить, что торрент-файл в нашем клиенте (в данном случае имеется в виду uTorrent 2.2) устарел и надо скачать новый. Первое, что я смог придумать, — парсинг страниц и сравнение с данными в торрент-файле. Такой способ работал, но у него был огромный минус в быстродействии: парсинг ста страниц, а именно такой лимит раздач на трекере, занимал около трёх минут. К тому же надо было все параметры раздачи сравнивать с результатом парсинга страницы, а это тоже отнимало немало времени. Такой метод работал без сбоев, но мне он не особо нравился, поэтому я продолжил поиски всевозможных решений поставленной задачи.

Вскоре, после долгих раздумий и поисков, я узнал о такой вещи как scrape. Scrape, как говорит википедия, — это дополнительный протокол запроса клиента к трекеру, при котором трекер сообщает клиенту общее количество сидов и пиров на раздаче. При помощи scrape-запроса можно легко узнать о том, существует ли раздача или нет. Также scrape-запрос клиентами посылается чаще, чем announce. Но надо знать, поддерживает ли конкретный трекер этот протокол или же нет. К моему счастью, мой трекер его поддерживает. Scrape-запрос посылается при помощи метода GET с заголовком и вот так выглядит адрес, по которому идёт запрос:
httр://example.com/scrape.php?info_hash=aaaaaaaaaaaaaaaaaaaa

Хэш уникален для каждой раздачи, он включает в себя 20 символов и его можно достать из файла resume.dat. Но прежде, чем доставать информацию, надо знать, что этот файл, как файлы с расширением .torrent и settings.dat, представлены в формате bencode. Если нужно расшифровать файл быстро и без углублений в способ кодирования, то стоит скачать специальный пакет для питона здесь.

Приступим к расшифровке файла:

# -*- coding: utf-8 -*-

import urllib2
from urllib import urlencode
from binascii import b2a_hex as bta, a2b_hex as atb
from os import remove
from shutil import move

from lxml.html import document_fromstring as doc
from bencode import bdecode, bencode
from httplib2 Http

http = Http()
username = 'username'
password = 'password'
ut_port = '12345' # Порт web-морды у uTorrent'а.
ut_username = 'utusername'
ut_password = 'utpassword'
site = 'http://example.com/'
scrape_body = site + 'scrape.php?info_hash='  # URL scrape-запроса.
login_url = site + 'takelogin.php'
torrent_body = site + 'download.php?id={0}&name={0}.torrent'
announce = site + 'announce.php?'  # URL анонса трекера.
webui_url = 'http://127.0.0.1:{0}/gui/'.format(ut_port)
webui_token = webui_url + 'token.html'

# Папка с .torrent файлами. Путь записан в settings.dat, пункт dir_torrent_files.
torrent_path = 'c:/utorrent/torrent/'
# Папка автозагрузки указывается в настройках клиента.
autoload_path = 'c:/utorrent/autoload/'
# Папка с системными файлами uTorrent'a (нужно для обработки resume.dat)
sys_torrent_path = 'c:/users/myname/appdata/utorrent/'

def authentication(username, password):
    data = {'username': username, 'password': password}
    headers = {'Content-type': 'application/x-www-form-urlencoded',
    'User-agent':'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.6'}
    resp, login = http.request(login_url, 'POST', headers=headers, body=urlencode(data))
    # Список имён атрибутов, подтверждающих авторизацию пользователя
    cookiekeys = ['uid', 'pass', 'PHPSESSID', 'pass_hash', 'session_id']
    split_resp = resp['set-cookie'].split(' ')
    lst = []
    # Далее оставляем только нужные нам атрибуты из ранее полученной строки.
    for split_res in split_resp:
        if split_res.split('=')[0] in cookiekeys:
                lst.append(split_res)
    cookie = ' '.join(lst)
    return {'Cookie': cookie}

def torrentDict(torr_path): #torr_path в нашем случае - папка с resume.dat .
    Dict = {}
    with open(u'{0}resume.dat'.format(torr_path), 'rb') as resume:
        t = bdecode(resume.read())
    for name in t:
        if name != '.fileguard' and name != 'rec':
            for tracker in t[name]['trackers']:
                if isinstance(tracker, str) and tracker.startswith(announce):
                    Dict[name.split('\\')[-1]] = bta(t[name]['info'])
    return Dict

Теперь у нас на руках есть словарь с именами и хэшами раздач. Теперь нам остается только посылать scrape-запросы с подставленным и видоизменённым хэшем и проверять, есть ли раздача с таким хэшем на трекере или её уже нет. Также не стоит забывать о том, что делать такой запрос нужно как бы от лица клиента, иначе трекер откажет в доступе.

uthead = {'User-Agent':'uTorrent/2210(21304)'}  # Имитируем заголовки uTorrent'а.
main_dict = torrentDict(sys_torrent_path)
for key in main_dict:
    lst = []
    for i in range(0, len(main_dict[key]), 2):
        lst.append('%{0}'.format(main_dict[key][i:i+2].upper()))
    scrp_str = ''.join(lst)  # Строка, содержащая видоизменённый хэш для запроса.
    resp, scrp = http.request('{0}{1}'.format(scrape_body, scrp_str), 'GET', headers=uthead)


Обычный ответ на запрос выглядит так:
d5:filesd20:aaaaaaaaaaaaaaaaaaaad8:completei5e10:downloadedi50e10:incompletei10eeee
20 символов «a» — это хэш раздачи, 5 — сидеров, 10 — личеров и 50 закончивших качать.
Если же раздача не существует, то ответ на запрос принимает вид:
d5:filesdee

Ответ на запрос тоже представлен в формате bencode, но расшифровывать нам его не надо, можно просто сравнить полученную строку со строкой, возвращаемой в случае отсутствия раздачи на трекере с таким хэшем.
Далее надо скачать наш файл с трекера, положить его в папку автозагрузки клиента и, по возможности, удалить запись об устаревшем торренте в самом клиенте.
С трекера просто так скачать файл не получится: нужна авторизация. Сама функция описана выше под заголовком «authentication». А далее мы авторизируемся, скачиваем файл, кладём его в папку автозагрузки и удаляем старый .torrent файл из папки с торрентами.

    # Этот код находится по иерархии ниже строчки "for key in Dict:".
    with open('{0}{1}'.format(torrent_path, key), 'rb') as torrent_file:
        torrent = bdecode(torrent_file.read())
        t_id = torrent['comment'][36:]  # Здесь мы получаем уникальный номер раздачи на трекере.
    brhead = authentication(username, password)
    resp, torrent = http.request(torrent_body.format(t_id), headers=brhead)
    with open('{0}.torrent'.format(t_id),'wb') as torrent_file:
        torrent_file.write(torrent)
    # Удаляем старый .torrent файл и добавляем новый в папку автозагрузки.
    remove('{0}{1}'.format(torrent_path, key))
    move('{0}.torrent'.format(t_id), '{0}{1}.torrent'.format(autoload_path, t_id))

    # Код удаления записи о торренте. О нём ниже.
    authkey, token = uTWebUI(ut_username, ut_password)
    webuiActions(main_dict[key], 'remove', authkey, token)


Чтобы уже несуществующий .torrent файл не путал нас своей записью в клиенте, его стоит удалить из клиента. Но uTorrent устроен так, что редактирование resume.dat, а именно там хранятся сведения о всех торрентах, при запущенном клиенте не даст результата: uTorrent восстановит resume.dat таким, каким он его запомнил при запуске. Поэтому для такого случая нужно постоянно выключать uTorrent, редактировать resume.dat, включать uTorrent. Такой метод подошёл бы для одной изменённой раздачи в день, а что если раздачи меняются пачками, т.е. по несколько сразу? Сначала я, будучи далёк от программирования в целом, думал о том, что придётся работать с процессами напрямую, а это очень сложно для меня. Но тут я узнал о существовании uTorrent WebUI. У WebUI есть API, документация к которому есть на официальном сайте. Благодаря возможностям API WebUI можно удалить запись, и не только удалить, о торренте из клиента. Сначала мы должны получить cookie, в которых есть специальный пароль, и token. Второе нам необходимо, если параметр webui.token_auth в клиенте активирован.

def uTWebUI(ut_name, ut_passw):
    # Получаем cookie и token.
    passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
    passmgr.add_password(None, webui_token, ut_name, ut_passw)
    authhandler = urllib2.HTTPBasicAuthHandler(passmgr)
    opener = urllib2.build_opener(authhandler)
    urllib2.install_opener(opener)
    req = urllib2.Request(webui_token)

    tkp = urllib2.urlopen(req)
    page = tkp.read()
    token = doc(page).xpath('//text()')[0]
    passw = req.unredirected_hdrs['Authorization']
    return passw, token

def webuiActions(torrent_hash, action, password, token):
    head = {'Authorization': password}
    if action == 'remove':
        # Удаляем запись в клиенте об устаревшей раздаче.
        action_req = '?token={0}&action=remove&hash={1}'.format(token, torrent_hash)
        r, act = http.request(webui_url+action_req, headers=head)


В uTorrent авторизация в web-интерфейсе реализована не так, как на сайте, поэтому простая отправка данных не пройдёт. Затем мы получаем токен и вместе с ним выполняем какую-нибудь функцию в клиенте. Конечно, можно было бы выделить класс под действия в клиенте, но я посчитал, что для этого хватит и обычной функции.
(Прим.: К сожалению, моих знаний на данный момент не хватило, чтобы правильно авторизироваться в web-интерфейсе, поэтому я воспользовался способом, описанном на просторах интернета.)

Что в итоге


В итоге я получил скрипт, удовлетворяющий мою потребность, немного знаний и море удовольствия: очень весело до утра сидеть над кодом, а потом, когда ляжешь спать, понять, в чём была загвоздка.

Надеюсь, данный способ сможет кому-нибудь помочь.

UPD: Дико извиняюсь за свою невнимательность: приводил код в более читаемый вид перед публикацией, в результате чего и сам запутался, и вас запутал.
Код залил на Github. Работаю с ним впервые, так что, если я сделал что-то неправильно, обращайтесь.
Tags:
Hubs:
+36
Comments22

Articles