Skip to content

Commit b9334b4

Browse files
committed
Add ability to load multiple urls and from local files
1 parent 0d25846 commit b9334b4

File tree

2 files changed

+140
-39
lines changed

2 files changed

+140
-39
lines changed

README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22
Script to automatically add trackers from a list to all torrents in Transmission.
33
This allows to get more peers to download/upload from/to.
44

5-
Logic:
6-
* Download & cache a list of trackers from a specified URL - currently tested only with [ngosang/trackerslist](https://github.com/ngosang/trackerslist). The cache will only be updated after the specified time passes (e.g. once a day)
7-
* Fetch the tracker list of each torrent in Transmission and add all trackers from a downloaded list to each of the torrents.
8-
* Optionally filter by torrent status (seeding, stopped etc)
9-
* If the tracker list of the torrent is already up-to-date then nothing is done.
5+
Features:
6+
* Download lists of trackers from any number of URLs and cache them locally. The cache will only be updated after the specified time passes (e.g. once a day)
7+
* Check if trackers obtained from remote URLs are resolvable in DNS
8+
* Load additional local tracker lists
9+
* Remove duplicates so that only unique hosts and IPs are used: if two or more URLs point to the same hostname or the hostname resolves to the same IP - only one URL will be loaded
10+
* Optionally filter torrents by status (seeding, stopped etc)
11+
* Compare the current tracker list of a torrent with the required one and only update Transmission if they don't match
1012

1113
Tracker list format:
1214
* One tracker URL per line
1315
* Empty lines are ignored
14-
* Only HTTP(S) and UDP trackers are loaded (Transmission does not support WebSocket trackers AFAIK)
16+
* Only `http[s]://` and `udp://` trackers are loaded (Transmission does not support WebSocket trackers AFAIK)
1517

1618
Requirements:
17-
* Should work with both Python 2 and 3, although there may be problems with logging in Python2 due to different unicode handling.
18-
* *transmissionrpc* Python module.
19+
* Should work with both Python 2.7 and 3.x, although there may be problems with logging in Python2 due to different unicode handling, I don't want to fix that :)
20+
* *transmissionrpc* Python module
1921

2022
Usage:
21-
* Install *transmissionrpc*: ```pip[3] install transmissionrpc```
23+
* Get *transmissionrpc*: ```pip[3] install transmissionrpc``` (or using any other method)
2224
* Put the *transmission-trackers.py* script somewhere
2325
* Make sure that the right Python interpreter is used (it's *python3* by default)
2426
* Adjust the host/port/credentials to access the Transmission RPC inside the script
27+
* Add your URLs and local files to the appropriate lists in the script
28+
* Adjust other parameters if needed (see comments)
2529
* Make the script run by cron e.g. every minute
2630
* You're done

transmission-trackers.py

Lines changed: 127 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#!/usr/bin/env python3
22

3-
import transmissionrpc, sys, os, time
4-
53
# Host, port, username and password to connect to Transmission
64
# Set user and pw to None if auth is not required
75
host, port, user, pw = 'localhost', 9091, 'admin', 'pwd'
@@ -14,63 +12,162 @@
1412
# How frequently to update trackers cache
1513
update_freq = 86400
1614

17-
# Path to trackers URL and local cache
18-
trackers_url = 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt'
19-
trackers_file = '/tmp/trackers_all.txt'
15+
# A list of URLs where to get the tracker lists from.
16+
# The lists are combined into one with duplicates removed.
17+
# The trackers from these lists are checked by looking up the URL's hostname in DNS.
18+
urls = [
19+
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt',
20+
# 'http://some.other.tracker.list/trackers.txt'
21+
# ...
22+
]
23+
24+
# Where to cache downloaded lists
25+
cache_file = '/tmp/trackers_cache.txt'
26+
27+
# Additional local lists of trackers to load.
28+
# Better to use absolute paths.
29+
# These are not checked against DNS
30+
local_lists = [
31+
# '/var/cache/trackers1.txt'
32+
# '/var/cache/trackers2.txt'
33+
# ...
34+
]
2035

2136
# Don't print anything (unless an error occures)
2237
silent = False
2338
# Debug output
2439
debug = False
2540

2641
###
27-
if silent: debug = False
28-
trackers = None
42+
hosts, ips = set(()), set(())
43+
44+
import transmissionrpc, sys, os, time, socket
45+
46+
if sys.version_info[0] == 2:
47+
from urllib import urlopen
48+
from urlparse import urlparse
49+
else:
50+
from urllib.request import urlopen
51+
from urllib.parse import urlparse
2952

30-
def readTrackers():
31-
f = open(trackers_file, 'r')
32-
trackers = set(())
53+
def lg(msg):
54+
if not silent: print(msg)
3355

34-
for t in f.readlines():
56+
def dbg(msg):
57+
if debug: lg(msg)
58+
59+
def parse(txt):
60+
l = []
61+
for t in txt.split('\n'):
3562
t = t.strip()
36-
if not t.startswith('http') or not t.startswith('udp'):
37-
continue
38-
trackers.add(t)
63+
if t.startswith('http') or t.startswith('udp'):
64+
l.append(t)
65+
return l
66+
67+
def validateTrackerURL(url, dns=True):
68+
try:
69+
h = urlparse(url).netloc.split(':', 1)[0]
70+
except:
71+
lg("Tracker URL '{}' is malformed".format(url))
72+
return False
73+
74+
if h in hosts:
75+
dbg("Host '{}' is duplicate".format(h))
76+
return False
77+
78+
if dns:
79+
try:
80+
ip = socket.gethostbyname(h)
81+
except:
82+
lg("Host '{}' is not resolvable".format(h))
83+
return False
84+
85+
if ip in ips:
86+
dbg("Host's '{}' IP '{}' is duplicate".format(h, ip))
87+
return False
88+
89+
ips.add(ip)
90+
91+
dbg("Approving tracker '{}'".format(url))
92+
hosts.add(h)
93+
return True
3994

95+
def loadFile(file):
96+
f = open(file, 'r')
97+
l = parse(f.read())
4098
f.close()
41-
if debug: print('{} trackers loaded from {}'.format(len(trackers), trackers_file))
42-
return trackers
99+
return l
43100

44-
def downloadTrackers():
101+
def loadURL(url):
102+
f = urlopen(url)
103+
l = parse(f.read().decode("utf-8"))
104+
f.close()
105+
return l
106+
107+
def downloadLists():
45108
update = False
46109

47110
try:
48-
mt = os.stat(trackers_file).st_mtime
111+
mt = os.stat(cache_file).st_mtime
49112
if time.time() - mt > update_freq:
50113
update = True
51114
except:
52115
update = True
53116

54117
if not update:
55-
return
118+
return None
56119

57-
if sys.version_info[0] == 2:
58-
import urllib
59-
urllib.urlretrieve(trackers_url, trackers_file)
60-
else:
61-
import urllib.request
62-
urllib.request.urlretrieve(trackers_url, trackers_file)
120+
trk = []
121+
for url in urls:
122+
l = loadURL(url)
123+
trk += l
124+
dbg("Remote URL '{}' loaded: {} trackers".format(url, len(l)))
125+
126+
valid = []
127+
for t in trk:
128+
if validateTrackerURL(t): valid.append(t)
129+
130+
f = open(cache_file, "w+")
131+
f.write('\n'.join(valid))
132+
f.close()
133+
134+
return valid
135+
136+
def readLocalLists():
137+
trk = []
138+
for f in local_lists:
139+
l = loadFile(f)
140+
trk += l
141+
dbg("Local list '{}' loaded: {} trackers".format(f, len(l)))
142+
143+
valid = []
144+
for t in trk:
145+
if validateTrackerURL(t, dns=False): valid.append(t)
146+
147+
return valid
148+
149+
trk_remote = downloadLists()
150+
if trk_remote:
151+
lg('Remote URLs downloaded: {} trackers'.format(len(trk_remote)))
152+
elif trk_remote is None:
153+
trk_remote = []
154+
local_lists.append(cache_file)
155+
156+
trk_local = readLocalLists()
157+
if trk_local:
158+
dbg('Local lists loaded: {} trackers'.format(len(trk_local)))
63159

64-
trackers = readTrackers()
65-
if not silent: print('Trackers list updated ({} loaded)'.format(len(trackers)))
160+
trackers = set(trk_remote + trk_local)
161+
dbg('Total trackers: {}'.format(len(trackers)))
66162

67-
downloadTrackers()
68-
if not trackers: trackers = readTrackers()
163+
if not trackers:
164+
lg("No trackers loaded, nothing to do")
165+
exit(1)
69166

70167
tc = transmissionrpc.Client(host, port=port, user=user, password=pw)
71168
torrents = tc.get_torrents()
72169

73-
if debug: print('{} torrents total'.format(len(torrents)))
170+
dbg('{} torrents total'.format(len(torrents)))
74171

75172
for t in torrents:
76173
if status_filter and not t.status in status_filter:

0 commit comments

Comments
 (0)