From be5382cd1ed78c9a1642533767c9e2034189acf5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 13 Jan 2026 18:26:51 +0000 Subject: [PATCH] Fix: Config initialization and category fallback (#442) - Added more robust config directory initialisation and file creation - Fixed category fallback not triggering correctly for one content type if another is cached --- shelfmark/core/settings_registry.py | 71 ++++++++++++++++++++ src/frontend/src/components/ReleaseModal.tsx | 22 +++--- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/shelfmark/core/settings_registry.py b/shelfmark/core/settings_registry.py index 350cb2fa..398b0f00 100644 --- a/shelfmark/core/settings_registry.py +++ b/shelfmark/core/settings_registry.py @@ -284,7 +284,78 @@ def save_config_file(tab_name: str, values: Dict[str, Any]) -> bool: return False +def initialize_default_configs() -> bool: + """Initialize config files with default values on first startup. + + Creates config files for all settings tabs that don't have one yet, + populating them with field default values. This ensures config files + exist from first startup rather than only being created on explicit save. + + Returns: + True if initialization succeeded or was skipped (already initialized), + False if there was an error accessing the config directory. + """ + try: + config_dir = _get_config_dir() + + # Check if config directory exists and is writable + if not config_dir.exists(): + logger.warning(f"Config directory does not exist: {config_dir}") + return False + + # Test writability + test_file = config_dir / ".write_test" + try: + test_file.touch() + test_file.unlink() + except (OSError, PermissionError) as e: + logger.warning(f"Config directory is not writable: {config_dir} - {e}") + return False + + initialized_tabs = [] + + for tab in get_all_settings_tabs(): + config_path = _get_config_file_path(tab.name) + + # Skip if config file already exists + if config_path.exists(): + continue + + # Collect default values for all fields + defaults = {} + for field in tab.fields: + # Skip non-value fields + if isinstance(field, (ActionButton, HeadingField)): + continue + + # Only include fields that have a non-None default + if field.default is not None: + defaults[field.key] = field.default + + # Create config file with defaults if we have any + if defaults: + _ensure_config_dir(tab.name) + try: + with open(config_path, 'w') as f: + json.dump(defaults, f, indent=2) + initialized_tabs.append(tab.name) + except Exception as e: + logger.error(f"Failed to initialize config for {tab.name}: {e}") + + if initialized_tabs: + logger.info(f"Initialized default configs for: {initialized_tabs}") + + return True + + except Exception as e: + logger.error(f"Error during config initialization: {e}") + return False + + def sync_env_to_config() -> None: + # Initialize default configs first (for fresh installs) + initialize_default_configs() + for tab in get_all_settings_tabs(): values_to_sync = {} diff --git a/src/frontend/src/components/ReleaseModal.tsx b/src/frontend/src/components/ReleaseModal.tsx index ed5d31f6..49d1ac5d 100644 --- a/src/frontend/src/components/ReleaseModal.tsx +++ b/src/frontend/src/components/ReleaseModal.tsx @@ -12,20 +12,20 @@ import { LanguageMultiSelect } from './LanguageMultiSelect'; import { LANGUAGE_OPTION_ALL, LANGUAGE_OPTION_DEFAULT, getLanguageFilterValues, releaseLanguageMatchesFilter } from '../utils/languageFilters'; // Module-level cache for release search results -// Key format: `${provider}:${provider_id}:${source}` +// Key format: `${provider}:${provider_id}:${source}:${contentType}` // This persists across modal open/close cycles const releaseCache = new Map(); -function getCacheKey(provider: string, providerId: string, source: string): string { - return `${provider}:${providerId}:${source}`; +function getCacheKey(provider: string, providerId: string, source: string, contentType: string): string { + return `${provider}:${providerId}:${source}:${contentType}`; } // Default cache TTL (5 minutes) - sources can override via column_config.cache_ttl_seconds const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; const cacheTimestamps = new Map(); -function getCachedReleases(provider: string, providerId: string, source: string): ReleasesResponse | null { - const key = getCacheKey(provider, providerId, source); +function getCachedReleases(provider: string, providerId: string, source: string, contentType: string): ReleasesResponse | null { + const key = getCacheKey(provider, providerId, source, contentType); const timestamp = cacheTimestamps.get(key); const cached = releaseCache.get(key); @@ -49,8 +49,8 @@ function getCachedReleases(provider: string, providerId: string, source: string) return null; } -function setCachedReleases(provider: string, providerId: string, source: string, data: ReleasesResponse): void { - const key = getCacheKey(provider, providerId, source); +function setCachedReleases(provider: string, providerId: string, source: string, contentType: string, data: ReleasesResponse): void { + const key = getCacheKey(provider, providerId, source, contentType); releaseCache.set(key, data); cacheTimestamps.set(key, Date.now()); } @@ -833,7 +833,7 @@ export const ReleaseModal = ({ if (releasesBySource[activeTab] !== undefined || loadingBySource[activeTab] || errorBySource[activeTab]) return; // Check module-level cache first - const cached = getCachedReleases(provider, bookId, activeTab); + const cached = getCachedReleases(provider, bookId, activeTab, contentType); if (cached) { setReleasesBySource((prev) => ({ ...prev, [activeTab]: cached })); return; @@ -845,7 +845,7 @@ export const ReleaseModal = ({ try { const response = await getReleases(provider, bookId, activeTab, book.title, book.author, undefined, undefined, contentType); - setCachedReleases(provider, bookId, activeTab, response); + setCachedReleases(provider, bookId, activeTab, contentType, response); setReleasesBySource((prev) => ({ ...prev, [activeTab]: response })); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to fetch releases'; @@ -1550,7 +1550,7 @@ export const ReleaseModal = ({ const bookId = book.provider_id; // Clear cache and state - const key = getCacheKey(provider, bookId, activeTab); + const key = getCacheKey(provider, bookId, activeTab, contentType); releaseCache.delete(key); cacheTimestamps.delete(key); setExpandedBySource((prev) => { @@ -1577,7 +1577,7 @@ export const ReleaseModal = ({ const response = await getReleases( provider, bookId, activeTab, book.title, book.author, false, languagesParam, contentType ); - setCachedReleases(provider, bookId, activeTab, response); + setCachedReleases(provider, bookId, activeTab, contentType, response); setReleasesBySource((prev) => ({ ...prev, [activeTab]: response })); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to fetch releases';