Files
jellycon/resources/lib/datamanager.py
mani 30a07327bc
Some checks failed
Build JellyCon / build (py2) (push) Has been cancelled
Build JellyCon / build (py3) (push) Has been cancelled
CodeQL Analysis / analyze (python, 3.9) (push) Has been cancelled
Release Drafter / Update release draft (push) Has been cancelled
Test JellyCon / test (3.9) (push) Has been cancelled
Add option to disable disk caching (RAM only mode)
Adds new advanced setting to disable disk-based caching, keeping data only in RAM. When enabled, prevents writing .pickle cache files and disables artwork preloading. Significantly reduces SD card wear on devices like Raspberry Pi while maintaining performance through RAM caching.
2026-01-05 18:50:26 +01:00

329 lines
12 KiB
Python

from __future__ import (
division, absolute_import, print_function, unicode_literals
)
import threading
import hashlib
import os
import time
import xbmc
import xbmcaddon
import xbmcvfs
import xbmcgui
from six.moves import cPickle
from .jellyfin import api
from .lazylogger import LazyLogger
from .item_functions import extract_item_info
from .kodi_utils import HomeWindow
from .tracking import timer
from .filelock import FileLock
from .utils import translate_string, load_user_details, translate_path
log = LazyLogger(__name__)
class CacheItem:
item_list = None
item_list_hash = None
date_saved = None
date_last_used = None
last_action = None
items_url = None
file_path = None
user_id = None
def __init__(self, *args):
pass
class DataManager:
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
def __init__(self, *args):
self.user_details = load_user_details()
self.api = api
@timer
def get_items(self, url, gui_options, use_cache=False):
home_window = HomeWindow()
log.debug("last_content_url : use_cache={0} url={1}".format(use_cache, url))
home_window.set_property("last_content_url", url)
# Check if disk caching is disabled
settings = xbmcaddon.Addon()
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
if disable_disk_cache:
use_cache = False
log.debug("Disk caching disabled - data will be kept in RAM only")
user_id = self.user_details.get('user_id')
server = self.api.server
m = hashlib.md5()
m.update('{}|{}|{}'.format(user_id, server, url).encode())
url_hash = m.hexdigest()
cache_file = os.path.join(self.addon_dir, "cache_" + url_hash + ".pickle")
item_list = None
total_records = 0
baseline_name = None
cache_thread = CacheManagerThread()
cache_thread.gui_options = gui_options
home_window.set_property(cache_file, "true")
clear_cache = home_window.get_property("skip_cache_for_" + url)
if clear_cache and os.path.isfile(cache_file):
log.debug("Clearing cache data and loading new data")
home_window.clear_property("skip_cache_for_" + url)
os.remove(cache_file)
# try to load the list item data from the cache
if os.path.isfile(cache_file) and use_cache:
log.debug("Loading url data from cached pickle data")
with FileLock(cache_file + ".locked", timeout=5):
with open(cache_file, 'rb') as handle:
try:
cache_item = cPickle.load(handle)
cache_thread.cached_item = cache_item
item_list = cache_item.item_list
total_records = cache_item.total_records
except Exception as err:
log.error("Pickle Data Load Failed : {0}".format(err))
item_list = None
# we need to load the list item data form the server
if item_list is None or len(item_list) == 0:
log.debug("Loading url data from server")
results = self.api.get(url)
if results is None:
results = []
if isinstance(results, dict):
total_records = results.get("TotalRecordCount", 0)
if isinstance(results, dict) and results.get("Items") is not None:
baseline_name = results.get("BaselineItemName")
results = results.get("Items", [])
elif isinstance(results, list) and len(results) > 0 and results[0].get("Items") is not None:
baseline_name = results[0].get("BaselineItemName")
results = results[0].get("Items")
item_list = []
for item in results:
item_data = extract_item_info(item, gui_options)
item_data.baseline_itemname = baseline_name
item_list.append(item_data)
cache_item = CacheItem()
cache_item.item_list = item_list
cache_item.file_path = cache_file
cache_item.items_url = url
cache_item.user_id = user_id
cache_item.last_action = "fresh_data"
cache_item.date_saved = time.time()
cache_item.date_last_used = time.time()
cache_item.total_records = total_records
cache_thread.cached_item = cache_item
if not use_cache:
cache_thread = None
return cache_file, item_list, total_records, cache_thread
class CacheManagerThread(threading.Thread):
cached_item = None
gui_options = None
def __init__(self, *args):
threading.Thread.__init__(self, *args)
@staticmethod
def get_data_hash(items):
m = hashlib.md5()
for item in items:
item_string = "%s_%s_%s_%s_%s_%s" % (
item.name,
item.play_count,
item.favorite,
item.resume_time,
item.recursive_unplayed_items_count,
item.etag
)
item_string = item_string.encode("UTF-8")
m.update(item_string)
return m.hexdigest()
def run(self):
log.debug("CacheManagerThread : Started")
home_window = HomeWindow()
settings = xbmcaddon.Addon()
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
if disable_disk_cache:
log.debug("CacheManagerThread : Disk caching disabled, skipping cache operations")
return
is_fresh = False
# if the data is fresh then just save it
# if the data is to old do a reload
if (self.cached_item.date_saved is not None
and (time.time() - self.cached_item.date_saved) < 20
and self.cached_item.last_action == "fresh_data"):
is_fresh = True
if is_fresh and self.cached_item.item_list is not None and len(self.cached_item.item_list) > 0:
log.debug("CacheManagerThread : Saving fresh data")
cached_hash = self.get_data_hash(self.cached_item.item_list)
self.cached_item.item_list_hash = cached_hash
self.cached_item.last_action = "cached_data"
self.cached_item.date_saved = time.time()
self.cached_item.date_last_used = time.time()
log.debug("CacheManagerThread : Saving New Data loops")
with FileLock(self.cached_item.file_path + ".locked", timeout=5):
with open(self.cached_item.file_path, 'wb') as handle:
cPickle.dump(self.cached_item, handle, protocol=cPickle.HIGHEST_PROTOCOL)
else:
log.debug("CacheManagerThread : Reloading to recheck data hashes")
cached_hash = self.cached_item.item_list_hash
log.debug("CacheManagerThread : Cache Hash : {0}".format(cached_hash))
data_manager = DataManager()
results = data_manager.api.get(self.cached_item.items_url)
if results is None:
results = []
if isinstance(results, dict) and results.get("Items") is not None:
results = results.get("Items", [])
elif isinstance(results, list) and len(results) > 0 and results[0].get("Items") is not None:
results = results[0].get("Items")
total_records = 0
if isinstance(results, dict):
total_records = results.get("TotalRecordCount", 0)
loaded_items = []
for item in results:
item_data = extract_item_info(item, self.gui_options)
loaded_items.append(item_data)
if loaded_items is None or len(loaded_items) == 0:
log.debug("CacheManagerThread : loaded_items is None or Empty so not saving it")
return
loaded_hash = self.get_data_hash(loaded_items)
log.debug("CacheManagerThread : Loaded Hash : {0}".format(loaded_hash))
# if they dont match then save the data and trigger a content reload
if cached_hash != loaded_hash:
log.debug("CacheManagerThread : Hashes different, saving new data and reloading container")
self.cached_item.item_list = loaded_items
self.cached_item.item_list_hash = loaded_hash
self.cached_item.last_action = "fresh_data"
self.cached_item.date_saved = time.time()
self.cached_item.date_last_used = time.time()
self.cached_item.total_records = total_records
with FileLock(self.cached_item.file_path + ".locked", timeout=5):
with open(self.cached_item.file_path, 'wb') as handle:
cPickle.dump(self.cached_item, handle, protocol=cPickle.HIGHEST_PROTOCOL)
# TODO: probably should only set this in simple check mode
current_time_stamp = str(time.time())
home_window.set_property("jellycon_widget_reload", current_time_stamp)
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
log.debug("CacheManagerThread : Sending container refresh")
xbmc.executebuiltin("Container.Refresh")
else:
self.cached_item.date_last_used = time.time()
with FileLock(self.cached_item.file_path + ".locked", timeout=5):
with open(self.cached_item.file_path, 'wb') as handle:
cPickle.dump(self.cached_item, handle, protocol=cPickle.HIGHEST_PROTOCOL)
log.debug("CacheManagerThread : Updating last used date for cache data")
log.debug("CacheManagerThread : Exited")
def clear_cached_server_data():
log.debug("clear_cached_server_data() called")
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
dirs, files = xbmcvfs.listdir(addon_dir)
del_count = 0
for filename in files:
if filename.startswith("cache_") and filename.endswith(".pickle"):
log.debug("Deleting CacheFile: {0}".format(filename))
xbmcvfs.delete(os.path.join(addon_dir, filename))
del_count += 1
log.debug('Deleted {} files'.format(del_count))
msg = translate_string(30394)
xbmcgui.Dialog().ok(translate_string(30393), msg)
def clear_old_cache_data():
log.debug("clear_old_cache_data() : called")
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
dirs, files = xbmcvfs.listdir(addon_dir)
del_count = 0
for filename in files:
if filename.startswith("cache_") and filename.endswith(".pickle"):
log.debug("clear_old_cache_data() : Checking CacheFile : {0}".format(filename))
cache_item = None
for x in range(0, 5):
try:
data_file = os.path.join(addon_dir, filename)
with FileLock(data_file + ".locked", timeout=5):
with open(data_file, 'rb') as handle:
cache_item = cPickle.load(handle)
break
except Exception as error:
log.debug("clear_old_cache_data() : Pickle load error : {0}".format(error))
cache_item = None
xbmc.sleep(1000)
if cache_item is not None:
item_last_used = -1
if cache_item.date_last_used is not None:
item_last_used = time.time() - cache_item.date_last_used
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago".format(item_last_used))
if item_last_used == -1 or item_last_used > (3600 * 24 * 7):
log.debug("clear_old_cache_data() : Deleting cache item age : {0}".format(item_last_used))
data_file = os.path.join(addon_dir, filename)
with FileLock(data_file + ".locked", timeout=5):
xbmcvfs.delete(data_file)
del_count += 1
else:
log.debug("clear_old_cache_data() : Deleting unloadable cache item")
data_file = os.path.join(addon_dir, filename)
with FileLock(data_file + ".locked", timeout=5):
xbmcvfs.delete(data_file)
log.debug("clear_old_cache_data() : Cache items deleted : {0}".format(del_count))