root/trunk/modules/modify_torrent.py @ 1

Revision 1, 7.4 KB (checked in by paranoidi, 3 years ago)

Initial

Line 
1__instance__ = "TorrentFilename"
2
3import yaml
4import re
5import logging
6import types
7
8# Torrent decoding is a short fragment from effbot.org. Site copyright says:
9# Test scripts and other short code fragments can be considered as being in the public domain.
10
11class Torrent:
12    """Represents a torrent"""
13
14    def __init__(self, content):
15        """Accepts torrent file as string"""
16       
17        # valid torrent files start with an announce block
18        if not content.startswith("d8:announce"):
19            raise Exception('Invalid content for a torrent')
20
21        self.encode_func = {}
22        self.encode_func[type(str())] = self.encode_string
23        self.encode_func[type(int())] = self.encode_integer
24        self.encode_func[type(long())] = self.encode_integer
25        self.encode_func[type(list())] = self.encode_list
26        self.encode_func[type(dict())] = self.encode_dictionary
27
28        # decoded torrent structure
29        self.content = self.decode(content)
30
31    def get_filelist(self):
32        """Return array containing fileinfo dictionaries (name, length, path)"""
33        files = []
34        # single file torrent
35        if self.content["info"].has_key("length"):
36            t = {}
37            t["name"] = self.content["info"]["name"]
38            t["size"] = self.content["info"]["length"]
39            t["path"] = ""
40            files.append(t)
41        else:
42            # multifile torrent
43            for item in self.content["info"]["files"]:
44                t = {}
45                import string
46                t['path'] = string.join(item['path'][:-1], '/')
47                t['name'] = item['path'][-1:]
48                t['size'] = item['length']
49                files.append(t)
50        return files
51
52    def get_multitrackers(self):
53        """
54        Return array containing all multi-trackers in this torrent.
55        Returns empty array if torrent has only standard single announce url.
56        """
57        trackers = []
58        # the spec says, if announce-list present use ONLY that
59        # funny iteration because of nesting, ie:
60        # [ [ tracker1, tracker2 ], [backup1] ]
61        for tl in self.content.get('announce-list', []):
62            for t in tl: trackers.append(t)
63        return trackers
64
65    def remove_multitracker(self, tracker):
66        """Removes passed multi-tracker from this torrent"""
67        for tl in self.content.get('announce-list', [])[:]:
68            try:
69                tl.remove(tracker)
70                # if no trackers left in list, remove whole list
71                if len(tl)==0:
72                    self.content['announce-list'].remove(tl)
73            except:
74                pass
75
76    def add_multitracker(self, tracker):
77        """Appends multi-tracker to this torrent"""
78        self.content.setdefault('announce-list', [])
79        self.content['announce-list'].append(tracker)
80
81    def __str__(self):
82        return 'Torrent instance. Files: %s' % self.get_filelist()
83       
84    def tokenize(self, text, match=re.compile("([idel])|(\d+):|(-?\d+)").match):
85        i = 0
86        while i < len(text):
87            m = match(text, i)
88            s = m.group(m.lastindex)
89            i = m.end()
90            if m.lastindex == 2:
91                yield "s"
92                yield text[i:i+int(s)]
93                i = i + int(s)
94            else:
95                yield s
96
97    def decode_item(self, next, token):
98        if token == "i":
99            # integer: "i" value "e"
100            data = int(next())
101            if next() != "e":
102                raise ValueError
103        elif token == "s":
104            # string: "s" value (virtual tokens)
105            data = next()
106        elif token == "l" or token == "d":
107            # container: "l" (or "d") values "e"
108            data = []
109            tok = next()
110            while tok != "e":
111                data.append(self.decode_item(next, tok))
112                tok = next()
113            if token == "d":
114                data = dict(zip(data[0::2], data[1::2]))
115        else:
116            raise ValueError
117        return data
118
119    def decode(self, text):
120        try:
121            src = self.tokenize(text)
122            data = self.decode_item(src.next, src.next())
123            for token in src: # look for more tokens
124                raise SyntaxError("trailing junk")
125        except (AttributeError, ValueError, StopIteration):
126            raise SyntaxError("syntax error")
127        return data
128
129    # encoding implementation by d0b
130   
131    def encode_string(self, data):
132        return "%d:%s" % (len(data), data)
133
134    def encode_integer(self, data):
135        return "i%de" % data
136
137    def encode_list(self, data):
138        encoded = "l"
139        for item in data:
140            encoded += self.encode_func[type(item)](item)
141        encoded += "e"
142        return encoded
143
144    def encode_dictionary(self, data):
145        encoded = "d"
146        items = data.items()
147        items.sort()
148        for (key, value) in items:
149            encoded += self.encode_string(key)
150            encoded += self.encode_func[type(value)](value)
151        encoded += "e"
152        return encoded
153
154    def encode(self):
155        data = self.content
156        return self.encode_func[type(data)](data)
157           
158
159class TorrentFilename:
160
161    """
162        Makes sure that entries containing torrent-file have .torrent
163        extension. This is enabled always by default (builtins).
164    """
165
166    def register(self, manager):
167        manager.register(instance=self, type='modify', keyword='torrent', callback=self.run, order=-200, builtin=True)
168
169    def run(self, feed):
170        for entry in feed.entries:
171            # create torrent object from torrent & match data
172            try:
173                torrent = Torrent(entry.get('data', ''))
174                entry['torrent'] = torrent
175                entry['filename'] = self.make_filename(torrent, entry)
176            except:
177                # not a torrent file, no need to mess with it
178                pass
179
180    def make_filename(self, torrent, entry):
181        """Build a filename for this torrent"""
182       
183        title = entry['title']
184        files = torrent.get_filelist()
185        if len(files) == 1 :
186            # single file, if filename is longer than title use it
187            import yaml
188            print yaml.dump(files)
189            fn = files[0]["name"]
190            if len(fn) > len(title):
191                title = fn[:fn.rfind('.')]
192        else:
193            # create a proper filename from crap we got from the feed
194            title = title.replace("/", "_")
195
196        # neatify further
197        title = title.replace(" ", "_")
198        title = title.encode('iso8859-1', 'ignore') # Damn \u200b -character, how I loathe thee
199
200        return title+".torrent"
201
202if __name__ == '__main__':
203    import sys
204    logging.basicConfig(level=logging.DEBUG)
205
206    from test_tools import MockFeed
207    feed = MockFeed()
208
209    f=open(sys.argv[1], 'r')
210    content = f.read()
211    f.close()
212
213    entry = {}
214    entry['url'] = 'http://127.0.0.1/mock'
215    entry['title'] = sys.argv[2]
216    entry['data'] = content
217
218    feed.entries.append(entry)
219
220    r = TorrentFilename()
221    r.run(feed)
222
223    trackers = entry['torrent'].get_multitrackers()
224
225    print trackers
226    entry['torrent'].remove_multitracker(trackers[0])
227    print entry['torrent'].get_multitrackers()
228
229    print yaml.safe_dump(entry['torrent'].content['announce-list'])
230   
231    feed.dump_entries()
232
Note: See TracBrowser for help on using the browser.