1 """
2
3 A WSGI application that generates mp3s dynamically
4 according to a ruleset.
5
6 """
7
8 import mimetypes
9 import os
10 import time
11
12 import eyeD3
13
14 import ewa.audio
15 from ewa.logutil import debug, info, error, exception
16
17 _codes = {200:'200 OK',
18 404:'404 Not Found',
19 500:'500 Internal Server Error'}
20
21 GENERIC_SENDFILE_HEADER = 'X-Sendfile'
22
23 LIGHTTPD_SENDFILE_HEADER = 'X-LIGHTTPD-send-file'
24
25 MP3_MIMETYPE = 'audio/mpeg'
26
27 mimetypes.init()
28
29
31 mtype, extension = mimetypes.guess_type(filename)
32 if mtype is None:
33 return 'application/octet-stream'
34 return mtype
35
36
38
39 - def __init__(self,
40 rule,
41 basedir,
42 targetdir=None,
43 stream=False,
44 refresh_rate=0,
45 use_xsendfile=True,
46 sendfile_header=GENERIC_SENDFILE_HEADER,
47 content_disposition='',
48 **spliceKwargs):
49 self.rule = rule
50 self.stream = stream
51 if stream and targetdir:
52 raise ValueError("in streaming mode but targetdir supplied")
53 self.spliceKwargs = spliceKwargs
54 self.refresh_rate = refresh_rate
55 self.use_xsendfile = use_xsendfile
56 self.sendfile_header = sendfile_header
57 self.content_disposition = content_disposition
58 if self.stream:
59 self.provider = ewa.audio.StreamAudioProvider(basedir,
60 tolerate_vbr=False,
61 tolerate_broken=False)
62 else:
63 self.provider = ewa.audio.FSAudioProvider(basedir,
64 False,
65 False,
66 targetdir)
67
68 basedir = property(lambda x: x.provider.basedir)
69
70 targetdir = property(lambda x: getattr(x.provider, 'basedir', None))
71
72 - def send(self, start_response, status, headers=None, iterable=None):
73 codeline = _codes[status]
74 if headers is None:
75 headers = [('Content-Type', 'text/plain')]
76 start_response(codeline, headers)
77 if iterable is None:
78 return [codeline[4:]]
79 else:
80 return iterable
81
83
84 if mp3file.startswith('/'):
85 mp3file = mp3file[1:]
86 mainpath = self.provider.get_main_path(mp3file)
87
88 maintime = os.path.getmtime(mainpath)
89 if os.path.isdir(mainpath):
90
91
92
93
94
95 raise OSError
96 if self.stream:
97 try:
98 return self.provider.create_combined(
99 mp3file,
100 self.rule,
101 **self.spliceKwargs), MP3_MIMETYPE
102 except (ewa.audio.AudioProviderException,
103 eyeD3.InvalidAudioFormatException):
104 info("%s cannot be processed. Serving statically", mainpath)
105 return open(mainpath), guess_mime(mainpath)
106 else:
107 path = self.provider.get_combined_path(mp3file)
108 try:
109 mtime = os.path.getmtime(path)
110 except OSError:
111 debug("OSError in getting mod time (ok)")
112 pass
113 else:
114
115 regen = maintime > mtime
116 if not regen:
117 if self.refresh_rate == 0:
118 debug("no refresh, returning target path")
119 return path, MP3_MIMETYPE
120 else:
121 t = time.time()
122 if t-mtime < self.refresh_rate:
123 debug("not necessary to refresh, "
124 "returning target path")
125 return path, MP3_MIMETYPE
126
127
128 debug("need to regenerate combined file")
129 try:
130 path2 = self.provider.create_combined(
131 mp3file,
132 self.rule,
133 **self.spliceKwargs)
134 except (ewa.audio.AudioProviderException,
135 eyeD3.InvalidAudioFormatException):
136 info("%s cannot be processed. Serving statically", mainpath)
137 return mainpath, guess_mime(mainpath)
138
139 debug("path returned from provider: %s", path2)
140 debug("our calculated path: %s", path)
141 return path2, MP3_MIMETYPE
142
143 - def __call__(self, environ, start_response):
144 mp3file = environ['SCRIPT_NAME']+environ['PATH_INFO']
145 info("mp3file: %s", mp3file)
146 if not mp3file:
147 return self.send(start_response, 404)
148 try:
149 result, mtype = self._create_combined(mp3file)
150 except (OSError, IOError):
151 exception("Error in looking for file %s", mp3file)
152 return self.send(start_response, 404)
153 except:
154 error("error creating combined file")
155 exception("internal server error")
156 return self.send(start_response, 500)
157 else:
158 return self.sendfile(result, start_response, mtype)
159
160 - def sendfile(self, result, start_response, mtype):
161 headers = [('Content-Type', mtype)]
162 if mtype == MP3_MIMETYPE and self.content_disposition:
163 headers.append(('Content-Disposition', self.content_disposition))
164 if self.use_xsendfile:
165 length = os.path.getsize(result)
166 headers.extend([(self.sendfile_header, result),
167 ('Content-Length', "%d" % length)])
168 debug('headers are: %s', headers)
169 return self.send(start_response,
170 200,
171 headers,
172 "OK")
173 else:
174 if not self.stream:
175 result = open(result, 'rb')
176 return self.send(start_response,
177 200,
178 headers,
179 result)
180