diff --git a/installer/osx/freeze.py b/installer/osx/freeze.py index 78c4cd923b..dd57b79be6 100644 --- a/installer/osx/freeze.py +++ b/installer/osx/freeze.py @@ -247,7 +247,6 @@ _check_symlinks_prescript() f.close() os.chmod(path, stat.S_IXUSR|stat.S_IXGRP|stat.S_IXOTH|stat.S_IREAD\ |stat.S_IWUSR|stat.S_IROTH|stat.S_IRGRP) - shutil.copyfile('/usr/lib/libiconv.2.dylib', os.path.join(frameworks_dir, 'libiconv.2.dylib')) self.add_plugins() @@ -266,7 +265,8 @@ _check_symlinks_prescript() print print 'Adding lxml dependencies' - subprocess.check_call('install_name_tool -id @executable_path/../Frameworks/libiconv.2.dylib '+ os.path.join(frameworks_dir, 'libiconv.2.dylib'), shell=True) + #shutil.copyfile('/usr/lib/libiconv.2.dylib', os.path.join(frameworks_dir, 'libiconv.2.dylib')) + #subprocess.check_call('install_name_tool -id @executable_path/../Frameworks/libiconv.2.dylib '+ os.path.join(frameworks_dir, 'libiconv.2.dylib'), shell=True) deps = [] for f in glob.glob(os.path.expanduser('~/libxml2/*')): tgt = os.path.join(frameworks_dir, os.path.basename(f)) diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index b0cddc6a71..2cc0cec4a7 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -66,6 +66,18 @@ class ConfigDialog(QDialog, Ui_Dialog): self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format)) self.cover_browse.setValue(config['cover_flow_queue_length']) self.confirm_delete.setChecked(config['confirm_delete']) + from calibre.translations.compiled import translations + from calibre.translations import language_codes + from calibre.startup import get_lang + lang = get_lang() + if lang is not None: + self.language.addItem(language_codes[lang], QVariant(lang)) + items = [(l, language_codes[l]) for l in translations.keys() if l != lang] + if lang != 'en': + items.append(('en', 'English')) + items.sort(cmp=lambda x, y: cmp(x[1], y[1])) + for item in items: + self.language.addItem(item[1], QVariant(item[0])) def compact(self, toggled): d = Vacuum(self, self.db) @@ -99,6 +111,7 @@ class ConfigDialog(QDialog, Ui_Dialog): prefs['filename_pattern'] = pattern config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()] config['cover_flow_queue_length'] = self.cover_browse.value() + prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) if not path or not os.path.exists(path) or not os.path.isdir(path): d = error_dialog(self, _('Invalid database location'), diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index 59fa9942d7..35b990efde 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -7,7 +7,7 @@ 0 0 709 - 685 + 687 @@ -78,14 +78,6 @@ 0 - - - 0 - 0 - 595 - 640 - - @@ -223,6 +215,19 @@ + + + + + + + Choose &language (requires restart): + + + language + + + @@ -389,14 +394,6 @@ - - - 0 - 0 - 595 - 638 - - diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 1c980b7ac1..173108afb8 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -6,7 +6,8 @@ __docformat__ = 'restructuredtext en' ''' The database used to store ebook metadata ''' -import os, re, sys, shutil, cStringIO, glob, collections +import os, re, sys, shutil, cStringIO, glob, collections, textwrap, \ + operator, itertools, functools import sqlite3 as sqlite from itertools import repeat @@ -159,6 +160,157 @@ class Concatenate(object): return self.ans[:-len(self.sep)] return self.ans +class ResultCache(object): + + ''' + Stores sorted and filtered metadata in memory. + ''' + + METHOD_MAP = { + 'title' : 'title', + 'authors' : 'author_sort', + 'author' : 'author_sort', + 'publisher' : 'publisher', + 'size' : 'size', + 'date' : 'timestamp', + 'timestamp' : 'timestamp', + 'rating' : 'rating', + 'tags' : 'tags', + 'series' : 'series', + } + + def __init__(self): + self._map = self._map_filtered = self._data = [] + + def __getitem__(self, row): + return self._data[self._map_filtered[row]] + + def __len__(self): + return len(self._map_filtered) + + def __iter__(self): + for id in self._map_filtered: + yield self._data[id] + + def remove(self, id): + self._data[id] = None + if id in self._map: + self._map.remove(id) + if id in self._map_filtered: + self._map_filtered.remove(id) + + def set(self, row, col, val): + id = self._map_filtered[row] + self._data[id][col] = val + + def index(self, id, cache=False): + x = self._map if cache else self._map_filtered + return x.index(id) + + def row(self, id): + return self.index(id) + + def refresh_ids(self, conn, ids): + for id in ids: + self._data[id] = conn.execute('SELECT * from meta WHERE id=?', (id,)).fetchone() + return map(self.row, ids) + + def refresh(self, db, field, ascending): + field = field.lower() + method = getattr(self, 'sort_on_' + self.METHOD_MAP[field]) + # Fast mapping from sorted row numbers to ids + self._map = map(operator.itemgetter(0), method('ASC' if ascending else 'DESC', db)) # Preserves sort order + # Fast mapping from sorted, filtered row numbers to ids + # At the moment it is the same as self._map + self._map_filtered = list(self._map) + temp = db.conn.execute('SELECT * FROM meta').fetchall() + # Fast mapping from ids to data. + # Can be None for ids that dont exist (i.e. have been deleted) + self._data = list(itertools.repeat(None, temp[-1][0]+2)) + for r in temp: + self._data[r[0]] = r + + def filter(self, filters, refilter=False, OR=False): + ''' + Filter data based on filters. All the filters must match for an item to + be accepted. Matching is case independent regexp matching. + @param filters: A list of SearchToken objects + @param refilter: If True filters are applied to the results of the previous + filtering. + @param OR: If True, keeps a match if any one of the filters matches. If False, + keeps a match only if all the filters match + ''' + if not refilter: + self._map_filtered = list(self._map) + if filters: + remove = [] + for id in self._map_filtered: + if OR: + keep = False + for token in filters: + if token.match(self._data[id]): + keep = True + break + if not keep: + remove.append(id) + else: + for token in filters: + if not token.match(self._data[id]): + remove.append(id) + break + for id in remove: + self._map_filtered.remove(id) + + def sort_on_title(self, order, db): + return db.conn.execute('SELECT id FROM books ORDER BY sort ' + order).fetchall() + + def sort_on_author_sort(self, order, db): + return db.conn.execute('SELECT id FROM books ORDER BY author_sort,sort ' + order).fetchall() + + def sort_on_timestamp(self, order, db): + return db.conn.execute('SELECT id FROM books ORDER BY id ' + order).fetchall() + + def sort_on_publisher(self, order, db): + no_publisher = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_publishers_link) ORDER BY books.sort').fetchall() + ans = [] + for r in db.conn.execute('SELECT id FROM publishers ORDER BY name '+order).fetchall(): + publishers_id = r[0] + ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_publishers_link WHERE publisher=?) ORDER BY books.sort '+order, (publishers_id,)).fetchall() + ans = (no_publisher + ans) if order == 'ASC' else (ans + no_publisher) + return ans + + + def sort_on_size(self, order, db): + return db.conn.execute('SELECT id FROM meta ORDER BY size ' + order).fetchall() + + def sort_on_rating(self, order, db): + no_rating = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_ratings_link) ORDER BY books.sort').fetchall() + ans = [] + for r in db.conn.execute('SELECT id FROM ratings ORDER BY rating '+order).fetchall(): + ratings_id = r[0] + ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_ratings_link WHERE rating=?) ORDER BY books.sort', (ratings_id,)).fetchall() + ans = (no_rating + ans) if order == 'ASC' else (ans + no_rating) + return ans + + + def sort_on_series(self, order, db): + no_series = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_series_link) ORDER BY books.sort').fetchall() + ans = [] + for r in db.conn.execute('SELECT id FROM series ORDER BY name '+order).fetchall(): + series_id = r[0] + ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_series_link WHERE series=?) ORDER BY books.series_index,books.id '+order, (series_id,)).fetchall() + ans = (no_series + ans) if order == 'ASC' else (ans + no_series) + return ans + + + def sort_on_tags(self, order, db): + no_tags = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_tags_link) ORDER BY books.sort').fetchall() + ans = [] + for r in db.conn.execute('SELECT id FROM tags ORDER BY name '+order).fetchall(): + tag_id = r[0] + ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_tags_link WHERE tag=?) ORDER BY books.sort '+order, (tag_id,)).fetchall() + ans = (no_tags + ans) if order == 'ASC' else (ans + no_tags) + return ans class LibraryDatabase2(LibraryDatabase): ''' @@ -210,12 +362,40 @@ class LibraryDatabase2(LibraryDatabase): if isinstance(self.dbpath, unicode): self.dbpath = self.dbpath.encode(filesystem_encoding) self.connect() + # Upgrade database + while True: + meth = getattr(self, 'upgrade_version_%d'%self.user_version, None) + if meth is None: + break + else: + print 'Upgrading database to version %d...'%(self.user_version+1) + meth() + self.conn.commit() + self.user_version += 1 + self.data = ResultCache() + self.filter = self.data.filter + self.refresh = functools.partial(self.data.refresh, self) + self.index = self.data.index + self.refresh_ids = functools.partial(self.data.refresh_ids, self.conn) + self.row = self.data.row def initialize_database(self): from calibre.resources import metadata_sqlite self.conn.executescript(metadata_sqlite) self.user_version = 1 + + def upgrade_version_1(self): + ''' + Normalize indices. + ''' + self.conn.executescript(textwrap.dedent('''\ + DROP INDEX authors_idx; + CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE, sort COLLATE NOCASE); + DROP INDEX series_idx; + CREATE INDEX series_idx ON series (name COLLATE NOCASE); + CREATE INDEX series_sort_idx ON books (series_index, id); + ''')) def path(self, index, index_is_id=False): 'Return the relative path to the directory containing this books files as a unicode string.' @@ -371,13 +551,9 @@ class LibraryDatabase2(LibraryDatabase): def delete_book(self, id): ''' - Removes book from self.cache, self.data and underlying database. + Removes book from the result cache and the underlying database. ''' - try: - self.cache.pop(self.index(id, cache=True)) - self.data.pop(self.index(id, cache=False)) - except TypeError: #If data and cache are the same object - pass + self.data.remove(id) path = os.path.join(self.library_path, self.path(id, True)) if os.path.exists(path): shutil.rmtree(path) @@ -400,6 +576,27 @@ class LibraryDatabase2(LibraryDatabase): self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format.upper())) self.conn.commit() + def set(self, row, column, val): + ''' + Convenience method for setting the title, authors, publisher or rating + ''' + id = self.data[row][0] + col = {'title':1, 'authors':2, 'publisher':3, 'rating':4, 'tags':7}[column] + + self.data.set(row, col, val) + if column == 'authors': + val = val.split('&,') + self.set_authors(id, val) + elif column == 'title': + self.set_title(id, val) + elif column == 'publisher': + self.set_publisher(id, val) + elif column == 'rating': + self.set_rating(id, val) + elif column == 'tags': + self.set_tags(id, val.split(','), append=False) + self.set_path(id, True) + def set_metadata(self, id, mi): ''' Set metadata for the book `id` from the `MetaInformation` object `mi` @@ -451,10 +648,31 @@ class LibraryDatabase2(LibraryDatabase): return self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.set_path(id, True) - + + def set_series(self, id, series): + self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) + if series: + s = self.conn.execute('SELECT id from series WHERE name=?', (series,)).fetchone() + if s: + aid = s[0] + else: + aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid + self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid)) + self.conn.commit() + row = self.row(id) + if row is not None: + self.data.set(row, 9, series) + + def set_series_index(self, id, idx): + self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id)) + self.conn.commit() + row = self.row(id) + if row is not None: + self.data.set(row, 10, idx) + def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): ''' - Add a book to the database. self.data and self.cache are not updated. + Add a book to the database. The result cache is not updated. @param paths: List of paths to book files of file-like objects ''' formats, metadata, uris = iter(formats), iter(metadata), iter(uris) diff --git a/src/calibre/startup.py b/src/calibre/startup.py index e46bc1acb9..a5d4582013 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -16,6 +16,7 @@ __builtin__.__dict__['_'] = lambda s: s from calibre.constants import iswindows, isosx, islinux, isfrozen,\ preferred_encoding from calibre.translations.msgfmt import make +from calibre.utils.config import prefs _run_once = False if not _run_once: @@ -24,6 +25,9 @@ if not _run_once: # Setup translations def get_lang(): + lang = prefs['language'] + if lang is not None: + return lang lang = locale.getdefaultlocale()[0] if lang is None and os.environ.has_key('LANG'): # Needed for OS X try: diff --git a/src/calibre/translations/__init__.py b/src/calibre/translations/__init__.py index 87e6f5c4ea..086803c12d 100644 --- a/src/calibre/translations/__init__.py +++ b/src/calibre/translations/__init__.py @@ -5,6 +5,36 @@ Manage translation of user visible strings. ''' import shutil, tarfile, re, os, subprocess, urllib2 +language_codes = { + 'aa':'Afar','ab':'Abkhazian','af':'Afrikaans','am':'Amharic','ar':'Arabic','as':'Assamese','ay':'Aymara','az':'Azerbaijani', + 'ba':'Bashkir','be':'Byelorussian','bg':'Bulgarian','bh':'Bihari','bi':'Bislama','bn':'Bengali','bo':'Tibetan','br':'Breton', + 'ca':'Catalan','co':'Corsican','cs':'Czech','cy':'Welsh', + 'da':'Danish','de':'German','dz':'Bhutani', + 'el':'Greek','en':'English','eo':'Esperanto','es':'Spanish','et':'Estonian','eu':'Basque', + 'fa':'Persian','fi':'Finnish','fj':'Fiji','fo':'Faroese','fr':'French','fy':'Frisian', + 'ga':'Irish','gd':'Scots Gaelic','gl':'Galician','gn':'Guarani','gu':'Gujarati', + 'ha':'Hausa','he':'Hebrew','hi':'Hindi','hr':'Croatian','hu':'Hungarian','hy':'Armenian', + 'ia':'Interlingua','id':'Indonesian','ie':'Interlingue','ik':'Inupiak','is':'Icelandic','it':'Italian','iu':'Inuktitut', + 'ja':'Japanese','jw':'Javanese', + 'ka':'Georgian','kk':'Kazakh','kl':'Greenlandic','km':'Cambodian','kn':'Kannada','ko':'Korean','ks':'Kashmiri','ku':'Kurdish','ky':'Kirghiz', + 'la':'Latin','ln':'Lingala','lo':'Laothian','lt':'Lithuanian','lv':'Latvian, Lettish', + 'mg':'Malagasy','mi':'Maori','mk':'Macedonian','ml':'Malayalam','mn':'Mongolian','mo':'Moldavian','mr':'Marathi','ms':'Malay','mt':'Maltese','my':'Burmese', + 'na':'Nauru','nb':'Norwegian Bokmal','nds':'German,Low','ne':'Nepali','nl':'Dutch','no':'Norwegian', + 'oc':'Occitan','om':'(Afan) Oromo','or':'Oriya', + 'pa':'Punjabi','pl':'Polish','ps':'Pashto, Pushto','pt':'Portuguese', + 'qu':'Quechua', + 'rm':'Rhaeto-Romance','rn':'Kirundi','ro':'Romanian','ru':'Russian','rw':'Kinyarwanda', + 'sa':'Sanskrit','sd':'Sindhi','sg':'Sangho','sh':'Serbo-Croatian','si':'Sinhalese','sk':'Slovak','sl':'Slovenian','sm':'Samoan','sn':'Shona','so':'Somali','sq':'Albanian','sr':'Serbian','ss':'Siswati','st':'Sesotho','su':'Sundanese','sv':'Swedish','sw':'Swahili', + 'ta':'Tamil','te':'Telugu','tg':'Tajik','th':'Thai','ti':'Tigrinya','tk':'Turkmen','tl':'Tagalog','tn':'Setswana','to':'Tonga','tr':'Turkish','ts':'Tsonga','tt':'Tatar','tw':'Twi', + 'ug':'Uighur','uk':'Ukrainian','ur':'Urdu','uz':'Uzbek', + 'vi':'Vietnamese','vo':'Volapuk', + 'wo':'Wolof', + 'xh':'Xhosa', + 'yi':'Yiddish','yo':'Yoruba', + 'za':'Zhuang','zh':'Chinese','zu':'Zulu' +} + + def import_from_launchpad(url): f = open('/tmp/launchpad_export.tar.gz', 'wb') shutil.copyfileobj(urllib2.urlopen(url), f) diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 1ae4afc15c..184aea604a 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -492,6 +492,8 @@ def _prefs(): help=_('Default timeout for network operations (seconds)')) c.add_opt('library_path', default=None, help=_('Path to directory in which your library of books is stored')) + c.add_opt('language', default=None, + help=_('The language in which to display the user interface')) c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c