Use comment.utf-8 if available
[extfs.d.git] / torrent
1 #! /usr/bin/env python
2 """Torrent Virtual FileSystem for Midnight Commander
3
4 The script requires Midnight Commander 3.1+
5 (http://www.midnight-commander.org/), Python 2.4+ (http://www.python.org/),
6 module eff_bdecode.py (http://effbot.org/zone/bencode.htm).
7
8 For mc 4.7+ put the script in $HOME/.mc/extfs.d.
9 For older versions put it in /usr/[local/][lib|share]/mc/extfs
10 and add a line "torrent" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini.
11 Make the script executable.
12
13 Run this "cd" command in the Midnight Commander (in the "bindings" file the
14 command is "%cd"): cd file#torrent, where "file" is the name of your torrent
15 metafile. The VFS lists all files and directories from the torrent metafile;
16 all files appear empty, of course, but the sizes are shown. Filenames are
17 reencoded from the metafile's encoding/codepage to the current locale.
18
19 Along with the files/directories in the torrent metafile the VFS also presents
20 meta information - in the form of files in .META directory. The size and
21 contents of these files are taken from the corresponding fields in the torrent
22 metafile. The script doesn't check if the torrent consists of a .META file or
23 directory (quite unlikely).
24
25 Date/time for all files is set to midnight of the 1st January of the current
26 year. The filesystem is, naturally, read-only.
27
28 """
29
30 __version__ = "1.1.0"
31 __author__ = "Oleg Broytman <phd@phdru.name>"
32 __copyright__ = "Copyright (C) 2010-2013 PhiloSoft Design"
33 __license__ = "GPL"
34
35 import locale, sys, os
36 from eff_bdecode import decode
37
38 import logging
39 logger = logging.getLogger('torrent-mcextfs')
40 log_err_handler = logging.StreamHandler(sys.stderr)
41 logger.addHandler(log_err_handler)
42 logger.setLevel(logging.INFO)
43
44 if len(sys.argv) < 3:
45     logger.critical("""\
46 Torrent Virtual FileSystem for Midnight Commander version %s
47 Author: %s
48 %s
49
50 This is not a program. Put the script in /usr/[local/][lib|share]/mc/extfs.
51 For more information read the source!""",
52    __version__, __author__, __copyright__
53 )
54     sys.exit(1)
55
56
57 locale.setlocale(locale.LC_ALL, '')
58 charset = locale.getpreferredencoding()
59
60
61 def mctorrent_list():
62     """List the entire VFS"""
63
64     if 'info' not in torrent:
65         torrent_error('Info absent')
66
67     info = torrent['info']
68     if 'name' not in info and 'name.utf-8' not in info:
69         torrent_error('Unknown name')
70
71     codepage = torrent.get('codepage', None)
72     encoding = torrent.get('encoding', None)
73     if not encoding and codepage:
74         encoding = str(codepage)
75
76     name = info['name']
77     name_utf8 = info.get('name.utf-8', None)
78
79     if 'files' in info:
80         files = info['files']
81         paths = []
82         for file in files:
83             if 'path' not in file and 'path.utf-8' not in file:
84                 torrent_error('Unknown path')
85             if 'length' not in file:
86                 torrent_error('Unknown length')
87             if 'path.utf-8' in file:
88                 if name_utf8:
89                     path = '/'.join([name_utf8] + file['path.utf-8'])
90                     if charset and (charset != 'utf-8'):
91                         path = path.decode('utf-8', 'replace').encode(charset, 'replace')
92                 else:
93                     _name_utf8 = name
94                     if encoding and (encoding != 'utf-8'):
95                         _name_utf8 = _name_utf8.decode(encoding, 'replace').encode('utf-8', 'replace')
96                     path = '/'.join([_name_utf8] + file['path.utf-8'])
97                     if charset and (charset != 'utf-8'):
98                         path = path.decode('utf-8', 'replace').encode(charset, 'replace')
99             else:
100                 if name_utf8:
101                     path = file['path']
102                     if encoding and (encoding != 'utf-8'):
103                         path = path.decode(encoding, 'replace').encode('utf-8', 'replace')
104                     path = '/'.join([name_utf8] + path)
105                     if charset and (charset != 'utf-8'):
106                         path = path.decode('utf-8', 'replace').encode(charset, 'replace')
107                 else:
108                     path = '/'.join([name] + file['path'])
109                     if charset and encoding and (charset != encoding):
110                         path = path.decode(encoding, 'replace').encode(charset, 'replace')
111             length = file['length']
112             paths.append((path, length))
113     else: # One-file torrent
114         if 'length' not in info:
115             torrent_error('Unknown length')
116         length = info['length']
117         if name_utf8:
118             if charset and (charset != 'utf-8'):
119                 name = name_utf8.decode('utf-8', 'replace').encode(charset, 'replace')
120         elif charset and encoding and (charset != encoding):
121             name = name.decode(encoding, 'replace').encode(charset, 'replace')
122         paths = [(name, length)]
123
124     meta = []
125     for name in 'announce', 'announce-list', 'codepage', 'comment', \
126                 'created by', 'creation date', 'encoding':
127         if name == 'comment' and 'comment.utf-8' in torrent:
128             data = torrent['comment.utf-8'].decode('utf-8').encode(charset, 'replace')
129             meta.append(('.META/' + name, len(data)))
130         elif name in torrent:
131             if name == 'announce-list':
132                 data = decode_announce_list(torrent[name])
133             elif name == 'codepage':
134                 data = str(torrent[name])
135             elif name == 'creation date':
136                 data = decode_datetime(torrent[name])
137             else:
138                 data = torrent[name]
139             meta.append(('.META/' + name, len(data)))
140
141     if 'private' in info:
142         meta.append(('.META/private', 1))
143
144     if 'piece length' in info:
145         meta.append(('.META/piece length', len(str(info['piece length']))))
146
147     for name, size in paths + meta:
148         print "-r--r--r-- 1 user group %d Jan 1 00:00 %s" % (size, name)
149
150
151 def mctorrent_copyout():
152     """Extract a file from the VFS"""
153
154     torrent_filename = sys.argv[3]
155     real_filename = sys.argv[4]
156     data = None
157
158     for name in 'announce', 'announce-list', 'codepage', 'comment', \
159                 'created by', 'creation date', 'encoding':
160         if name == 'comment' and 'comment.utf-8' in torrent:
161             data = torrent['comment.utf-8'].decode('utf-8').encode(charset, 'replace')
162             meta.append(('.META/' + name, len(data)))
163         elif torrent_filename == '.META/' + name:
164             if name in torrent:
165                 if name == 'announce-list':
166                     data = decode_announce_list(torrent[name])
167                 elif name == 'codepage':
168                     data = str(torrent[name])
169                 elif name == 'creation date':
170                     data = decode_datetime(torrent[name])
171                 else:
172                     data = str(torrent[name])
173             else:
174                 torrent_error('Unknown ' + name)
175             break
176
177     if torrent_filename in ('.META/private', '.META/piece length'):
178         if 'info' not in torrent:
179             torrent_error('Info absent')
180         info = torrent['info']
181         if torrent_filename == '.META/private':
182             if 'private' not in info:
183                 torrent_error('Info absent')
184         if torrent_filename == '.META/piece length':
185             if 'piece length' not in info:
186                 torrent_error('Info absent')
187         data = str(info[torrent_filename[len('.META/'):]])
188
189     if not torrent_filename.startswith('.META/'):
190         data = ''
191
192     if data is None:
193         torrent_error('Unknown file name')
194     else:
195         outfile = open(real_filename, 'w')
196         outfile.write(data)
197         outfile.close()
198
199
200 def mctorrent_copyin():
201     """Put a file to the VFS"""
202     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
203
204 def mctorrent_rm():
205     """Remove a file from the VFS"""
206     sys.exit("Torrent VFS doesn't support removing files/directories (read-only filesystem)")
207
208 mctorrent_rmdir = mctorrent_rm
209
210 def mctorrent_mkdir():
211     """Create a directory in the VFS"""
212     sys.exit("Torrent VFS doesn't support creating directories (read-only filesystem)")
213
214
215 def torrent_error(error_str):
216     logger.critical("Error parsing the torrent metafile: %s", error_str)
217     sys.exit(1)
218
219 def decode_torrent():
220     try:
221         torrent_file = open(sys.argv[2], 'r')
222         data = torrent_file.read()
223         torrent_file.close()
224         return decode(data)
225     except IOError, error_str:
226         torrent_error(error_str)
227
228
229 def decode_datetime(dt):
230     from time import localtime, asctime
231     the_time = float(dt)
232     l_now = localtime(the_time)
233     return asctime(l_now)
234
235 def decode_announce_list(announce):
236     return '\n'.join(l[0] for l in announce)
237
238
239 command = sys.argv[1]
240 procname = "mctorrent_" + command
241
242 g = globals()
243 if not g.has_key(procname):
244     logger.critical("Unknown command %s", command)
245     sys.exit(1)
246
247 torrent = decode_torrent()
248
249 try:
250     g[procname]()
251 except SystemExit:
252     raise
253 except:
254     logger.exception("Error during run")