256 lines
8.2 KiB
Python
256 lines
8.2 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""
|
||
|
|
||
|
Copyright (C) 2014-2016 bromix (plugin.video.youtube)
|
||
|
Copyright (C) 2016-2019 plugin.video.youtube
|
||
|
|
||
|
SPDX-License-Identifier: GPL-2.0-only
|
||
|
See LICENSES/GPL-2.0-only for more information.
|
||
|
"""
|
||
|
|
||
|
from six import PY2
|
||
|
from six.moves import range
|
||
|
# noinspection PyPep8Naming
|
||
|
from six.moves import cPickle as pickle
|
||
|
|
||
|
import datetime
|
||
|
import os
|
||
|
import sqlite3
|
||
|
import time
|
||
|
import traceback
|
||
|
|
||
|
from .. import logger
|
||
|
|
||
|
|
||
|
class Storage(object):
|
||
|
def __init__(self, filename, max_item_count=0, max_file_size_kb=-1):
|
||
|
self._table_name = 'storage'
|
||
|
self._filename = filename
|
||
|
if not self._filename.endswith('.sqlite'):
|
||
|
self._filename = ''.join([self._filename, '.sqlite'])
|
||
|
self._file = None
|
||
|
self._cursor = None
|
||
|
self._max_item_count = max_item_count
|
||
|
self._max_file_size_kb = max_file_size_kb
|
||
|
|
||
|
self._table_created = False
|
||
|
self._needs_commit = False
|
||
|
|
||
|
def set_max_item_count(self, max_item_count):
|
||
|
self._max_item_count = max_item_count
|
||
|
|
||
|
def set_max_file_size_kb(self, max_file_size_kb):
|
||
|
self._max_file_size_kb = max_file_size_kb
|
||
|
|
||
|
def __del__(self):
|
||
|
self._close()
|
||
|
|
||
|
def _open(self):
|
||
|
if self._file is None:
|
||
|
self._optimize_file_size()
|
||
|
|
||
|
path = os.path.dirname(self._filename)
|
||
|
if not os.path.exists(path):
|
||
|
os.makedirs(path)
|
||
|
|
||
|
self._file = sqlite3.connect(self._filename, check_same_thread=False,
|
||
|
detect_types=0, timeout=1)
|
||
|
|
||
|
self._file.isolation_level = None
|
||
|
self._cursor = self._file.cursor()
|
||
|
self._cursor.execute('PRAGMA journal_mode=MEMORY')
|
||
|
self._cursor.execute('PRAGMA busy_timeout=20000')
|
||
|
# self._cursor.execute('PRAGMA synchronous=OFF')
|
||
|
self._create_table()
|
||
|
|
||
|
def _execute(self, needs_commit, query, values=None):
|
||
|
if values is None:
|
||
|
values = []
|
||
|
if not self._needs_commit and needs_commit:
|
||
|
self._needs_commit = True
|
||
|
self._cursor.execute('BEGIN')
|
||
|
|
||
|
"""
|
||
|
Tests revealed that sqlite has problems to release the database in time. This happens no so often, but just to
|
||
|
be sure, we try at least 3 times to execute out statement.
|
||
|
"""
|
||
|
for tries in range(3):
|
||
|
try:
|
||
|
return self._cursor.execute(query, values)
|
||
|
except TypeError:
|
||
|
return None
|
||
|
except:
|
||
|
time.sleep(0.1)
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def _close(self):
|
||
|
if self._file is not None:
|
||
|
self.sync()
|
||
|
self._file.commit()
|
||
|
self._cursor.close()
|
||
|
self._cursor = None
|
||
|
self._file.close()
|
||
|
self._file = None
|
||
|
|
||
|
def _optimize_file_size(self):
|
||
|
# do nothing - only we have given a size
|
||
|
if self._max_file_size_kb <= 0:
|
||
|
return
|
||
|
|
||
|
# do nothing - only if this folder exists
|
||
|
path = os.path.dirname(self._filename)
|
||
|
if not os.path.exists(path):
|
||
|
return
|
||
|
|
||
|
if not os.path.exists(self._filename):
|
||
|
return
|
||
|
|
||
|
try:
|
||
|
file_size_kb = (os.path.getsize(self._filename) // 1024)
|
||
|
if file_size_kb >= self._max_file_size_kb:
|
||
|
os.remove(self._filename)
|
||
|
except OSError:
|
||
|
pass
|
||
|
|
||
|
def _create_table(self):
|
||
|
self._open()
|
||
|
if not self._table_created:
|
||
|
query = 'CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, time TIMESTAMP, value BLOB)' % self._table_name
|
||
|
self._execute(True, query)
|
||
|
self._table_created = True
|
||
|
|
||
|
def sync(self):
|
||
|
if self._cursor is not None and self._needs_commit:
|
||
|
self._needs_commit = False
|
||
|
return self._execute(False, 'COMMIT')
|
||
|
|
||
|
def _set(self, item_id, item):
|
||
|
def _encode(obj):
|
||
|
return sqlite3.Binary(pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL))
|
||
|
|
||
|
if self._max_file_size_kb < 1 and self._max_item_count < 1:
|
||
|
self._optimize_item_count()
|
||
|
else:
|
||
|
self._open()
|
||
|
now = datetime.datetime.now() + datetime.timedelta(microseconds=1) # add 1 microsecond, required for dbapi2
|
||
|
query = 'REPLACE INTO %s (key,time,value) VALUES(?,?,?)' % self._table_name
|
||
|
self._execute(True, query, values=[item_id, now, _encode(item)])
|
||
|
self._close()
|
||
|
self._optimize_item_count()
|
||
|
|
||
|
def _optimize_item_count(self):
|
||
|
if self._max_item_count < 1:
|
||
|
if not self._is_empty():
|
||
|
self._clear()
|
||
|
else:
|
||
|
self._open()
|
||
|
query = 'SELECT key FROM %s ORDER BY time DESC LIMIT -1 OFFSET %d' % (self._table_name, self._max_item_count)
|
||
|
result = self._execute(False, query)
|
||
|
if result is not None:
|
||
|
for item in result:
|
||
|
self._remove(item[0])
|
||
|
self._close()
|
||
|
|
||
|
def _clear(self):
|
||
|
self._open()
|
||
|
query = 'DELETE FROM %s' % self._table_name
|
||
|
self._execute(True, query)
|
||
|
self._create_table()
|
||
|
self._close()
|
||
|
self._open()
|
||
|
self._execute(False, 'VACUUM')
|
||
|
self._close()
|
||
|
|
||
|
def _is_empty(self):
|
||
|
self._open()
|
||
|
query = 'SELECT exists(SELECT 1 FROM %s LIMIT 1);' % self._table_name
|
||
|
result = self._execute(False, query)
|
||
|
is_empty = True
|
||
|
if result is not None:
|
||
|
for item in result:
|
||
|
is_empty = item[0] == 0
|
||
|
break
|
||
|
self._close()
|
||
|
return is_empty
|
||
|
|
||
|
def _get_ids(self, oldest_first=True):
|
||
|
self._open()
|
||
|
# self.sync()
|
||
|
query = 'SELECT key FROM %s' % self._table_name
|
||
|
if oldest_first:
|
||
|
query = '%s ORDER BY time ASC' % query
|
||
|
else:
|
||
|
query = '%s ORDER BY time DESC' % query
|
||
|
|
||
|
query_result = self._execute(False, query)
|
||
|
|
||
|
result = []
|
||
|
if query_result:
|
||
|
for item in query_result:
|
||
|
result.append(item[0])
|
||
|
|
||
|
self._close()
|
||
|
return result
|
||
|
|
||
|
def _get(self, item_id):
|
||
|
def _decode(obj):
|
||
|
if PY2:
|
||
|
obj = str(obj)
|
||
|
return pickle.loads(obj)
|
||
|
|
||
|
self._open()
|
||
|
query = 'SELECT time, value FROM %s WHERE key=?' % self._table_name
|
||
|
result = self._execute(False, query, [item_id])
|
||
|
if result is None:
|
||
|
self._close()
|
||
|
return None
|
||
|
|
||
|
item = result.fetchone()
|
||
|
if item is None:
|
||
|
self._close()
|
||
|
return None
|
||
|
|
||
|
self._close()
|
||
|
return _decode(item[1]), item[0]
|
||
|
|
||
|
def _remove(self, item_id):
|
||
|
self._open()
|
||
|
query = 'DELETE FROM %s WHERE key = ?' % self._table_name
|
||
|
self._execute(True, query, [item_id])
|
||
|
|
||
|
@staticmethod
|
||
|
def strptime(stamp, stamp_fmt):
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
import _strptime
|
||
|
try:
|
||
|
time.strptime('01 01 2012', '%d %m %Y') # dummy call
|
||
|
except:
|
||
|
pass
|
||
|
return time.strptime(stamp, stamp_fmt)
|
||
|
|
||
|
def get_seconds_diff(self, current_stamp):
|
||
|
stamp_format = '%Y-%m-%d %H:%M:%S.%f'
|
||
|
current_datetime = datetime.datetime.now()
|
||
|
if not current_stamp:
|
||
|
return 86400 # 24 hrs
|
||
|
try:
|
||
|
stamp_datetime = datetime.datetime(*(self.strptime(current_stamp, stamp_format)[0:6]))
|
||
|
except ValueError: # current_stamp has no microseconds
|
||
|
stamp_format = '%Y-%m-%d %H:%M:%S'
|
||
|
stamp_datetime = datetime.datetime(*(self.strptime(current_stamp, stamp_format)[0:6]))
|
||
|
except TypeError:
|
||
|
logger.log_error('Exception while calculating timestamp difference: '
|
||
|
'current_stamp |{cs}|{cst}| stamp_format |{sf}|{sft}| \n{tb}'
|
||
|
.format(cs=current_stamp, cst=type(current_stamp),
|
||
|
sf=stamp_format, sft=type(stamp_format),
|
||
|
tb=traceback.print_exc())
|
||
|
)
|
||
|
return 604800 # one week
|
||
|
|
||
|
time_delta = current_datetime - stamp_datetime
|
||
|
total_seconds = 0
|
||
|
if time_delta:
|
||
|
total_seconds = ((time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) // (10 ** 6)
|
||
|
return total_seconds
|