88import os
99import pathlib
1010import threading
11+ from datetime import datetime
1112from typing import (
1213 TYPE_CHECKING ,
1314 Optional ,
@@ -79,6 +80,7 @@ def terminate():
7980class _DataManager :
8081 _cacheLatestFilename : str = "_cachedLatestAddons.json"
8182 _cacheCompatibleFilename : str = "_cachedCompatibleAddons.json"
83+ _cacheCompatibleOldFilename : str = "_cachedCompatibleAddons-old.json"
8284 _downloadsPendingInstall : Set [Tuple ["AddonListItemVM[_AddonStoreModel]" , os .PathLike ]] = set ()
8385 _downloadsPendingCompletion : Set ["AddonListItemVM[_AddonStoreModel]" ] = set ()
8486
@@ -87,6 +89,9 @@ def __init__(self):
8789 self ._preferredChannel = Channel .ALL
8890 self ._cacheLatestFile = os .path .join (WritePaths .addonStoreDir , _DataManager ._cacheLatestFilename )
8991 self ._cacheCompatibleFile = os .path .join (WritePaths .addonStoreDir , _DataManager ._cacheCompatibleFilename )
92+ self ._cacheCompatibleOldFile = os .path .join (
93+ WritePaths .addonStoreDir , _DataManager ._cacheCompatibleOldFilename
94+ )
9095 self ._installedAddonDataCacheDir = WritePaths .addonsDir
9196
9297 if NVDAState .shouldWriteToDisk ():
@@ -96,6 +101,7 @@ def __init__(self):
96101
97102 self ._latestAddonCache = self ._getCachedAddonData (self ._cacheLatestFile )
98103 self ._compatibleAddonCache = self ._getCachedAddonData (self ._cacheCompatibleFile )
104+ self ._oldAddonCache = self ._getCachedAddonData (self ._cacheCompatibleOldFile )
99105 self ._installedAddonsCache = _InstalledAddonsCache ()
100106 # Fetch available add-ons cache early
101107 self ._initialiseAvailableAddonsThread = threading .Thread (
@@ -110,6 +116,8 @@ def terminate(self):
110116 self ._initialiseAvailableAddonsThread .join (timeout = 1 )
111117 if self ._initialiseAvailableAddonsThread .is_alive ():
112118 log .debugWarning ("initialiseAvailableAddons thread did not terminate immediately" )
119+ if self ._shouldCacheCompatibleAddonsBackup ():
120+ self ._cacheCompatibleAddonsBackup ()
113121
114122 def _getLatestAddonsDataForVersion (self , apiVersion : str ) -> Optional [bytes ]:
115123 url = _getAddonStoreURL (self ._preferredChannel , self ._lang , apiVersion )
@@ -158,6 +166,33 @@ def _cacheCompatibleAddons(self, addonData: str, cacheHash: Optional[str]):
158166 with open (self ._cacheCompatibleFile , 'w' , encoding = 'utf-8' ) as cacheFile :
159167 json .dump (cacheData , cacheFile , ensure_ascii = False )
160168
169+ def _shouldCacheCompatibleAddonsBackup (self ) -> bool :
170+ if not os .path .exists (self ._cacheCompatibleOldFile ):
171+ return True
172+ resetNewAddons = config .conf ["addonStore" ]["resetNewAddons" ]
173+ if resetNewAddons == "startup" :
174+ return True
175+ lastBackupTime = os .path .getmtime (self ._cacheCompatibleOldFile )
176+ lastBackupDate = datetime .fromtimestamp (lastBackupTime )
177+ nowDate = datetime .now ()
178+ diffDate = nowDate - lastBackupDate
179+ if resetNewAddons == "weekly" and diffDate .days >= 7 :
180+ return True
181+ if resetNewAddons == "monthly" and diffDate .days >= 30 :
182+ return True
183+ return False
184+
185+ def _cacheCompatibleAddonsBackup (self ):
186+ if not NVDAState .shouldWriteToDisk ():
187+ return
188+ try :
189+ with open (self ._cacheCompatibleFile , 'r' , encoding = 'utf-8' ) as cacheFile :
190+ cacheData = json .load (cacheFile )
191+ except Exception :
192+ log .exception ("Invalid add-on store cache" )
193+ with open (self ._cacheCompatibleOldFile , 'w' , encoding = 'utf-8' ) as cacheFile :
194+ json .dump (cacheData , cacheFile , ensure_ascii = False )
195+
161196 def _cacheLatestAddons (self , addonData : str , cacheHash : Optional [str ]):
162197 if not NVDAState .shouldWriteToDisk ():
163198 return
@@ -281,6 +316,26 @@ def getLatestAddons(
281316 return _createAddonGUICollection ()
282317 return deepcopy (self ._latestAddonCache .cachedAddonData )
283318
319+ def _checkForNewAddons (self ) -> bool :
320+ oldAddons = self ._getOldAddons ()
321+ compatibleAddons = self .getLatestCompatibleAddons ()
322+ installedAddons = self ._installedAddonsCache ._get_installedAddons ()
323+ for channel in compatibleAddons :
324+ for addonId in compatibleAddons [channel ]:
325+ compatibleAddon = compatibleAddons [channel ][addonId ]
326+ if (
327+ addonId not in oldAddons [channel ]
328+ or compatibleAddon .addonVersionNumber != oldAddons [channel ][addonId ].addonVersionNumber
329+ ) and addonId not in installedAddons :
330+ return True
331+ return False
332+
333+ def _getOldAddons (self ) -> "AddonGUICollectionT" :
334+ if self ._oldAddonCache is None :
335+ return _createAddonGUICollection ()
336+ oldAddons = deepcopy (self ._oldAddonCache .cachedAddonData )
337+ return oldAddons
338+
284339 def _deleteCacheInstalledAddon (self , addonId : str ):
285340 addonCachePath = os .path .join (self ._installedAddonDataCacheDir , f"{ addonId } .json" )
286341 if pathlib .Path (addonCachePath ).exists ():
0 commit comments