На одном трекере я являюсь активным сидером. Но когда приходит время обновлять раздачи, для меня начинается ужас: некоторые раздачи имеют разные название в торрент-клиенте и на трекере, раздач с идентичным названием на трекере очень много, да и искать какую-то конкретную раздачу очень трудно. К тому же у меня нет столько времени, чтобы заниматься таким рутинным делом. Поэтому мне понадобился небольшой скрипт, который бы обновлял раздачи в клиенте, при обновлении оных на трекере.
Передо мной встала задача: найти какое-нибудь готовое решение или же попробовать написать нужный скрипт самому. На хабре встречались способы, в некотором роде выполняющие мою задачу, но способ либо мне не подходил, либо он мне не совсем нравился. При этом я никогда не писал программ или даже скриптов, поэтому вариант собственноручно написанного скрипта мне нравился ещё больше. Сначала надо было выбрать инструмент, язык, который был бы лёгок для освоения и погружения в программирование, и моё внимание привлёк python.
Python мне сразу понравился. Кажется, что он придаёт некую «легкость» в написании кода. Как первое чтиво по python'у я выбрал книгу Марка Лутца «Изучаем Python (4-е издание)». Что ж, инструмент есть, какая-никакая помощь в виде книги есть, поехали!
Итак, для начала нужно определить, что торрент-файл в нашем клиенте (в данном случае имеется в виду uTorrent 2.2) устарел и надо скачать новый. Первое, что я смог придумать, — парсинг страниц и сравнение с данными в торрент-файле. Такой способ работал, но у него был огромный минус в быстродействии: парсинг ста страниц, а именно такой лимит раздач на трекере, занимал около трёх минут. К тому же надо было все параметры раздачи сравнивать с результатом парсинга страницы, а это тоже отнимало немало времени. Такой метод работал без сбоев, но мне он не особо нравился, поэтому я продолжил поиски всевозможных решений поставленной задачи.
Вскоре, после долгих раздумий и поисков, я узнал о такой вещи как scrape. Scrape, как говорит википедия, — это дополнительный протокол запроса клиента к трекеру, при котором трекер сообщает клиенту общее количество сидов и пиров на раздаче. При помощи scrape-запроса можно легко узнать о том, существует ли раздача или нет. Также scrape-запрос клиентами посылается чаще, чем announce. Но надо знать, поддерживает ли конкретный трекер этот протокол или же нет. К моему счастью, мой трекер его поддерживает. Scrape-запрос посылается при пом��щи метода GET с заголовком и вот так выглядит адрес, по которому идёт запрос:
Хэш уникален для каждой раздачи, он включает в себя 20 символов и его можно достать из файла resume.dat. Но прежде, чем доставать информацию, надо знать, что этот файл, как файлы с расширением .torrent и settings.dat, представлены в формате bencode. Если нужно расшифровать файл быстро и без углублений в способ кодирования, то стоит скачать специальный пакет для питона здесь.
Приступим к расшифровке файла:
Теперь у нас на руках есть словарь с именами и хэшами раздач. Теперь нам остается только посылать scrape-запросы с подставленным и видоизменённым хэшем и проверять, есть ли раздача с таким хэшем на трекере или её уже нет. Также не стоит забывать о том, что делать такой запрос нужно как бы от лица клиента, иначе трекер откажет в доступе.
Обычный ответ на запрос выглядит так:
Если же раздача не существует, то ответ на запрос принимает вид:
Ответ на запрос тоже представлен в формате bencode, но расшифровывать нам его не надо, можно просто сравнить полученную строку со строкой, возвращаемой в случае отсутствия раздачи на трекере с таким хэшем.
Далее надо скачать наш файл с трекера, положить его в папку автозагрузки клиента и, по возможности, удалить запись об устаревшем торренте в самом клиенте.
С трекера просто так скачать файл не получится: нужна авторизация. Сама функция описана выше под заголовком «authentication». А далее мы авторизируемся, скачиваем файл, кладём его в папку автозагрузки и удаляем старый .torrent файл из папки с торрентами.
Чтобы уже несуществующий .torrent файл не путал нас своей записью в клиенте, его стоит удалить из клиента. Но uTorrent устроен так, что редактирование resume.dat, а именно там хранятся сведения о всех торрентах, при запущенном клиенте не даст результата: uTorrent восстановит resume.dat таким, каким он его запомнил при запуске. Поэтому для такого случая нужно постоянно выключать uTorrent, редактировать resume.dat, включать uTorrent. Такой метод подошёл бы для одной изменённой раздачи в день, а что если раздачи меняются пачками, т.е. по несколько сразу? Сначала я, будучи далёк от программирования в целом, думал о том, что придётся работать с процессами напрямую, а это очень сложно для меня. Но тут я узнал о существовании uTorrent WebUI. У WebUI есть API, документация к которому есть на официальном сайте. Благодаря возможностям API WebUI можно удалить запись, и не только удалить, о торренте из клиента. Сначала мы должны получить cookie, в которых есть специальный пароль, и token. Второе нам необходимо, если параметр webui.token_auth в клиенте активирован.
В uTorrent авторизация в web-интерфейсе реализована не так, как на сайте, поэтому простая отправка данных не пройдёт. Затем мы получаем токен и вместе с ним выполняем какую-нибудь функцию в клиенте. Конечно, можно было бы выделить класс под действия в клиенте, но я посчитал, что для этого хватит и обычной функции.
(Прим.: К сожалению, моих знаний на данный момент не хватило, чтобы правильно авторизироваться в web-интерфейсе, поэтому я воспользовался способом, описанном на просторах интернета.)
В итоге я получил скрипт, удовлетворяющий мою потребность, немного знаний и море удовольствия: очень весело до утра сидеть над кодом, а потом, когда ляжешь спать, понять, в чём была загвоздка.
Надеюсь, данный способ сможет кому-нибудь помочь.
UPD: Дико извиняюсь за свою невнимательность: приводил код в более читаемый вид перед публикацией, в результате чего и сам запутался, и вас запутал.
Код залил на Github. Работаю с ним впервые, так что, если я сделал что-то неправильно, обращайтесь.
Что же делать?
Передо мной встала задача: найти какое-нибудь готовое решение или же попробовать написать нужный скрипт самому. На хабре встречались способы, в некотором роде выполняющие мою задачу, но способ либо мне не подходил, либо он мне не совсем нравился. При этом я никогда не писал программ или даже скриптов, поэтому вариант собственноручно написанного скрипта мне нравился ещё больше. Сначала надо было выбрать инструмент, язык, который был бы лёгок для освоения и погружения в программирование, и моё внимание привлёк 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:incompletei10eeee20 символов «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. Работаю с ним впервые, так что, если я сделал что-то неправильно, обращайтесь.