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

« back to all changes in this revision

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

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-30 12:49:41 UTC
  • mfrom: (1.3.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090730124941-qjdsmri25zt8zocn
Tags: 0.6.3+dfsg-0ubuntu1
* New upstream release. Please see http://calibre.kovidgoyal.net/new_in_6/
  for the list of new features and changes.
* remove_postinstall.patch: Update for new version.
* build_debug.patch: Does not apply any more, disable for now. Might not be
  necessary any more.
* debian/copyright: Fix reference to versionless GPL.
* debian/rules: Drop obsolete dh_desktop call.
* debian/rules: Add workaround for weird Python 2.6 setuptools behaviour of
  putting compiled .so files into src/calibre/plugins/calibre/plugins
  instead of src/calibre/plugins.
* debian/rules: Drop hal fdi moving, new upstream version does not use hal
  any more. Drop hal dependency, too.
* debian/rules: Install udev rules into /lib/udev/rules.d.
* Add debian/calibre.preinst: Remove unmodified
  /etc/udev/rules.d/95-calibre.rules on upgrade.
* debian/control: Bump Python dependencies to 2.6, since upstream needs
  it now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
from itertools import repeat
12
12
from datetime import datetime
13
13
 
14
 
from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock
15
 
from PyQt4.QtGui import QApplication, QImage
16
 
__app = None
17
 
 
18
 
from calibre.library import title_sort
 
14
from PyQt4.QtCore import QThread, QReadWriteLock
 
15
try:
 
16
    from PIL import Image as PILImage
 
17
    PILImage
 
18
except ImportError:
 
19
    import Image as PILImage
 
20
 
 
21
 
 
22
from PyQt4.QtGui import QImage
 
23
 
 
24
from calibre.ebooks.metadata import title_sort
19
25
from calibre.library.database import LibraryDatabase
20
26
from calibre.library.sqlite import connect, IntegrityError
21
27
from calibre.utils.search_query_parser import SearchQueryParser
23
29
                                    MetaInformation, authors_to_sort_string
24
30
from calibre.ebooks.metadata.meta import get_metadata, set_metadata, \
25
31
    metadata_from_formats
26
 
from calibre.ebooks.metadata.opf2 import OPFCreator
 
32
from calibre.ebooks.metadata.opf2 import metadata_to_opf
27
33
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
28
34
from calibre.ptempfile import PersistentTemporaryFile
29
35
from calibre.customize.ui import run_plugins_on_import
30
36
 
31
 
from calibre import sanitize_file_name
 
37
from calibre.utils.filenames import ascii_filename, shorten_components_to, \
 
38
                                    supports_long_names
32
39
from calibre.ebooks import BOOK_EXTENSIONS
33
40
 
34
41
if iswindows:
40
47
    except:
41
48
        os.remove(path)
42
49
 
43
 
def delete_tree(path):
44
 
    try:
45
 
        winshell.delete_file(path, silent=True, no_confirm=True)
46
 
    except:
 
50
def delete_tree(path, permanent=False):
 
51
    if permanent:
47
52
        shutil.rmtree(path)
 
53
    else:
 
54
        try:
 
55
            if not permanent:
 
56
                winshell.delete_file(path, silent=True, no_confirm=True)
 
57
        except:
 
58
            shutil.rmtree(path)
48
59
 
49
60
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
50
61
 
51
62
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
52
63
             'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
53
 
             'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15}
 
64
             'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
 
65
             'lccn':16, 'pubdate':17, 'flags':18, 'cover':19}
54
66
INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys()))
55
67
 
56
68
 
197
209
                query = query.decode('utf-8')
198
210
            if location in ('tag', 'author', 'format'):
199
211
                location += 's'
200
 
            all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats')
 
212
            all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover')
201
213
            MAP = {}
202
214
            for x in all:
203
215
                MAP[x] = FIELD_MAP[x]
 
216
            EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
204
217
            location = [location] if location != 'all' else list(MAP.keys())
205
218
            for i, loc in enumerate(location):
206
219
                location[i] = MAP[loc]
 
220
            try:
 
221
                rating_query = int(query) * 2
 
222
            except:
 
223
                rating_query = None
207
224
            for item in self._data:
208
225
                if item is None: continue
209
226
                for loc in location:
210
 
                    if item[loc] and query in item[loc].lower():
211
 
                        matches.add(item[0])
212
 
                        break
 
227
                    if query == 'false' and not item[loc]:
 
228
                        if isinstance(item[loc], basestring):
 
229
                            if item[loc].strip() != '':
 
230
                                continue
 
231
                        matches.add(item[0])
 
232
                        break
 
233
                    if query == 'true' and item[loc]:
 
234
                        if isinstance(item[loc], basestring):
 
235
                            if item[loc].strip() == '':
 
236
                                continue
 
237
                        matches.add(item[0])
 
238
                        break
 
239
                    if rating_query and item[loc] and loc == MAP['rating'] and rating_query == int(item[loc]):
 
240
                        matches.add(item[0])
 
241
                        break
 
242
                    if item[loc] and loc not in EXCLUDE_FIELDS and query in item[loc].lower():
 
243
                        matches.add(item[0])
 
244
                        break
 
245
 
213
246
        return matches
214
247
 
215
248
    def remove(self, id):
223
256
        id = row if row_is_id else self._map_filtered[row]
224
257
        self._data[id][col] = val
225
258
 
 
259
    def get(self, row, col, row_is_id=False):
 
260
        id = row if row_is_id else self._map_filtered[row]
 
261
        return self._data[id][col]
 
262
 
226
263
    def index(self, id, cache=False):
227
264
        x = self._map if cache else self._map_filtered
228
265
        return x.index(id)
237
274
            pass
238
275
        return False
239
276
 
240
 
    def refresh_ids(self, conn, ids):
 
277
    def refresh_ids(self, db, ids):
241
278
        '''
242
279
        Refresh the data in the cache for books identified by ids.
243
280
        Returns a list of affected rows or None if the rows are filtered.
244
281
        '''
245
282
        for id in ids:
246
283
            try:
247
 
                self._data[id] = conn.get('SELECT * from meta WHERE id=?',
 
284
                self._data[id] = db.conn.get('SELECT * from meta WHERE id=?',
248
285
                        (id,))[0]
 
286
                self._data[id].append(db.has_cover(id, index_is_id=True))
249
287
            except IndexError:
250
288
                return None
251
289
        try:
254
292
            pass
255
293
        return None
256
294
 
257
 
    def books_added(self, ids, conn):
 
295
    def books_added(self, ids, db):
258
296
        if not ids:
259
297
            return
260
298
        self._data.extend(repeat(None, max(ids)-len(self._data)+2))
261
299
        for id in ids:
262
 
            self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0]
 
300
            self._data[id] = db.conn.get('SELECT * from meta WHERE id=?', (id,))[0]
 
301
            self._data[id].append(db.has_cover(id, index_is_id=True))
263
302
        self._map[0:0] = ids
264
303
        self._map_filtered[0:0] = ids
265
304
 
277
316
        self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else []
278
317
        for r in temp:
279
318
            self._data[r[0]] = r
 
319
        for item in self._data:
 
320
            if item is not None:
 
321
                item.append(db.has_cover(item[0], index_is_id=True))
280
322
        self._map = [i[0] for i in self._data if i is not None]
281
323
        if field is not None:
282
324
            self.sort(field, ascending)
342
384
    An ebook metadata database that stores references to ebook files on disk.
343
385
    '''
344
386
    PATH_LIMIT = 40 if 'win32' in sys.platform else 100
345
 
    @apply
346
 
    def user_version():
 
387
    @dynamic_property
 
388
    def user_version(self):
347
389
        doc = 'The user version of this database'
348
390
 
349
391
        def fget(self):
395
437
        self.refresh = functools.partial(self.data.refresh, self)
396
438
        self.sort    = self.data.sort
397
439
        self.index   = self.data.index
398
 
        self.refresh_ids = functools.partial(self.data.refresh_ids, self.conn)
 
440
        self.refresh_ids = functools.partial(self.data.refresh_ids, self)
399
441
        self.row     = self.data.row
400
442
        self.has_id  = self.data.has_id
401
443
        self.count   = self.data.count
472
514
        FROM books;
473
515
        ''')
474
516
 
 
517
    def upgrade_version_4(self):
 
518
        'Rationalize books table'
 
519
        self.conn.executescript('''
 
520
        BEGIN TRANSACTION;
 
521
        CREATE TEMPORARY TABLE
 
522
        books_backup(id,title,sort,timestamp,series_index,author_sort,isbn,path);
 
523
        INSERT INTO books_backup SELECT id,title,sort,timestamp,series_index,author_sort,isbn,path FROM books;
 
524
        DROP TABLE books;
 
525
        CREATE TABLE books ( id      INTEGER PRIMARY KEY AUTOINCREMENT,
 
526
                             title     TEXT NOT NULL DEFAULT 'Unknown' COLLATE NOCASE,
 
527
                             sort      TEXT COLLATE NOCASE,
 
528
                             timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 
529
                             pubdate   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 
530
                             series_index REAL NOT NULL DEFAULT 1.0,
 
531
                             author_sort TEXT COLLATE NOCASE,
 
532
                             isbn TEXT DEFAULT "" COLLATE NOCASE,
 
533
                             lccn TEXT DEFAULT "" COLLATE NOCASE,
 
534
                             path TEXT NOT NULL DEFAULT "",
 
535
                             flags INTEGER NOT NULL DEFAULT 1
 
536
                        );
 
537
        INSERT INTO
 
538
            books (id,title,sort,timestamp,pubdate,series_index,author_sort,isbn,path)
 
539
            SELECT id,title,sort,timestamp,timestamp,series_index,author_sort,isbn,path FROM books_backup;
 
540
        DROP TABLE books_backup;
 
541
 
 
542
        DROP VIEW meta;
 
543
        CREATE VIEW meta AS
 
544
        SELECT id, title,
 
545
               (SELECT concat(name) FROM authors WHERE authors.id IN (SELECT author from books_authors_link WHERE book=books.id)) authors,
 
546
               (SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher,
 
547
               (SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating,
 
548
               timestamp,
 
549
               (SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size,
 
550
               (SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags,
 
551
               (SELECT text FROM comments WHERE book=books.id) comments,
 
552
               (SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series,
 
553
               series_index,
 
554
               sort,
 
555
               author_sort,
 
556
               (SELECT concat(format) FROM data WHERE data.book=books.id) formats,
 
557
               isbn,
 
558
               path,
 
559
               lccn,
 
560
               pubdate,
 
561
               flags
 
562
        FROM books;
 
563
        ''')
 
564
 
 
565
    def upgrade_version_5(self):
 
566
        'Update indexes/triggers for new books table'
 
567
        self.conn.executescript('''
 
568
        BEGIN TRANSACTION;
 
569
        CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE);
 
570
        CREATE INDEX books_idx ON books (sort COLLATE NOCASE);
 
571
        CREATE TRIGGER books_delete_trg
 
572
            AFTER DELETE ON books
 
573
            BEGIN
 
574
                DELETE FROM books_authors_link WHERE book=OLD.id;
 
575
                DELETE FROM books_publishers_link WHERE book=OLD.id;
 
576
                DELETE FROM books_ratings_link WHERE book=OLD.id;
 
577
                DELETE FROM books_series_link WHERE book=OLD.id;
 
578
                DELETE FROM books_tags_link WHERE book=OLD.id;
 
579
                DELETE FROM data WHERE book=OLD.id;
 
580
                DELETE FROM comments WHERE book=OLD.id;
 
581
                DELETE FROM conversion_options WHERE book=OLD.id;
 
582
        END;
 
583
        CREATE TRIGGER books_insert_trg
 
584
            AFTER INSERT ON books
 
585
            BEGIN
 
586
            UPDATE books SET sort=title_sort(NEW.title) WHERE id=NEW.id;
 
587
        END;
 
588
        CREATE TRIGGER books_update_trg
 
589
            AFTER UPDATE ON books
 
590
            BEGIN
 
591
            UPDATE books SET sort=title_sort(NEW.title) WHERE id=NEW.id;
 
592
        END;
 
593
 
 
594
        UPDATE books SET sort=title_sort(title) WHERE sort IS NULL;
 
595
 
 
596
        END TRANSACTION;
 
597
        '''
 
598
        )
 
599
 
 
600
 
 
601
    def upgrade_version_6(self):
 
602
        'Show authors in order'
 
603
        self.conn.executescript('''
 
604
        BEGIN TRANSACTION;
 
605
        DROP VIEW meta;
 
606
        CREATE VIEW meta AS
 
607
        SELECT id, title,
 
608
               (SELECT sortconcat(bal.id, name) FROM books_authors_link AS bal JOIN authors ON(author = authors.id) WHERE book = books.id) authors,
 
609
               (SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher,
 
610
               (SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating,
 
611
               timestamp,
 
612
               (SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size,
 
613
               (SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags,
 
614
               (SELECT text FROM comments WHERE book=books.id) comments,
 
615
               (SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series,
 
616
               series_index,
 
617
               sort,
 
618
               author_sort,
 
619
               (SELECT concat(format) FROM data WHERE data.book=books.id) formats,
 
620
               isbn,
 
621
               path,
 
622
               lccn,
 
623
               pubdate,
 
624
               flags
 
625
        FROM books;
 
626
        END TRANSACTION;
 
627
        ''')
 
628
 
 
629
 
475
630
 
476
631
    def last_modified(self):
477
632
        ''' Return last modified time as a UTC datetime object'''
498
653
        authors = self.authors(id, index_is_id=True)
499
654
        if not authors:
500
655
            authors = _('Unknown')
501
 
        author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
502
 
        title  = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
 
656
        author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
 
657
        title  = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
503
658
        path   = author + '/' + title + ' (%d)'%id
504
659
        return path
505
660
 
510
665
        authors = self.authors(id, index_is_id=True)
511
666
        if not authors:
512
667
            authors = _('Unknown')
513
 
        author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
514
 
        title  = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
 
668
        author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
 
669
        title  = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
515
670
        name   = title + ' - ' + author
 
671
        while name.endswith('.'):
 
672
            name = name[:-1]
516
673
        return name
517
674
 
518
 
    def rmtree(self, path):
 
675
    def rmtree(self, path, permanent=False):
519
676
        if not self.normpath(self.library_path).startswith(self.normpath(path)):
520
 
            delete_tree(path)
 
677
            delete_tree(path, permanent=permanent)
521
678
 
522
679
    def normpath(self, path):
523
680
        path = os.path.abspath(os.path.realpath(path))
569
726
        # Delete not needed directories
570
727
        if current_path and os.path.exists(spath):
571
728
            if self.normpath(spath) != self.normpath(tpath):
572
 
                self.rmtree(spath)
 
729
                self.rmtree(spath, permanent=True)
573
730
                parent  = os.path.dirname(spath)
574
731
                if len(os.listdir(parent)) == 0:
575
 
                    self.rmtree(parent)
 
732
                    self.rmtree(parent, permanent=True)
576
733
 
577
734
    def add_listener(self, listener):
578
735
        '''
610
767
                return img
611
768
            return f if as_file else f.read()
612
769
 
 
770
    def timestamp(self, index, index_is_id=False):
 
771
        if index_is_id:
 
772
            return self.conn.get('SELECT timestamp FROM meta WHERE id=?', (index,), all=False)
 
773
        return self.data[index][FIELD_MAP['timestamp']]
 
774
 
 
775
    def pubdate(self, index, index_is_id=False):
 
776
        if index_is_id:
 
777
            return self.conn.get('SELECT pubdate FROM meta WHERE id=?', (index,), all=False)
 
778
        return self.data[index][FIELD_MAP['pubdate']]
 
779
 
613
780
    def get_metadata(self, idx, index_is_id=False, get_cover=False):
614
781
        '''
615
782
        Convenience method to return metadata as a L{MetaInformation} object.
621
788
        mi.comments    = self.comments(idx, index_is_id=index_is_id)
622
789
        mi.publisher   = self.publisher(idx, index_is_id=index_is_id)
623
790
        mi.timestamp   = self.timestamp(idx, index_is_id=index_is_id)
 
791
        mi.pubdate     = self.pubdate(idx, index_is_id=index_is_id)
624
792
        tags = self.tags(idx, index_is_id=index_is_id)
625
793
        if tags:
626
794
            mi.tags = [i.strip() for i in tags.split(',')]
658
826
        if callable(getattr(data, 'save', None)):
659
827
            data.save(path)
660
828
        else:
661
 
            if not QCoreApplication.instance():
662
 
                global __app
663
 
                __app = QApplication([])
664
 
            p = QImage()
665
 
            if callable(getattr(data, 'read', None)):
666
 
                data = data.read()
667
 
            p.loadFromData(data)
668
 
            p.save(path)
 
829
            f = data
 
830
            if not callable(getattr(data, 'read', None)):
 
831
                f = cStringIO.StringIO(data)
 
832
            im = PILImage.open(f)
 
833
            im.convert('RGB').save(path, 'JPEG')
669
834
 
670
835
    def all_formats(self):
671
836
        formats = self.conn.get('SELECT format from data')
883
1048
            self.set_rating(id, val, notify=False)
884
1049
        elif column == 'tags':
885
1050
            self.set_tags(id, val.split(','), append=False, notify=False)
886
 
        self.data.refresh_ids(self.conn, [id])
 
1051
        self.data.refresh_ids(self, [id])
887
1052
        self.set_path(id, True)
888
1053
        self.notify('metadata', [id])
889
1054
 
897
1062
                mi.authors = [_('Unknown')]
898
1063
        authors = []
899
1064
        for a in mi.authors:
900
 
            authors += a.split('&')
 
1065
            authors += string_to_authors(a)
901
1066
        self.set_authors(id, authors, notify=False)
902
1067
        if mi.author_sort:
903
1068
            self.set_author_sort(id, mi.author_sort, notify=False)
917
1082
            self.set_comment(id, mi.comments, notify=False)
918
1083
        if mi.isbn and mi.isbn.strip():
919
1084
            self.set_isbn(id, mi.isbn, notify=False)
920
 
        if mi.series_index and mi.series_index > 0:
 
1085
        if mi.series_index:
921
1086
            self.set_series_index(id, mi.series_index, notify=False)
 
1087
        if mi.pubdate:
 
1088
            self.set_pubdate(id, mi.pubdate, notify=False)
922
1089
        if getattr(mi, 'timestamp', None) is not None:
923
1090
            self.set_timestamp(id, mi.timestamp, notify=False)
924
1091
        self.set_path(id, True)
983
1150
            if notify:
984
1151
                self.notify('metadata', [id])
985
1152
 
 
1153
    def set_pubdate(self, id, dt, notify=True):
 
1154
        if dt:
 
1155
            self.conn.execute('UPDATE books SET pubdate=? WHERE id=?', (dt, id))
 
1156
            self.data.set(id, FIELD_MAP['pubdate'], dt, row_is_id=True)
 
1157
            self.conn.commit()
 
1158
            if notify:
 
1159
                self.notify('metadata', [id])
 
1160
 
 
1161
 
986
1162
    def set_publisher(self, id, publisher, notify=True):
987
1163
        self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
988
1164
        self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1')
1000
1176
            if notify:
1001
1177
                self.notify('metadata', [id])
1002
1178
 
 
1179
    def get_tags(self, id):
 
1180
        result = self.conn.get(
 
1181
        'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',
 
1182
        (id,), all=True)
 
1183
        if not result:
 
1184
            return set([])
 
1185
        return set([r[0] for r in result])
 
1186
 
1003
1187
    def set_tags(self, id, tags, append=False, notify=True):
1004
1188
        '''
1005
1189
        @param tags: list of strings
1008
1192
        if not append:
1009
1193
            self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
1010
1194
            self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1')
1011
 
        for tag in set(tags):
 
1195
        otags = self.get_tags(id)
 
1196
        for tag in (set(tags)-otags):
1012
1197
            tag = tag.strip()
1013
1198
            if not tag:
1014
1199
                continue
1033
1218
                self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)',
1034
1219
                              (id, tid))
1035
1220
        self.conn.commit()
1036
 
        try:
1037
 
            otags = [t.strip() for t in self.data[self.data.row(id)][FIELD_MAP['tags']].split(',')]
1038
 
        except AttributeError:
1039
 
            otags = []
1040
 
        if not append:
1041
 
            otags = []
1042
 
        tags = ','.join(otags+tags)
 
1221
        tags = ','.join(self.get_tags(id))
1043
1222
        self.data.set(id, FIELD_MAP['tags'], tags, row_is_id=True)
1044
1223
        if notify:
1045
1224
            self.notify('metadata', [id])
1050
1229
            if id:
1051
1230
                self.conn.execute('DELETE FROM books_tags_link WHERE tag=? AND book=?', (id, book_id))
1052
1231
        self.conn.commit()
1053
 
        self.data.refresh_ids(self.conn, [book_id])
 
1232
        self.data.refresh_ids(self, [book_id])
1054
1233
        if notify:
1055
1234
            self.notify('metadata', [id])
1056
1235
 
1103
1282
 
1104
1283
    def set_series_index(self, id, idx, notify=True):
1105
1284
        if idx is None:
1106
 
            idx = 1
1107
 
        idx = int(idx)
1108
 
        self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id))
 
1285
            idx = 1.0
 
1286
        idx = float(idx)
 
1287
        self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (idx, id))
1109
1288
        self.conn.commit()
1110
 
        try:
1111
 
            row = self.row(id)
1112
 
            if row is not None:
1113
 
                self.data.set(row, 10, idx)
1114
 
        except ValueError:
1115
 
            pass
1116
 
        self.data.set(id, FIELD_MAP['series_index'], int(idx), row_is_id=True)
 
1289
        self.data.set(id, FIELD_MAP['series_index'], idx, row_is_id=True)
1117
1290
        if notify:
1118
1291
            self.notify('metadata', [id])
1119
1292
 
1156
1329
        stream.seek(0)
1157
1330
        mi = get_metadata(stream, format, use_libprs_metadata=False)
1158
1331
        stream.seek(0)
1159
 
        mi.series_index = 1
 
1332
        mi.series_index = 1.0
1160
1333
        mi.tags = [_('News'), recipe.title]
1161
1334
        obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)',
1162
1335
                              (mi.title, mi.authors[0]))
1163
1336
        id = obj.lastrowid
1164
 
        self.data.books_added([id], self.conn)
 
1337
        self.data.books_added([id], self)
1165
1338
        self.set_path(id, index_is_id=True)
1166
1339
        self.conn.commit()
1167
1340
        self.set_metadata(id, mi)
1170
1343
        if not hasattr(path, 'read'):
1171
1344
            stream.close()
1172
1345
        self.conn.commit()
1173
 
        self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
 
1346
        self.data.refresh_ids(self, [id]) # Needed to update format list and size
1174
1347
        return id
1175
1348
 
1176
1349
    def run_import_plugins(self, path_or_stream, format):
1185
1358
            path = path_or_stream
1186
1359
        return run_plugins_on_import(path, format)
1187
1360
 
1188
 
    def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
 
1361
    def create_book_entry(self, mi, cover=None, add_duplicates=True):
 
1362
        if not add_duplicates and self.has_book(mi):
 
1363
            return None
 
1364
        series_index = 1.0 if mi.series_index is None else mi.series_index
 
1365
        aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
 
1366
        title = mi.title
 
1367
        if isinstance(aus, str):
 
1368
            aus = aus.decode(preferred_encoding, 'replace')
 
1369
        if isinstance(title, str):
 
1370
            title = title.decode(preferred_encoding)
 
1371
        obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
 
1372
                            (title, series_index, aus))
 
1373
        id = obj.lastrowid
 
1374
        self.data.books_added([id], self)
 
1375
        self.set_path(id, True)
 
1376
        self.conn.commit()
 
1377
        self.set_metadata(id, mi)
 
1378
        if cover is not None:
 
1379
            self.set_cover(id, cover)
 
1380
        return id
 
1381
 
 
1382
 
 
1383
    def add_books(self, paths, formats, metadata, add_duplicates=True):
1189
1384
        '''
1190
1385
        Add a book to the database. The result cache is not updated.
1191
1386
        :param:`paths` List of paths to book files or file-like objects
1192
1387
        '''
1193
 
        formats, metadata, uris = iter(formats), iter(metadata), iter(uris)
 
1388
        formats, metadata = iter(formats), iter(metadata)
1194
1389
        duplicates = []
1195
1390
        ids = []
1196
1391
        for path in paths:
1197
1392
            mi = metadata.next()
1198
1393
            format = formats.next()
1199
 
            try:
1200
 
                uri = uris.next()
1201
 
            except StopIteration:
1202
 
                uri = None
1203
1394
            if not add_duplicates and self.has_book(mi):
1204
 
                duplicates.append((path, format, mi, uri))
 
1395
                duplicates.append((path, format, mi))
1205
1396
                continue
1206
 
            series_index = 1 if mi.series_index is None else mi.series_index
 
1397
            series_index = 1.0 if mi.series_index is None else mi.series_index
1207
1398
            aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
1208
1399
            title = mi.title
1209
1400
            if isinstance(aus, str):
1210
1401
                aus = aus.decode(preferred_encoding, 'replace')
1211
1402
            if isinstance(title, str):
1212
1403
                title = title.decode(preferred_encoding)
1213
 
            obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
1214
 
                              (title, uri, series_index, aus))
 
1404
            obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
 
1405
                              (title, series_index, aus))
1215
1406
            id = obj.lastrowid
1216
 
            self.data.books_added([id], self.conn)
 
1407
            self.data.books_added([id], self)
1217
1408
            ids.append(id)
1218
1409
            self.set_path(id, True)
1219
1410
            self.conn.commit()
1224
1415
            self.add_format(id, format, stream, index_is_id=True)
1225
1416
            stream.close()
1226
1417
        self.conn.commit()
1227
 
        self.data.refresh_ids(self.conn, ids) # Needed to update format list and size
 
1418
        self.data.refresh_ids(self, ids) # Needed to update format list and size
1228
1419
        if duplicates:
1229
1420
            paths    = list(duplicate[0] for duplicate in duplicates)
1230
1421
            formats  = list(duplicate[1] for duplicate in duplicates)
1231
1422
            metadata = list(duplicate[2] for duplicate in duplicates)
1232
 
            uris     = list(duplicate[3] for duplicate in duplicates)
1233
 
            return (paths, formats, metadata, uris), len(ids)
 
1423
            return (paths, formats, metadata), len(ids)
1234
1424
        return None, len(ids)
1235
1425
 
1236
1426
    def import_book(self, mi, formats, notify=True):
1237
 
        series_index = 1 if mi.series_index is None else mi.series_index
 
1427
        series_index = 1.0 if mi.series_index is None else mi.series_index
1238
1428
        if not mi.title:
1239
1429
            mi.title = _('Unknown')
1240
1430
        if not mi.authors:
1244
1434
            aus = aus.decode(preferred_encoding, 'replace')
1245
1435
        title = mi.title if isinstance(mi.title, unicode) else \
1246
1436
                mi.title.decode(preferred_encoding, 'replace')
1247
 
        obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
1248
 
                          (title, None, series_index, aus))
 
1437
        obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
 
1438
                          (title, series_index, aus))
1249
1439
        id = obj.lastrowid
1250
 
        self.data.books_added([id], self.conn)
 
1440
        self.data.books_added([id], self)
1251
1441
        self.set_path(id, True)
1252
1442
        self.set_metadata(id, mi)
1253
1443
        for path in formats:
1256
1446
                continue
1257
1447
            self.add_format_with_hooks(id, ext, path, index_is_id=True)
1258
1448
        self.conn.commit()
1259
 
        self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
 
1449
        self.data.refresh_ids(self, [id]) # Needed to update format list and size
1260
1450
        if notify:
1261
1451
            self.notify('add', [id])
1262
1452
 
1263
 
    def move_library_to(self, newloc, progress=None):
1264
 
        header = _(u'<p>Copying books to %s<br><center>')%newloc
 
1453
    def move_library_to(self, newloc, progress=lambda x: x):
1265
1454
        books = self.conn.get('SELECT id, path, title FROM books')
1266
 
        if progress is not None:
1267
 
            progress.setValue(0)
1268
 
            progress.setLabelText(header)
1269
 
            QCoreApplication.processEvents()
1270
 
            progress.setAutoReset(False)
1271
 
            progress.setRange(0, len(books))
1272
1455
        if not os.path.exists(newloc):
1273
1456
            os.makedirs(newloc)
1274
1457
        old_dirs = set([])
1275
1458
        for i, book in enumerate(books):
1276
 
            if progress is not None:
1277
 
                progress.setLabelText(header+_(u'Copying <b>%s</b>')%book[2])
1278
1459
            path = book[1]
1279
1460
            if not path:
1280
1461
                continue
1286
1467
            if os.path.exists(srcdir):
1287
1468
                shutil.copytree(srcdir, tdir)
1288
1469
            old_dirs.add(srcdir)
1289
 
            if progress is not None:
1290
 
                progress.setValue(i+1)
 
1470
            progress(book[2])
1291
1471
 
1292
1472
        dbpath = os.path.join(newloc, os.path.basename(self.dbpath))
1293
1473
        shutil.copyfile(self.dbpath, dbpath)
1301
1481
                shutil.rmtree(dir)
1302
1482
        except:
1303
1483
            pass
1304
 
        if progress is not None:
1305
 
            progress.reset()
1306
 
            progress.hide()
1307
 
 
1308
1484
 
1309
1485
    def __iter__(self):
1310
1486
        for record in self.data._data:
1335
1511
            data.append(x)
1336
1512
            x['id'] = record[FIELD_MAP['id']]
1337
1513
            x['formats'] = []
 
1514
            if not x['authors']:
 
1515
                x['authors'] = _('Unknown')
1338
1516
            x['authors'] = [i.replace('|', ',') for i in x['authors'].split(',')]
1339
1517
            if authors_as_string:
1340
1518
                x['authors'] = authors_to_string(x['authors'])
1343
1521
            x['cover'] = os.path.join(path, 'cover.jpg')
1344
1522
            if not self.has_cover(x['id'], index_is_id=True):
1345
1523
                x['cover'] = None
1346
 
            path += os.sep +  self.construct_file_name(record[FIELD_MAP['id']]) + '.%s'
1347
1524
            formats = self.formats(record[FIELD_MAP['id']], index_is_id=True)
1348
1525
            if formats:
1349
1526
                for fmt in formats.split(','):
1350
 
                    x['formats'].append(path%fmt.lower())
1351
 
                    x['fmt_'+fmt.lower()] = path%fmt.lower()
 
1527
                    path = self.format_abspath(x['id'], fmt, index_is_id=True)
 
1528
                    x['formats'].append(path)
 
1529
                    x['fmt_'+fmt.lower()] = path
1352
1530
                x['available_formats'] = [i.upper() for i in formats.split(',')]
1353
1531
 
1354
1532
        return data
1355
1533
 
1356
1534
    def migrate_old(self, db, progress):
 
1535
        from PyQt4.QtCore import QCoreApplication
1357
1536
        header = _(u'<p>Migrating old database to ebook library in %s<br><center>')%self.library_path
1358
1537
        progress.setValue(0)
1359
1538
        progress.setLabelText(header)
1360
1539
        QCoreApplication.processEvents()
1361
1540
        db.conn.row_factory = lambda cursor, row : tuple(row)
1362
1541
        db.conn.text_factory = lambda x : unicode(x, 'utf-8', 'replace')
1363
 
        books = db.conn.get('SELECT id, title, sort, timestamp, uri, series_index, author_sort, isbn FROM books ORDER BY id ASC')
 
1542
        books = db.conn.get('SELECT id, title, sort, timestamp, series_index, author_sort, isbn FROM books ORDER BY id ASC')
1364
1543
        progress.setAutoReset(False)
1365
1544
        progress.setRange(0, len(books))
1366
1545
 
1367
1546
        for book in books:
1368
 
            self.conn.execute('INSERT INTO books(id, title, sort, timestamp, uri, series_index, author_sort, isbn) VALUES(?, ?, ?, ?, ?, ?, ?, ?);', book)
 
1547
            self.conn.execute('INSERT INTO books(id, title, sort, timestamp, series_index, author_sort, isbn) VALUES(?, ?, ?, ?, ?, ?, ?, ?);', book)
1369
1548
 
1370
1549
        tables = '''
1371
1550
authors  ratings      tags    series    books_tags_link
1411
1590
            raise IOError('Target directory does not exist: '+dir)
1412
1591
        by_author = {}
1413
1592
        count = 0
 
1593
        path_len, au_len = (1000, 500) if supports_long_names(dir) else (240, 50)
1414
1594
        for index in indices:
1415
1595
            id = index if index_is_id else self.id(index)
1416
1596
            au = self.conn.get('SELECT author_sort FROM books WHERE id=?',
1424
1604
                by_author[au] = []
1425
1605
            by_author[au].append(index)
1426
1606
        for au in by_author.keys():
1427
 
            apath = os.path.join(dir, sanitize_file_name(au))
 
1607
            aname = ascii_filename(au)[:au_len]
 
1608
            apath = os.path.abspath(os.path.join(dir, aname))
1428
1609
            if not single_dir and not os.path.exists(apath):
1429
1610
                os.mkdir(apath)
1430
1611
            for idx in by_author[au]:
1431
1612
                title = re.sub(r'\s', ' ', self.title(idx, index_is_id=index_is_id))
1432
 
                tpath = os.path.join(apath, sanitize_file_name(title))
 
1613
                name = au + ' - ' + title if byauthor else title + ' - ' + au
 
1614
                name = ascii_filename(name)
 
1615
                tname = ascii_filename(title)
 
1616
                tname, name = shorten_components_to(path_len-len(apath), (tname,
 
1617
                    name))
 
1618
                name += '_'+str(id)
 
1619
 
 
1620
                tpath = os.path.join(apath, tname)
1433
1621
                id = idx if index_is_id else self.id(idx)
1434
1622
                id = str(id)
1435
1623
                if not single_dir and not os.path.exists(tpath):
1436
 
                    os.mkdir(tpath)
 
1624
                    os.makedirs(tpath)
1437
1625
 
1438
 
                name = au + ' - ' + title if byauthor else title + ' - ' + au
1439
 
                name += '_'+id
1440
1626
                base  = dir if single_dir else tpath
1441
1627
                mi = self.get_metadata(idx, index_is_id=index_is_id, get_cover=True)
1442
 
                f = open(os.path.join(base, sanitize_file_name(name)+'.opf'), 'wb')
1443
1628
                if not mi.authors:
1444
1629
                    mi.authors = [_('Unknown')]
1445
1630
                cdata = self.cover(int(id), index_is_id=True)
1446
1631
                if cdata is not None:
1447
 
                    cname = sanitize_file_name(name)+'.jpg'
 
1632
                    cname = name+'.jpg'
1448
1633
                    open(os.path.join(base, cname), 'wb').write(cdata)
1449
1634
                    mi.cover = cname
1450
 
                opf = OPFCreator(base, mi)
1451
 
                opf.render(f)
1452
 
                f.close()
 
1635
                with open(os.path.join(base, name+'.opf'),
 
1636
                        'wb') as f:
 
1637
                    f.write(metadata_to_opf(mi))
1453
1638
 
1454
1639
                fmts = self.formats(idx, index_is_id=index_is_id)
1455
1640
                if not fmts:
1459
1644
                    if not data:
1460
1645
                        continue
1461
1646
                    fname = name +'.'+fmt.lower()
1462
 
                    fname = sanitize_file_name(fname)
1463
1647
                    f = open(os.path.join(base, fname), 'w+b')
1464
1648
                    f.write(data)
1465
1649
                    f.flush()
1471
1655
                    f.close()
1472
1656
                count += 1
1473
1657
                if callable(callback):
1474
 
                    if not callback(count, mi.title):
 
1658
                    if not callback(int(id), mi.title):
1475
1659
                        return
1476
1660
 
1477
1661
    def export_single_format_to_dir(self, dir, indices, format,
1480
1664
        if not index_is_id:
1481
1665
            indices = map(self.id, indices)
1482
1666
        failures = []
 
1667
        plen = 1000 if supports_long_names(dir) else 245
1483
1668
        for count, id in enumerate(indices):
1484
1669
            try:
1485
1670
                data = self.format(id, format, index_is_id=True)
1494
1679
            if not au:
1495
1680
                au = _('Unknown')
1496
1681
            fname = '%s - %s.%s'%(title, au, format.lower())
1497
 
            fname = sanitize_file_name(fname)
 
1682
            fname = ascii_filename(fname)
 
1683
            dir = os.path.abspath(dir)
 
1684
            fname = shorten_components_to(plen - len(dir), (fname,))[0]
1498
1685
            if not os.path.exists(dir):
1499
1686
                os.makedirs(dir)
1500
1687
            f = open(os.path.join(dir, fname), 'w+b')
1501
1688
            f.write(data)
 
1689
            f.flush()
1502
1690
            f.seek(0)
1503
1691
            try:
1504
1692
                set_metadata(f, self.get_metadata(id, index_is_id=True, get_cover=True),
1507
1695
                pass
1508
1696
            f.close()
1509
1697
            if callable(callback):
1510
 
                if not callback(count, title):
 
1698
                if not callback(int(id), title):
1511
1699
                    break
1512
1700
        return failures
1513
1701
 
1568
1756
        formats = self.find_books_in_directory(dirpath, True)
1569
1757
        if not formats:
1570
1758
            return
1571
 
 
 
1759
        formats = list(formats)
1572
1760
        mi = metadata_from_formats(formats)
1573
1761
        if mi.title is None:
1574
1762
            return
1594
1782
        return duplicates
1595
1783
 
1596
1784
 
1597