forked from wiserain/torrent_info
/
logic.py
357 lines (306 loc) · 13.6 KB
/
logic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# -*- coding: utf-8 -*-
#########################################################
# python
import os
import sys
import traceback
import subprocess
import json
import time
import random
from datetime import datetime
import platform
# third-party
import requests
from sqlitedict import SqliteDict
# sjva 공용
from framework import db, scheduler, app
from framework.util import Util
# 패키지
from .plugin import logger, package_name
from .model import ModelSetting, db_file
sys.path.append('/usr/lib/python2.7/site-packages')
#########################################################
class Logic(object):
# 디폴트 세팅값
db_default = {
'use_dht': 'True',
'scrape': 'False',
'force_dht': 'False',
'timeout': '10',
'trackers': '',
'n_try': '3',
'tracker_last_update': '1970-01-01',
'tracker_update_every': '30',
'libtorrent_build': '191217',
}
torrent_cache = None
@staticmethod
def db_init():
try:
for key, value in Logic.db_default.items():
if db.session.query(ModelSetting).filter_by(key=key).count() == 0:
db.session.add(ModelSetting(key, value))
db.session.commit()
except Exception as e:
logger.error('Exception:%s', e)
logger.error(traceback.format_exc())
@staticmethod
def plugin_load():
try:
# DB 초기화
Logic.db_init()
# 편의를 위해 json 파일 생성
from plugin import plugin_info
Util.save_from_dict_to_json(plugin_info, os.path.join(os.path.dirname(__file__), 'info.json'))
#
# 자동시작 옵션이 있으면 보통 여기서
#
# 토렌트 캐쉬 초기화
Logic.cache_init()
# libtorrent 자동 설치
new_build = int(plugin_info['install'].split('-')[-1].split('.')[0])
installed_build = ModelSetting.get_int('libtorrent_build')
if (new_build > installed_build) or (not Logic.is_installed()):
Logic.install()
# tracker 자동 업데이트
tracker_update_every = ModelSetting.get_int('tracker_update_every')
tracker_last_update = ModelSetting.get('tracker_last_update')
if tracker_update_every > 0:
if (datetime.now() - datetime.strptime(tracker_last_update, '%Y-%m-%d')).days >= tracker_update_every:
Logic.update_tracker()
except Exception as e:
logger.error('Exception:%s', e)
logger.error(traceback.format_exc())
@staticmethod
def plugin_unload():
try:
logger.debug('%s plugin_unload', package_name)
except Exception as e:
logger.error('Exception:%s', e)
logger.error(traceback.format_exc())
@staticmethod
def setting_save(req):
try:
for key, value in req.form.items():
logger.debug('Key:%s Value:%s', key, value)
entity = db.session.query(ModelSetting).filter_by(key=key).with_for_update().first()
entity.value = value
db.session.commit()
return True
except Exception as e:
logger.error('Exception:%s', e)
logger.error(traceback.format_exc())
return False
# 기본 구조 End
##################################################################
@staticmethod
def cache_init():
Logic.torrent_cache = SqliteDict(
db_file, tablename='plugin_{}_cache'.format(package_name), encode=json.dumps, decode=json.loads, autocommit=True
)
@staticmethod
def tracker_save(req):
for key, value in req.form.items():
logger.debug({'key': key, 'value': value})
if key == 'trackers':
value = json.dumps(value.split('\n'))
logger.debug('Key:%s Value:%s', key, value)
entity = db.session.query(ModelSetting).filter_by(key=key).with_for_update().first()
entity.value = value
db.session.commit()
@staticmethod
def update_tracker():
# https://github.com/ngosang/trackerslist
trackers_url_from = 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt'
new_trackers = requests.get(trackers_url_from).content.decode('utf8').split('\n\n')[:-1]
ModelSetting.set('trackers', json.dumps(new_trackers))
ModelSetting.set('tracker_last_update', datetime.now().strftime('%Y-%m-%d'))
@staticmethod
def is_installed():
try:
import libtorrent as lt
return lt.version
except ImportError:
return False
@staticmethod
def install():
try:
from plugin import plugin_info
# platform check - whitelist
if platform.machine() == 'x86_64' and app.config['config']['running_type'] == 'docker':
tarball = os.path.join(os.path.dirname(__file__), 'install', plugin_info['install'])
# file existence check
if not os.path.isfile(tarball):
return {'success': False, 'log': '파일이 없습니다. {}'.format(os.path.basename(tarball))}
returncode = subprocess.check_call(['tar', '-zxf', tarball, '-C', '/'])
# tar command check
if returncode != 0:
return {'success': False, 'log': '설치 중 에러 발생 exitcode: {}'.format(returncode)}
# finally check libtorrent imported
lt_ver = Logic.is_installed()
if lt_ver:
# 현재 설치된 빌드 번호 업데이트
ModelSetting.set('libtorrent_build', plugin_info['install'].split('-')[-1].split('.')[0])
return {'success': True, 'log': 'libtorrent v{}'.format(lt_ver)}
else:
return {'success': False, 'log': '설치 후 알수없는 에러. 개발자에게 보고바람'}
else:
return {'succes': False, 'log': '지원하지 않는 시스템입니다.'}
except Exception as e:
logger.error('Exception:%s', e)
logger.error(traceback.format_exc())
return {'success': False, 'log': str(e)}
@staticmethod
def size_fmt(num, suffix='B'):
# Windows에서 쓰는 단위로 가자 https://superuser.com/a/938259
for unit in ['','K','M','G','T','P','E','Z']:
if abs(num) < 1000.0:
return "%3.1f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f %s%s" % (num, 'Y', suffix)
@staticmethod
def convert_torrent_info(torrent_info):
"""from libtorrent torrent_info to python dictionary object"""
try:
import libtorrent as lt
except ImportError:
raise ImportError('libtorrent package required')
return {
'name': torrent_info.name(),
'num_files': torrent_info.num_files(),
'total_size': torrent_info.total_size(), # in byte
'total_size_fmt': Logic.size_fmt(torrent_info.total_size()), # in byte
'info_hash': str(torrent_info.info_hash()), # original type: libtorrent.sha1_hash
'num_pieces': torrent_info.num_pieces(),
'creator': torrent_info.creator() if torrent_info.creator() else 'libtorrent v{}'.format(lt.version),
'comment': torrent_info.comment(),
'files': [{'path': file.path, 'size': file.size, 'size_fmt': Logic.size_fmt(file.size)} for file in torrent_info.files()],
'magnet_uri': lt.make_magnet_uri(torrent_info),
}
@staticmethod
def parse_magnet_uri(magnet_uri, scrape=False, use_dht=True, force_dht=False, timeout=10, trackers=None,
no_cache=False, n_try=3):
try:
import libtorrent as lt
except ImportError:
raise ImportError('libtorrent package required')
# parameters
params = lt.parse_magnet_uri(magnet_uri)
# prevent downloading
# https://stackoverflow.com/q/45680113
if isinstance(params, dict):
params['flags'] |= lt.add_torrent_params_flags_t.flag_upload_mode
else:
params.flags |= lt.add_torrent_params_flags_t.flag_upload_mode
lt_version = [int(v) for v in lt.version.split('.')]
if [0, 16, 13, 0] < lt_version < [1, 1, 3, 0]:
# for some reason the info_hash needs to be bytes but it's a struct called sha1_hash
if type({}) == type(params):
params['info_hash'] = params['info_hash'].to_bytes()
else:
params.info_hash = params.info_hash.to_bytes()
# 캐시에 있으면 ...
info_hash_from_magnet = str(params['info_hash'] if type({}) == type(params) else params.info_hash)
if (not no_cache) and (info_hash_from_magnet in Logic.torrent_cache):
return Logic.torrent_cache[info_hash_from_magnet]['info']
# add trackers
if type({}) == type(params):
if len(params['trackers']) == 0:
if trackers is None:
trackers = json.loads(ModelSetting.get('trackers'))
params['trackers'] = random.sample(trackers, 5)
else:
if len(params.trackers) == 0:
if trackers is None:
trackers = json.loads(ModelSetting.get('trackers'))
params.trackers = random.sample(trackers, 5)
# session
session = lt.session()
session.listen_on(6881, 6891)
session.add_extension('ut_metadata')
session.add_extension('ut_pex')
session.add_extension('metadata_transfer')
if use_dht:
session.add_dht_router('router.utorrent.com', 6881)
session.add_dht_router('router.bittorrent.com', 6881)
session.add_dht_router("dht.transmissionbt.com", 6881)
session.add_dht_router('127.0.0.1', 6881)
session.start_dht()
# handle
handle = session.add_torrent(params)
if force_dht:
handle.force_dht_announce()
for tryid in range(max(n_try,1)):
timeout_value = timeout
while not handle.has_metadata():
time.sleep(0.1)
timeout_value -= 0.1
if timeout_value <= 0:
logger.debug('Failed to get metadata on trial: {}/{}'.format(tryid+1, n_try))
break
if handle.has_metadata():
lt_info = handle.get_torrent_info()
logger.debug('Successfully get metadata after {} seconds on trial {}'.format(timeout - timeout_value, tryid+1))
break
else:
if tryid+1 == max(n_try,1):
session.remove_torrent(handle, True)
raise Exception('Timed out after {}x{} seconds trying to get metainfo'.format(timeout, n_try))
# create torrent object and generate file stream
torrent = lt.create_torrent(lt_info)
torrent.set_creator('libtorrent v{}'.format(lt.version)) # signature
torrent_dict = torrent.generate()
torrent_info = Logic.convert_torrent_info(lt_info)
torrent_info.update({
'trackers': params.trackers if type({}) != type(params) else params['trackers'],
'creation_date': datetime.fromtimestamp(torrent_dict[b'creation date']).isoformat(),
'time': {'total': timeout - timeout_value, 'metadata': timeout - timeout_value},
})
if scrape:
# start scraping
timeout_value = timeout
while handle.status(0).num_complete < 0:
time.sleep(0.1)
timeout_value -= 0.1
if timeout_value <= 0:
logger.error('Timed out after {} seconds trying to get peer info'.format(timeout))
if handle.status(0).num_complete >= 0:
torrent_status = handle.status(0)
torrent_info.update({
'seeders': torrent_status.num_complete,
'peers': torrent_status.num_incomplete,
})
torrent_info['time']['scrape'] = timeout - timeout_value
torrent_info['time']['total'] = torrent_info['time']['metadata'] + torrent_info['time']['scrape']
session.remove_torrent(handle, True)
# caching for later use
if Logic.torrent_cache is None:
Logic.cache_init()
Logic.torrent_cache[torrent_info['info_hash']] = {
'info': torrent_info,
}
return torrent_info
@staticmethod
def parse_torrent_file(torrent_file):
# torrent_file >> torrent_dict >> lt_info >> torrent_info
try:
import libtorrent as lt
except ImportError:
raise ImportError('libtorrent package required')
torrent_dict = lt.bdecode(torrent_file)
lt_info = lt.torrent_info(torrent_dict)
torrent_info = Logic.convert_torrent_info(lt_info)
if b'announce-list' in torrent_dict:
torrent_info.update({'trackers': [x.decode('utf-8') for x in torrent_dict[b'announce-list'][0]]})
torrent_info.update({'creation_date': datetime.fromtimestamp(torrent_dict[b'creation date']).isoformat()})
# caching for later use
if Logic.torrent_cache is None:
Logic.cache_init()
Logic.torrent_cache[torrent_info['info_hash']] = {
'info': torrent_info,
}
return torrent_info
@staticmethod
def parse_torrent_url(url):
return Logic.parse_torrent_file(requests.get(url).content)