~ubuntu-branches/ubuntu/karmic/calibre/karmic-updates

« back to all changes in this revision

Viewing changes to src/calibre/library/server.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-04-05 18:42:16 UTC
  • mfrom: (1.1.7 sid)
  • Revision ID: james.westby@ubuntu.com-20090405184216-cyb0x4edrwjcaw33
Tags: 0.5.9+dfsg-1
* New upstream release. (Closes: #525339)
* manpages-installation.patch: Encode generated manpages as UTF-8, to avoid
  UnicodeDecodeErrors when writing them out to files.
* debian/control: Demote calibre dependency of calibre-bin to Recommends:,
  which is sufficient and avoids a circular dependency. (Closes: #522059)
* debian/control: Drop build dependency help2man, current version does not
  need it any more.
* debian/control: Drop versioned build dependency on python-mechanize,
  current sid version is enough.
* debian/rules: Copy "setup.py install" command from cdbs'
  python-distutils.mk, since the current version broke this. This is a
  hackish workaround until #525436 gets fixed.
* debian/rules: Drop using $(wildcard ), use `ls`; the former does not work
  any more.

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
HTTP server for remote access to the calibre database.
8
8
'''
9
9
 
10
 
import sys, textwrap, mimetypes, operator, os, re, logging
 
10
import sys, textwrap, operator, os, re, logging
11
11
from itertools import repeat
12
12
from logging.handlers import RotatingFileHandler
13
13
from datetime import datetime
18
18
 
19
19
from calibre.constants import __version__, __appname__
20
20
from calibre.utils.genshi.template import MarkupTemplate
21
 
from calibre import fit_image
 
21
from calibre import fit_image, guess_type
22
22
from calibre.resources import jquery, server_resources, build_time
23
23
from calibre.library import server_config as config
24
24
from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
25
25
from calibre.utils.config import config_dir
 
26
from calibre.utils.mdns import publish as publish_zeroconf, \
 
27
                               stop_server as stop_zeroconf
26
28
 
27
29
build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
28
30
server_resources['jquery.js'] = jquery
29
31
 
30
32
def expose(func):
31
 
    
 
33
 
32
34
    def do(self, *args, **kwargs):
33
35
        dict.update(cherrypy.response.headers, {'Server':self.server_name})
34
36
        return func(self, *args, **kwargs)
35
 
    
 
37
 
36
38
    return cherrypy.expose(do)
37
39
 
38
40
log_access_file = os.path.join(config_dir, 'server_access_log.txt')
39
41
log_error_file = os.path.join(config_dir, 'server_error_log.txt')
40
 
    
 
42
 
41
43
 
42
44
class LibraryServer(object):
43
 
    
 
45
 
44
46
    server_name = __appname__ + '/' + __version__
45
47
 
46
48
    BOOK = textwrap.dedent('''\
47
 
        <book xmlns:py="http://genshi.edgewall.org/" 
48
 
            id="${r[0]}" 
 
49
        <book xmlns:py="http://genshi.edgewall.org/"
 
50
            id="${r[0]}"
49
51
            title="${r[1]}"
50
52
            sort="${r[11]}"
51
53
            author_sort="${r[12]}"
52
 
            authors="${authors}" 
 
54
            authors="${authors}"
53
55
            rating="${r[4]}"
54
 
            timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}" 
55
 
            size="${r[6]}" 
 
56
            timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}"
 
57
            size="${r[6]}"
56
58
            isbn="${r[14] if r[14] else ''}"
57
59
            formats="${r[13] if r[13] else ''}"
58
60
            series = "${r[9] if r[9] else ''}"
61
63
            publisher="${r[3] if r[3] else ''}">${r[8] if r[8] else ''}
62
64
            </book>
63
65
        ''')
64
 
    
 
66
 
65
67
    LIBRARY = MarkupTemplate(textwrap.dedent('''\
66
68
    <?xml version="1.0" encoding="utf-8"?>
67
69
    <library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}">
70
72
    </py:for>
71
73
    </library>
72
74
    '''))
73
 
    
 
75
 
74
76
    STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
75
77
    <entry xmlns:py="http://genshi.edgewall.org/">
76
78
        <title>${record[FM['title']]}</title>
77
79
        <id>urn:calibre:${record[FM['id']]}</id>
78
80
        <author><name>${authors}</name></author>
79
81
        <updated>${record[FM['timestamp']].strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
80
 
        <link type="application/epub+zip" href="/get/epub/${record[FM['id']]}" />
 
82
        <link type="${mimetype}" href="/get/${fmt}/${record[FM['id']]}" />
81
83
        <link rel="x-stanza-cover-image" type="image/jpeg" href="/get/cover/${record[FM['id']]}" />
82
84
        <link rel="x-stanza-cover-image-thumbnail" type="image/jpeg" href="/get/thumb/${record[FM['id']]}" />
83
85
        <content type="xhtml">
85
87
        </content>
86
88
    </entry>
87
89
    '''))
88
 
    
 
90
 
89
91
    STANZA = MarkupTemplate(textwrap.dedent('''\
90
92
    <?xml version="1.0" encoding="utf-8"?>
91
93
    <feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
105
107
    </feed>
106
108
    '''))
107
109
 
108
 
    
 
110
 
109
111
    def __init__(self, db, opts, embedded=False, show_tracebacks=True):
110
112
        self.db = db
111
113
        for item in self.db:
114
116
        self.opts = opts
115
117
        self.max_cover_width, self.max_cover_height = \
116
118
                        map(int, self.opts.max_cover.split('x'))
117
 
        
 
119
 
118
120
        cherrypy.config.update({
119
121
                                'log.screen'             : opts.develop,
120
122
                                'engine.autoreload_on'   : opts.develop,
139
141
                      'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'),
140
142
                      'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
141
143
                      }
142
 
            
 
144
 
143
145
        self.is_running = False
144
146
        self.exception = None
145
 
        
 
147
 
146
148
    def setup_loggers(self):
147
149
        access_file = log_access_file
148
150
        error_file  = log_error_file
150
152
 
151
153
        maxBytes = getattr(log, "rot_maxBytes", 10000000)
152
154
        backupCount = getattr(log, "rot_backupCount", 1000)
153
 
        
 
155
 
154
156
        # Make a new RotatingFileHandler for the error log.
155
157
        h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount)
156
158
        h.setLevel(logging.DEBUG)
157
159
        h.setFormatter(cherrypy._cplogging.logfmt)
158
160
        log.error_log.addHandler(h)
159
 
        
 
161
 
160
162
        # Make a new RotatingFileHandler for the access log.
161
163
        h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount)
162
164
        h.setLevel(logging.DEBUG)
163
165
        h.setFormatter(cherrypy._cplogging.logfmt)
164
166
        log.access_log.addHandler(h)
165
167
 
166
 
    
 
168
 
167
169
    def start(self):
168
170
        self.is_running = False
169
171
        self.setup_loggers()
171
173
        try:
172
174
            cherrypy.engine.start()
173
175
            self.is_running = True
 
176
            publish_zeroconf('Books in calibre', '_stanza._tcp',
 
177
                             self.opts.port, {'path':'/stanza'})
174
178
            cherrypy.engine.block()
175
179
        except Exception, e:
176
180
            self.exception = e
177
181
        finally:
178
182
            self.is_running = False
179
 
        
 
183
            stop_zeroconf()
 
184
 
180
185
    def exit(self):
181
186
        cherrypy.engine.exit()
182
 
    
 
187
 
183
188
    def get_cover(self, id, thumbnail=False):
184
189
        cover = self.db.cover(id, index_is_id=True, as_file=False)
185
190
        if cover is None:
191
196
        try:
192
197
            if QApplication.instance() is None:
193
198
                QApplication([])
194
 
            
 
199
 
195
200
            im = QImage()
196
201
            im.loadFromData(cover)
197
202
            if im.isNull():
198
203
                raise cherrypy.HTTPError(404, 'No valid cover found')
199
204
            width, height = im.width(), im.height()
200
 
            scaled, width, height = fit_image(width, height, 
201
 
                60 if thumbnail else self.max_cover_width, 
 
205
            scaled, width, height = fit_image(width, height,
 
206
                60 if thumbnail else self.max_cover_width,
202
207
                80 if thumbnail else self.max_cover_height)
203
208
            if not scaled:
204
209
                return cover
212
217
            import traceback
213
218
            traceback.print_exc()
214
219
            raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err)
215
 
        
 
220
 
216
221
    def get_format(self, id, format):
217
222
        format = format.upper()
218
223
        fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb')
219
224
        if fmt is None:
220
225
            raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
221
 
        mt = mimetypes.guess_type('dummy.'+format.lower())[0]
 
226
        mt = guess_type('dummy.'+format.lower())[0]
222
227
        if mt is None:
223
228
            mt = 'application/octet-stream'
224
229
        cherrypy.response.headers['Content-Type'] = mt
227
232
            updated = datetime.utcfromtimestamp(os.stat(path).st_mtime)
228
233
            cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
229
234
        return fmt.read()
230
 
    
 
235
 
231
236
    def sort(self, items, field, order):
232
237
        field = field.lower().strip()
233
238
        if field == 'author':
238
243
            raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
239
244
        cmpf = cmp if field in ('rating', 'size', 'timestamp') else \
240
245
                lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '')
241
 
        field = FIELD_MAP[field]
242
 
        getter = operator.itemgetter(field)
243
 
        items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
244
 
    
 
246
        if field == 'series':
 
247
            items.sort(cmp=self.seriescmp, reverse=not order)
 
248
        else:
 
249
            field = FIELD_MAP[field]
 
250
            getter = operator.itemgetter(field)
 
251
            items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
 
252
 
 
253
    def seriescmp(self, x, y):
 
254
        si = FIELD_MAP['series']
 
255
        try:
 
256
            ans = cmp(x[si].lower(), y[si].lower())
 
257
        except AttributeError: # Some entries may be None
 
258
            ans = cmp(x[si], y[si])
 
259
        if ans != 0: return ans
 
260
        return cmp(x[FIELD_MAP['series_index']], y[FIELD_MAP['series_index']])
 
261
 
 
262
 
245
263
    def last_modified(self, updated):
246
264
        lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
247
265
        day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'}
249
267
        month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul',
250
268
                 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
251
269
        return lm.replace('month', month[updated.month])
252
 
        
253
 
        
 
270
 
 
271
 
254
272
    @expose
255
273
    def stanza(self):
256
274
        ' Feeds to read calibre books on a ipod with stanza.'
258
276
        for record in iter(self.db):
259
277
            r = record[FIELD_MAP['formats']]
260
278
            r = r.upper() if r else ''
261
 
            if 'EPUB' in r:
262
 
                authors = ' & '.join([i.replace('|', ',') for i in record[FIELD_MAP['authors']].split(',')])
 
279
            if 'EPUB' in r or 'PDB' in r:
 
280
                authors = ' & '.join([i.replace('|', ',') for i in
 
281
                                      record[FIELD_MAP['authors']].split(',')])
263
282
                extra = []
264
283
                rating = record[FIELD_MAP['rating']]
265
284
                if rating > 0:
270
289
                    extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
271
290
                series = record[FIELD_MAP['series']]
272
291
                if series:
273
 
                    extra.append('SERIES: %s [%d]<br />'%(series, record[FIELD_MAP['series_index']]))
274
 
                books.append(self.STANZA_ENTRY.generate(authors=authors,
275
 
                                                        record=record, FM=FIELD_MAP,
276
 
                                                        port=self.opts.port,
277
 
                                                        extra = ''.join(extra),
278
 
                                                        ).render('xml').decode('utf8'))
279
 
        
 
292
                    extra.append('SERIES: %s [%d]<br />'%(series,
 
293
                                            record[FIELD_MAP['series_index']]))
 
294
                fmt = 'epub' if 'EPUB' in r else 'pdb'
 
295
                mimetype = guess_type('dummy.'+fmt)[0]
 
296
                books.append(self.STANZA_ENTRY.generate(
 
297
                                                authors=authors,
 
298
                                                record=record, FM=FIELD_MAP,
 
299
                                                port=self.opts.port,
 
300
                                                extra = ''.join(extra),
 
301
                                                mimetype=mimetype,
 
302
                                                fmt=fmt,
 
303
                                                ).render('xml').decode('utf8'))
 
304
 
280
305
        updated = self.db.last_modified()
281
306
        cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
282
307
        cherrypy.response.headers['Content-Type'] = 'text/xml'
283
 
        
 
308
 
284
309
        return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP,
285
310
                    updated=updated, id='urn:calibre:main').render('xml')
286
 
    
 
311
 
287
312
    @expose
288
 
    def library(self, start='0', num='50', sort=None, search=None, 
 
313
    def library(self, start='0', num='50', sort=None, search=None,
289
314
                _=None, order='ascending'):
290
315
        '''
291
316
        Serves metadata from the calibre database as XML.
292
 
        
 
317
 
293
318
        :param sort: Sort results by ``sort``. Can be one of `title,author,rating`.
294
319
        :param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax
295
320
        :param start,num: Return the slice `[start:start+num]` of the sorted and filtered results
296
 
        :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching 
 
321
        :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching
297
322
        '''
298
323
        try:
299
324
            start = int(start)
309
334
        items = [r for r in iter(self.db) if r[0] in ids]
310
335
        if sort is not None:
311
336
            self.sort(items, sort, order)
312
 
        
 
337
 
313
338
        book, books = MarkupTemplate(self.BOOK), []
314
339
        for record in items[start:start+num]:
315
 
            aus = record[2] if record[2] else _('Unknown')
 
340
            aus = record[2] if record[2] else __builtins__._('Unknown')
316
341
            authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
317
342
            books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
318
343
        updated = self.db.last_modified()
319
 
        
 
344
 
320
345
        cherrypy.response.headers['Content-Type'] = 'text/xml'
321
346
        cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
322
 
        return self.LIBRARY.generate(books=books, start=start, updated=updated, 
 
347
        return self.LIBRARY.generate(books=books, start=start, updated=updated,
323
348
                                     total=len(ids)).render('xml')
324
 
    
 
349
 
325
350
    @expose
326
351
    def index(self, **kwargs):
327
352
        'The / URL'
328
 
        return self.static('index.html')
329
 
    
 
353
        stanza = cherrypy.request.headers.get('Stanza-Device-Name', 919)
 
354
        if stanza == 919:
 
355
            return self.static('index.html')
 
356
        return self.stanza()
 
357
 
 
358
 
330
359
    @expose
331
360
    def get(self, what, id):
332
361
        'Serves files, covers, thumbnails from the calibre database'
345
374
        if what == 'cover':
346
375
            return self.get_cover(id)
347
376
        return self.get_format(id, what)
348
 
    
 
377
 
349
378
    @expose
350
379
    def static(self, name):
351
380
        'Serves static content'
376
405
    server.thread.setDaemon(True)
377
406
    server.thread.start()
378
407
    return server
379
 
    
 
408
 
380
409
def stop_threaded_server(server):
381
410
    server.exit()
382
411
    server.thread = None
383
 
    
 
412
 
384
413
def option_parser():
385
414
    return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.'))
386
415