# -*- coding: utf-8 -*- import os import sqlite3 from concurrent.futures import ThreadPoolExecutor import tornado.concurrent import tornado.gen import tornado.ioloop import tornado.web from tornado.concurrent import run_on_executor import ox from settings import static_path from utils import resize_image, is_svg import db import logging logger = logging.getLogger(__name__) MAX_WORKERS = 4 class Icons(dict): def __init__(self, db): self._db = db self.create() def connect(self): conn = sqlite3.connect(self._db, timeout=90) return conn def create(self): conn = self.connect() c = conn.cursor() c.execute('CREATE TABLE IF NOT EXISTS icon (id varchar(64) unique, data blob)') c.execute('CREATE TABLE IF NOT EXISTS setting (key varchar(256) unique, value text)') if int(self.get_setting(c, 'version', 0)) < 1: self.set_setting(c, 'version', 1) def get_setting(self, c, key, default=None): c.execute('SELECT value FROM setting WHERE key = ?', (key, )) for row in c: return row[0] return default def set_setting(self, c, key, value): c.execute('INSERT OR REPLACE INTO setting values (?, ?)', (key, str(value))) def default_cover(self): with open(os.path.join(static_path, 'png', 'cover.png'), 'rb') as f: data = f.read() return data def __getitem__(self, id, default=None): sql = 'SELECT data FROM icon WHERE id=?' conn = self.connect() c = conn.cursor() c.execute(sql, (id, )) data = default for row in c: data = row[0] break c.close() conn.close() return data def __setitem__(self, id, data): sql = 'INSERT OR REPLACE INTO icon values (?, ?)' try: conn = self.connect() c = conn.cursor() data = sqlite3.Binary(data) c.execute(sql, (id, data)) conn.commit() c.close() conn.close() except: logger.debug('failed to insert icon %s (%s)', id, self._db, exc_info=True) def __delitem__(self, id): sql = 'DELETE FROM icon WHERE id = ?' try: conn = self.connect() c = conn.cursor() c.execute(sql, (id, )) conn.commit() c.close() conn.close() except: logger.debug('failed to delete icon %s (%s)', id, self._db) def clear(self, prefix): try: conn = self.connect() c = conn.cursor() sql = 'DELETE FROM icon WHERE id = ?' for size in (64, 128, 256, 512, 1024): id = '%s%s' % (prefix, size) c.execute(sql, (id, )) conn.commit() c.close() conn.close() except: logger.debug('failed to clear icon %s', prefix) def vacuum(self, ids): conn = self.connect() c = conn.cursor() sql = 'SELECT id from icon' c.execute(sql) icons = [row[0] for row in c] sql = 'DELETE FROM icon WHERE id = ?' for i in icons: id = i.split(':')[1] if id not in ids: c.execute(sql, (id, )) conn.commit() sql = 'VACUUM' c.execute(sql) conn.commit() c.close() conn.close() def get_icons_db_path(): import settings import shutil library = os.path.expanduser(settings.preferences['libraryPath']) metadata = os.path.join(library, 'Metadata') icons_db_path = os.path.join(metadata, 'icons.db') if os.path.exists(os.path.dirname(library)): ox.makedirs(metadata) old_icons_db_path = os.path.join(settings.data_path, 'icons.db') if not os.path.exists(icons_db_path) and os.path.exists(old_icons_db_path): shutil.move(old_icons_db_path, icons_db_path) return icons_db_path def get_icon_sync(id, type_, size): if size: skey = '%s:%s:%s' % (type_, id, size) data = icons[skey] if data: return bytes(data) key = '%s:%s' % (type_, id) data = icons[key] if is_svg(data): return bytes(data) if not data: type_ = 'preview' if type_ == 'cover' else 'cover' key = '%s:%s' % (type_, id) if size: skey = '%s:%s:%s' % (type_, id, size) if size: data = icons[skey] if data: size = None if not data: data = icons[key] if not data: skey = '%s:%s:%s' % ('default', 'cover', size) if size: data = icons[skey] if data: size = None if not data: data = icons.default_cover() if size: try: data = resize_image(data, size=size) except: logger.debug('failed to resize default cover %s %s %s', id, size, skey) data = None if data: icons[skey] = data size = None if size: try: data = resize_image(data, size=size) except: logger.debug('failed to resize %s %s %s', id, size, skey) data = None if data: icons[skey] = data if data: data = bytes(data) else: data = '' return data def clear_default_cover_cache(): icons.clear('default:cover:') class IconHandler(tornado.web.RequestHandler): executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) def initialize(self): pass @run_on_executor def get_icon(self, id, type_, size): return get_icon_sync(id, type_, size) @tornado.gen.coroutine def get(self, id, type_, size=None): size = int(size) if size else None if type_ not in ('cover', 'preview'): self.set_status(404) return self.set_header('Content-Type', 'image/jpeg') response = yield self.get_icon(id, type_, size) if not response: self.set_status(404) return if self._finished: return self.write(response) icons = Icons(get_icons_db_path())