instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / Core / DecentralizedTracking / MagnetLink / MagnetLink.py
1 # Written by Boudewijn Schoon
2 # see LICENSE.txt for license information
3 """
4 The MagnetLink module handles the retrieval of the 'info' part of a
5 .torrent file given a magnet link.
6
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.
10
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.
14 """
15 import sys
16 from binascii import unhexlify
17 from urlparse import urlsplit
18 from traceback import print_exc
19 from threading import Lock
20
21 try:
22     # parse_sql requires python 2.6 or higher
23     from urlparse import parse_qsl
24 except ImportError:
25     from urllib import unquote_plus
26     def parse_qsl(query):
27         """
28         'foo=bar&moo=milk' --> [('foo', 'bar'), ('moo', 'milk')]
29         """
30         query = unquote_plus(query)
31         for part in query.split("&"):
32             if "=" in part:
33                 yield part.split("=", 1)
34
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
38
39 DEBUG = False
40
41 class Singleton:
42     _singleton_lock = Lock()
43
44     @classmethod
45     def get_instance(cls, *args, **kargs):
46         if hasattr(cls, "_singleton_instance"):
47             return getattr(cls, "_singleton_instance")
48
49         else:
50             cls._singleton_lock.acquire()
51             try:
52                 if not hasattr(cls, "_singleton_instance"):
53                     setattr(cls, "_singleton_instance", cls(*args, **kargs))
54                 return getattr(cls, "_singleton_instance")
55                 
56             finally:
57                 cls._singleton_lock.release()
58
59 class MagnetHandler(Singleton):
60     def __init__(self, raw_server):
61         self._raw_server = raw_server
62         self._magnets = []
63
64     def get_raw_server(self):
65         return self._raw_server
66
67     def add_magnet(self, magnet_link):
68         self._magnets.append(magnet_link)
69
70     def remove_magnet(self, magnet_link):
71         self._magnets.remove(magnet_link)
72
73     def get_magnets(self):
74         return self._magnets
75
76 class MagnetLink:
77     def __init__(self, url, callback):
78         """
79         If the URL conforms to a magnet link, the .torrent info is
80         downloaded and returned to CALLBACK.
81         """
82         # _callback is called when the metadata is retrieved.
83         self._callback = callback
84
85         dn, xt, tr = self._parse_url(url)
86
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)
89         self._name = dn
90
91         # _info_hash is the 20 byte binary info hash that identifies
92         # the swarm.
93         assert isinstance(xt, str), "XT has invalid type: %s" % type(xt)
94         assert len(xt) == 20, "XT has invalid length: %d" % len(xt)
95         self._info_hash = xt
96
97         # _tracker is an optional tracker address.
98         self._tracker = tr
99
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)
105
106     def get_infohash(self):
107         return self._info_hash
108
109     def get_name(self):
110         return self._name
111
112     def retrieve(self):
113         """
114         Start retrieving the metainfo
115         
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.
119         """
120         if self._info_hash:
121             # todo: catch the result from get_peers and call its stop
122             # method.  note that this object does not yet contain a
123             # stop method...
124             dht = mainlineDHT.dht
125             dht.get_peers(Id(self._info_hash), self._swarm.add_potential_peers)
126
127             try:
128                 if self._tracker:
129                     MiniTracker(self._swarm, self._tracker)
130             except:
131                 print_exc()
132
133             return True
134         else:
135             return False
136
137     def metainfo_retrieved(self, metainfo, peers=[]):
138         """
139         Called when info part for metadata is retrieved.  If we have
140         more metadata, we will add it at this point.
141
142         PEERS optionally contains a list of valid BitTorrent peers,
143         found during metadata download, to help bootstrap the
144         download.
145         """
146         assert isinstance(metainfo, dict)
147         assert isinstance(peers, list)
148         if __debug__:
149             for address in peers:
150                 assert isinstance(address[0], str)
151                 assert isinstance(address[1], int)
152
153         # create metadata
154         metadata = {"info":metainfo}
155         if self._tracker:
156             metadata["announce"] = self._tracker
157         else:
158             metadata["nodes"] = []
159         if peers:
160             metadata["initial peers"] = peers
161
162         self._callback(metadata)
163         self.close()
164
165     def close(self):
166         magnet_handler = MagnetHandler.get_instance()
167         magnet_handler.remove_magnet(self)
168         
169         # close all MiniBitTorrent activities
170         self._swarm.close()
171
172     @staticmethod
173     def _parse_url(url):
174         # url must be a magnet link
175         dn = None
176         xt = None
177         tr = None
178
179         if DEBUG: print >> sys.stderr, "Magnet._parse_url()", url
180
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
185             # query part.
186             if "?" in path:
187                 pre, post = path.split("?", 1)
188                 if query:
189                     query = "&".join((post, query))
190                 else:
191                     query = post
192
193             for key, value in parse_qsl(query):
194                 if key == "dn":
195                     # convert to unicode
196                     dn = value.decode()
197
198                 elif key == "xt" and value.startswith("urn:btih:"):
199                     xt = unhexlify(value[9:49])
200
201                 elif key == "tr":
202                     tr = value
203
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
207
208         return (dn, xt, tr)