1 # Written by Boudewijn Schoon
2 # see LICENSE.txt for license information
4 The MagnetLink module handles the retrieval of the 'info' part of a
5 .torrent file given a magnet link.
7 Ideally we should use the regular BitTorrent connection classes to
8 make connection to peers, but all these classes assume that the
9 .torrent information is already available.
11 Hence, this module will make BitTorrent connection for the sole
12 purpose of retrieving the .torrent info part. After retrieval has
13 finished all connections are closed and a regular download will begin.
16 from binascii import unhexlify
17 from urlparse import urlsplit
18 from traceback import print_exc
19 from threading import Lock
22 # parse_sql requires python 2.6 or higher
23 from urlparse import parse_qsl
25 from urllib import unquote_plus
28 'foo=bar&moo=milk' --> [('foo', 'bar'), ('moo', 'milk')]
30 query = unquote_plus(query)
31 for part in query.split("&"):
33 yield part.split("=", 1)
35 from BaseLib.Core.DecentralizedTracking.kadtracker.identifier import Id, IdError
36 from BaseLib.Core.DecentralizedTracking.MagnetLink.MiniBitTorrent import MiniSwarm, MiniTracker
37 import BaseLib.Core.DecentralizedTracking.mainlineDHT as mainlineDHT
42 _singleton_lock = Lock()
45 def get_instance(cls, *args, **kargs):
46 if hasattr(cls, "_singleton_instance"):
47 return getattr(cls, "_singleton_instance")
50 cls._singleton_lock.acquire()
52 if not hasattr(cls, "_singleton_instance"):
53 setattr(cls, "_singleton_instance", cls(*args, **kargs))
54 return getattr(cls, "_singleton_instance")
57 cls._singleton_lock.release()
59 class MagnetHandler(Singleton):
60 def __init__(self, raw_server):
61 self._raw_server = raw_server
64 def get_raw_server(self):
65 return self._raw_server
67 def add_magnet(self, magnet_link):
68 self._magnets.append(magnet_link)
70 def remove_magnet(self, magnet_link):
71 self._magnets.remove(magnet_link)
73 def get_magnets(self):
77 def __init__(self, url, callback):
79 If the URL conforms to a magnet link, the .torrent info is
80 downloaded and returned to CALLBACK.
82 # _callback is called when the metadata is retrieved.
83 self._callback = callback
85 dn, xt, tr = self._parse_url(url)
87 # _name is the unicode name suggested for the swarm.
88 assert dn is None or isinstance(dn, unicode), "DN has invalid type: %s" % type(dn)
91 # _info_hash is the 20 byte binary info hash that identifies
93 assert isinstance(xt, str), "XT has invalid type: %s" % type(xt)
94 assert len(xt) == 20, "XT has invalid length: %d" % len(xt)
97 # _tracker is an optional tracker address.
100 # _swarm is a MiniBitTorrent.MiniSwarm instance that connects
101 # to peers to retrieve the metadata.
102 magnet_handler = MagnetHandler.get_instance()
103 magnet_handler.add_magnet(self)
104 self._swarm = MiniSwarm(self._info_hash, magnet_handler.get_raw_server(), self.metainfo_retrieved)
106 def get_infohash(self):
107 return self._info_hash
114 Start retrieving the metainfo
116 Returns True when attempting to obtain the metainfo, in this
117 case CALLBACK will always be called. Otherwise False is
118 returned, in this case CALLBACK will not be called.
121 # todo: catch the result from get_peers and call its stop
122 # method. note that this object does not yet contain a
124 dht = mainlineDHT.dht
125 dht.get_peers(Id(self._info_hash), self._swarm.add_potential_peers)
129 MiniTracker(self._swarm, self._tracker)
137 def metainfo_retrieved(self, metainfo, peers=[]):
139 Called when info part for metadata is retrieved. If we have
140 more metadata, we will add it at this point.
142 PEERS optionally contains a list of valid BitTorrent peers,
143 found during metadata download, to help bootstrap the
146 assert isinstance(metainfo, dict)
147 assert isinstance(peers, list)
149 for address in peers:
150 assert isinstance(address[0], str)
151 assert isinstance(address[1], int)
154 metadata = {"info":metainfo}
156 metadata["announce"] = self._tracker
158 metadata["nodes"] = []
160 metadata["initial peers"] = peers
162 self._callback(metadata)
166 magnet_handler = MagnetHandler.get_instance()
167 magnet_handler.remove_magnet(self)
169 # close all MiniBitTorrent activities
174 # url must be a magnet link
179 if DEBUG: print >> sys.stderr, "Magnet._parse_url()", url
181 schema, netloc, path, query, fragment = urlsplit(url)
182 if schema == "magnet":
183 # magnet url's do not conform to regular url syntax (they
184 # do not have a netloc.) This causes path to contain the
187 pre, post = path.split("?", 1)
189 query = "&".join((post, query))
193 for key, value in parse_qsl(query):
198 elif key == "xt" and value.startswith("urn:btih:"):
199 xt = unhexlify(value[9:49])
204 if DEBUG: print >> sys.stderr, "Magnet._parse_url() NAME:", dn
205 if DEBUG: print >> sys.stderr, "Magnet._parse_url() HASH:", xt
206 if DEBUG: print >> sys.stderr, "Magnet._parse_url() TRAC:", tr